mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
4826 lines
166 KiB
ObjectPascal
4826 lines
166 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, WinFirewall, Forms, Controls, Graphics, Dialogs,
|
||
StdCtrls, ExtCtrls, ComCtrls, Buttons, Menus, Math, Types,
|
||
LCLIntf, LCLType, GraphType,
|
||
HPSDRProtocol, HPSDRNetwork,
|
||
WDSPEngine, AudioOutput,
|
||
IntfGraphics, FPImage,
|
||
Settings,
|
||
WebServer;
|
||
|
||
const
|
||
CLR_BG = TColor($00101010);
|
||
CLR_PANEL = TColor($00181818);
|
||
CLR_BORDER = TColor($00303030);
|
||
CLR_FREQ = TColor($0000FF00);
|
||
CLR_FREQ_DIM = TColor($00008000);
|
||
CLR_AMBER = TColor($0000AAFF);
|
||
CLR_ACTIVE = TColor($00003300);
|
||
CLR_INACTIVE = TColor($00202020);
|
||
CLR_TEXT = TColor($00CCCCCC);
|
||
CLR_TEXTDIM = TColor($00666666);
|
||
CLR_METER_ON = TColor($0000CC44);
|
||
CLR_METER_OVR= TColor($000000CC);
|
||
CLR_SPECTRUM = TColor($0044FF44);
|
||
|
||
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');
|
||
|
||
|
||
|
||
WFALL_PALETTE: array[0..255] of LongWord = (
|
||
$00000000, $00020000, $00050000, $00080000, $000A0000, $000D0000, $00100000, $00120000,
|
||
$00150000, $00180000, $001A0000, $001D0000, $00200000, $00220000, $00250000, $00280000,
|
||
$002A0000, $002D0000, $00300000, $00320000, $00350000, $00380000, $003A0000, $003D0000,
|
||
$00400000, $00420000, $00450000, $00480000, $004A0000, $004D0000, $00500000, $00530000,
|
||
$00560000, $005A0000, $005D0000, $00600000, $00640000, $00670000, $006A0000, $006E0000,
|
||
$00710000, $00740000, $00780000, $007B0000, $007E0000, $00820000, $00850000, $00880000,
|
||
$008C0000, $008F0000, $00920000, $00960000, $00990000, $009C0000, $00A00000, $00A30000,
|
||
$00A60000, $00AA0000, $00AD0000, $00B00000, $00B40000, $00B60200, $00B90500, $00BB0700,
|
||
$00BE0A00, $00C00C00, $00C30F00, $00C51100, $00C81400, $00CA1600, $00CD1900, $00CF1B00,
|
||
$00D21E00, $00D42000, $00D72300, $00D92500, $00DC2800, $00DE2A00, $00E12D00, $00E32F00,
|
||
$00E63200, $00E73700, $00E83C00, $00E94100, $00EB4600, $00EC4B00, $00ED5000, $00EE5500,
|
||
$00F05A00, $00F15F00, $00F26400, $00F36900, $00F56E00, $00F67300, $00F77800, $00F87D00,
|
||
$00FA8200, $00FB8700, $00FC8C00, $00FD9100, $00FF9600, $00FD9A00, $00FB9E00, $00F9A200,
|
||
$00F8A600, $00F6AA00, $00F4AE00, $00F2B200, $00F1B600, $00EFBA00, $00EDBE00, $00EBC200,
|
||
$00EAC600, $00E8CA00, $00E6CE00, $00E4D200, $00E3D600, $00E1DA00, $00DFDE00, $00DDE200,
|
||
$00DCE600, $00D6E700, $00D0E800, $00CAE900, $00C4EB00, $00BEEC00, $00B8ED00, $00B2EE00,
|
||
$00ACF000, $00A6F100, $00A0F200, $009AF300, $0094F500, $008EF600, $0088F700, $0082F800,
|
||
$007CFA00, $0076FB00, $0070FC00, $006AFD00, $0064FF00, $005FFF04, $005AFF08, $0055FF0C,
|
||
$0050FF10, $004BFF14, $0046FF18, $0041FF1C, $003CFF20, $0037FF24, $0032FF28, $002CFF2C,
|
||
$0028FF30, $0023FF34, $001EFF38, $0019FF3C, $0014FF40, $000FFF44, $000AFF48, $0005FF4C,
|
||
$0000FF50, $0000FF56, $0000FF5C, $0000FF62, $0000FF68, $0000FF6E, $0000FF74, $0000FF7A,
|
||
$0000FF80, $0000FF86, $0000FF8C, $0000FF92, $0000FF98, $0000FF9E, $0000FFA4, $0000FFAA,
|
||
$0000FFB0, $0000FFB6, $0000FFBC, $0000FFC2, $0000FFC8, $0000FDCA, $0000FBCD, $0000F9D0,
|
||
$0000F8D3, $0000F6D5, $0000F4D8, $0000F2DB, $0000F1DE, $0000EFE0, $0000EDE3, $0000EBE6,
|
||
$0000EAE9, $0000E8EB, $0000E6EE, $0000E4F1, $0000E3F4, $0000E1F6, $0000DFF9, $0000DDFC,
|
||
$0000DCFF, $0000D7FF, $0000D2FF, $0000CDFF, $0000C8FF, $0000C3FF, $0000BEFF, $0000B9FF,
|
||
$0000B4FF, $0000AFFF, $0000AAFF, $0000A5FF, $0000A0FF, $00009BFF, $000096FF, $000091FF,
|
||
$00008CFF, $000087FF, $000082FF, $00007DFF, $000078FF, $000073FF, $00006FFF, $00006AFF,
|
||
$000066FF, $000061FF, $00005DFF, $000058FF, $000054FF, $00004FFF, $00004BFF, $000046FF,
|
||
$000042FF, $00003DFF, $000039FF, $000034FF, $000030FF, $00002BFF, $000027FF, $000022FF,
|
||
$00001EFF, $00112DFF, $00223CFF, $00334BFF, $00445AFF, $005569FF, $006678FF, $007787FF,
|
||
$008896FF, $0099A5FF, $00AAB4FF, $00BBC3FF, $00CCD2FF, $00DDE1FF, $00EEF0FF, $00FFFFFF
|
||
);
|
||
|
||
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;
|
||
public
|
||
constructor Create(AForm: TObject; FW, SW, SV: Double; PLL: 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;
|
||
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; // таймер должен перерисовать спектр/водопад
|
||
// Маркер правой кнопки: вертикальная линия на спектре и водопаде
|
||
FMarkerActive: Boolean; // True = линия видима
|
||
FMarkerX: Integer; // X в пикселях (относительно ширины панели)
|
||
FDriveLevel: Byte;
|
||
FRunning: Boolean;
|
||
FTransmitting: Boolean;
|
||
FMuted: Boolean;
|
||
FVolume: Integer;
|
||
FLastSMeter: Double;
|
||
FSMeterPeak: Double; // верхняя граница светлой зоны
|
||
FSMeterMin: Double; // нижняя граница светлой зоны
|
||
FSMeterAvg: Double; // сглаженное среднее (EMA)
|
||
FRXPacketCount: LongWord; // счётчик принятых IQ пакетов
|
||
FRXStartTime: QWord; // GetTickCount64 момента нажатия START
|
||
FRXLastPktTime: QWord; // GetTickCount64 последнего принятого пакета
|
||
FActiveDDC: Integer; // DDC index для текущей платы (0 или 2)
|
||
FLastDDCSeq: LongWord; // последний seq (пишется из сетевого потока)
|
||
FLastDDCIndex: Integer; // последний DDC index
|
||
FLastFwdW: Double;
|
||
FLastSWR: Double;
|
||
|
||
// ---- Spectrum ----
|
||
FSpectrumBuf: array[0..1023] of Single;
|
||
FWfAvgBuf: array[0..1023] of Single;
|
||
FSpectrumBitmap: TBitmap;
|
||
FWfAGCEnabled: Boolean;
|
||
FWfNFEnabled: Boolean;
|
||
FWfHigh: Double;
|
||
FWfLow: Double;
|
||
FWaterfallBitmap:TBitmap;
|
||
FWaterfallTemp: TBitmap;
|
||
FSpectrumWidth: Integer;
|
||
FSpectrumHeight: Integer;
|
||
FWaterfallHeight:Integer;
|
||
// ---- Splitter между спектром и водопадом ----
|
||
FSplitter: TPanel; // визуальная полоса-разделитель
|
||
FSplitterDrag: Boolean; // True = идёт перетаскивание
|
||
FSplitterDragY0: Integer; // Y мыши в начале перетаскивания (в координатах PanelRight)
|
||
FSplitterSH0: Integer; // FSpectrumHeight в начале перетаскивания
|
||
FSplitterRatio: Double; // соотношение высоты спектра (0.15..0.85), сохраняется
|
||
// Водопад: прямой массив пикселей BGRA + RawImage bitmap без IntfImage roundtrip
|
||
FWfPixels: array of LongWord; // W×H пикселей, строка 0 = верх (новейшая)
|
||
FWfBitmap: TBitmap;
|
||
FWfBitmapW: Integer;
|
||
FWfBitmapH: Integer;
|
||
// Спектр: кэш фона (сетка+фон) — перерисовывается только при ресайзе
|
||
FGridBitmap: TBitmap;
|
||
FGridBitmapW: Integer;
|
||
FGridBitmapH: Integer;
|
||
FSpPts: array of TPoint;
|
||
FSpPtsLen: Integer;
|
||
// Градиент спектра: кэшированный IntfImage — без alloc каждый кадр
|
||
FSpecGradImg: TLazIntfImage;
|
||
FSpecGradBmp: TBitmap; // кэш bitmap для градиента — без alloc каждый кадр
|
||
// Водопад: кэшированный IntfImage — без alloc/free каждый кадр
|
||
FWfIntfImg: TLazIntfImage;
|
||
// Кэш линейки частот — перерисовывается только при смене FCenterFreq/FVfoA
|
||
FRulerBitmap: TBitmap;
|
||
FRulerLastFreq: Double;
|
||
FRulerLastVfo: Double;
|
||
// --- Settings ---
|
||
FSettings: TSettingsManager;
|
||
FWebServer: TWebServer; // веб-интерфейс (порт 8080)
|
||
// Временные поля для передачи параметров в Synchronize-методы
|
||
FWebSyncFreq: Double;
|
||
FWebSyncInt: Integer;
|
||
FWebSyncBool: Boolean;
|
||
FWebSyncM: TThreadMethod;
|
||
FCurrentBand: Integer; // текущий активный диапазон 0..10
|
||
FDevMAC: array[0..5] of Byte; // MAC подключённого трансивера
|
||
FPendingDev: THPSDRDevice; // устройство ожидающее открытия WDSP
|
||
FBandCache: array[0..CFG_BAND_COUNT-1] of TBandSettings; // кэш диапазонов
|
||
FDevConnected: Boolean; // True после первого подключения
|
||
FCenterFreq: Double; // центр спектра = LO (DDC). При CTUN ON не меняется при кручении VFO
|
||
FSpanHz: Double;
|
||
FSampleRate: Integer; // текущий DDC sample rate (Hz)
|
||
|
||
// ---- Timers ----
|
||
FMeterTimer: TTimer;
|
||
FSpectrumTimer: TTimer;
|
||
FAfterShowTimer:TTimer; // однократный таймер для пост-инициализации
|
||
|
||
// ---- Toolbar ----
|
||
PanelToolbar: TPanel;
|
||
BtnDiscover: TFlatButton;
|
||
BtnStartStop: TFlatButton; // START / STOP (connect+run в одном)
|
||
|
||
// ---- Left panel ----
|
||
PanelLeft: TPanel;
|
||
|
||
PanelVfoA: TPanel;
|
||
LblVfoALabel: TLabel;
|
||
FreqDispA: TFreqDisplay;
|
||
|
||
PanelVfoB: TPanel;
|
||
LblVfoBLabel: TLabel;
|
||
FreqDispB: TFreqDisplay;
|
||
|
||
PanelVfoButtons: TPanel;
|
||
BtnVfoSwap: TFlatButton;
|
||
BtnVfoACopyB: TFlatButton;
|
||
BtnVfoBCopyA: TFlatButton;
|
||
|
||
PanelBands: TPanel;
|
||
BtnBand: array[0..BAND_COUNT-1] of TFlatButton;
|
||
|
||
PanelMode: TPanel;
|
||
BtnMode: array[0..MODE_COUNT-1] of TFlatButton;
|
||
|
||
PanelFilter: TPanel;
|
||
BtnFilter: array[0..FILT_COUNT-1] of TFlatButton;
|
||
BtnCTun: TFlatButton;
|
||
|
||
PanelRX: TPanel;
|
||
LblAGC: TLabel;
|
||
BtnAGCMode: array[0..4] of TFlatButton; // FAST MED SLOW LONG OFF
|
||
LblAGCTop: TLabel; // показывает значение уровня
|
||
TrkAGC: TTrackBar; // ползунок уровня AGC
|
||
LblVol: TLabel;
|
||
TrkVolume: TTrackBar;
|
||
BtnNR: TFlatButton;
|
||
BtnNB: TFlatButton;
|
||
BtnSNB: TFlatButton;
|
||
BtnANF: TFlatButton;
|
||
BtnMute: TFlatButton;
|
||
BtnWfAGC: TFlatButton;
|
||
BtnWfNF: TFlatButton;
|
||
|
||
PanelSMeterRight: TPanel; // контейнер S-метра справа
|
||
PbSMeterRight: TPaintBox;// новый большой S-метр справа
|
||
FSmBitmap: TBitmap; // off-screen буфер для alpha-blend
|
||
|
||
PanelTX: TPanel;
|
||
LblDrv: TLabel;
|
||
TrkDrive: TTrackBar;
|
||
BtnMOX: TFlatButton;
|
||
PbFwdPower: TPaintBox;
|
||
PbSWR: TPaintBox;
|
||
LblFwdPwr: TLabel;
|
||
LblSWRVal: TLabel;
|
||
|
||
// ---- Right panel ----
|
||
PanelRight: TPanel;
|
||
PanelSpanButtons: TPanel;
|
||
LblSpan: TLabel;
|
||
BtnSpan48k: TFlatButton;
|
||
BtnSpan96k: TFlatButton;
|
||
BtnSpan192k: TFlatButton;
|
||
BtnSpan384k: TFlatButton;
|
||
BtnSpan768k: TFlatButton;
|
||
BtnSpan1536k: TFlatButton;
|
||
BtnHidePanel: TFlatButton; // скрыть/показать левую панель
|
||
PbSpectrum: TPaintBox;
|
||
PbRuler: TPaintBox; // полоса частотных меток между спектром и водопадом
|
||
PanelSplitter: TPanel; // перетаскиваемый разделитель спектр/водопад
|
||
PbWaterfall: TPaintBox;
|
||
|
||
// ---- Status bar ----
|
||
StatusBar1: TStatusBar;
|
||
|
||
// ---- Helpers ----
|
||
procedure BuildUI;
|
||
procedure ApplyDarkTheme;
|
||
procedure StyleButton(B: TFlatButton; Active: Boolean = False);
|
||
procedure StyleSpanButton(B: TFlatButton; Active: Boolean = False);
|
||
procedure PositionSpanOverlayButtons;
|
||
procedure UpdateVfoDisplay;
|
||
function FormatFreq(Hz: Double): string;
|
||
function ScaleX(X, Total, Width: Integer): Integer;
|
||
|
||
procedure DrawSMeterWide(ACanvas: TCanvas; R: TRect; Value, Peak, MinVal: Double;
|
||
out ZX1, ZX2, ZY1, ZY2: Integer);
|
||
procedure DrawSMeterZone(ACanvas: TCanvas; R: TRect; X1, X2, Y1, Y2: Integer);
|
||
procedure PbSMeterRightPaint(Sender: TObject);
|
||
procedure DrawBarMeter(ACanvas: TCanvas; R: TRect;
|
||
Value, MaxVal: Double; BarColor: TColor);
|
||
procedure DrawSpectrum;
|
||
procedure DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer);
|
||
procedure DrawMarkerLine(C: TCanvas; W, H: Integer);
|
||
procedure DrawWaterfall;
|
||
procedure ResetWfAvgBuf;
|
||
procedure ResizeSMeter;
|
||
procedure FillDemoSpectrum;
|
||
procedure ResetSpectrumBuf;
|
||
procedure ResizeSpectrumPanels;
|
||
procedure RecreateDSPEngine(ASampleRate: Integer);
|
||
|
||
// 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);
|
||
|
||
// Public UI update (called from sync objects)
|
||
procedure DoAddDevice(const Dev: THPSDRDevice; const Entry: string);
|
||
procedure DoUpdateStatus(FwdW, SWRV, SupplyV: Double; PLLLock: Boolean);
|
||
procedure DoUpdateDDCSeq(DDCIdx: Integer; Seq: LongWord);
|
||
procedure DoUpdateDDCSeqOrNoDevice(DDCIdx: Integer; Seq: LongWord);
|
||
|
||
// Event handlers
|
||
procedure BtnDiscoverClick(Sender: TObject);
|
||
procedure BtnDiscoverFromDialog(Sender: TObject);
|
||
procedure BtnStartStopClick(Sender: TObject);
|
||
procedure FreqDispAChanged(Sender: TObject; NewFreq: Int64);
|
||
procedure ApplyVfoA(NewFreq: Int64);
|
||
procedure FreqDispAClick(Sender: TObject);
|
||
procedure FreqDispBClick(Sender: TObject);
|
||
procedure ActivateVfo(Idx: Integer);
|
||
function ActiveVfoFreq: Int64;
|
||
// Настройки
|
||
procedure SaveCurrentBand;
|
||
procedure RestoreBand(BandIdx: Integer);
|
||
procedure SaveAllAndExit;
|
||
procedure RestoreWindowBounds;
|
||
function MakeGlobalSettings: TGlobalSettings;
|
||
function MakeBandSettings: TBandSettings;
|
||
procedure FreqDispBChanged(Sender: TObject; NewFreq: Int64);
|
||
procedure BtnBandClick(Sender: TObject);
|
||
procedure ApplyModeFilter;
|
||
procedure BtnModeClick(Sender: TObject);
|
||
procedure BtnFilterClick(Sender: TObject);
|
||
procedure BtnCTunClick(Sender: TObject);
|
||
procedure UpdateFilterButtons;
|
||
procedure PbSpectrumMouseDown(Sender: TObject; Button: TMouseButton;
|
||
Shift: TShiftState; X, Y: Integer);
|
||
procedure PbSpectrumMouseMove(Sender: TObject; Shift: TShiftState;
|
||
X, Y: Integer);
|
||
procedure PbSpectrumMouseUp(Sender: TObject; Button: TMouseButton;
|
||
Shift: TShiftState; X, Y: Integer);
|
||
procedure DrawRuler;
|
||
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);
|
||
// Synchronize-обёртки (выполняются в UI-потоке)
|
||
procedure SyncWebFreq;
|
||
procedure SyncWebMode;
|
||
procedure SyncWebFilter;
|
||
procedure SyncWebAGC;
|
||
procedure SyncWebAGCTop;
|
||
procedure SyncWebBand;
|
||
procedure SyncWebSpan;
|
||
procedure SyncWebVolume;
|
||
procedure SyncWebWfAGC;
|
||
procedure SyncWebWfNF;
|
||
procedure SyncWebRun;
|
||
procedure SyncWebMute;
|
||
procedure SyncWebCtun;
|
||
procedure SyncWebNR;
|
||
procedure SyncWebNB;
|
||
procedure SyncWebSNB;
|
||
procedure SyncWebANF;
|
||
procedure BtnWfAGCClick(Sender: TObject);
|
||
procedure BtnWfNFClick(Sender: TObject);
|
||
procedure BtnHidePanelClick(Sender: TObject);
|
||
procedure PositionVfoOverlay;
|
||
procedure OnModeFilterSelect(Mode: Integer; FilterBW: Integer);
|
||
procedure PbSpectrumDblClick(Sender: TObject);
|
||
procedure TrkDriveChange(Sender: TObject);
|
||
procedure TrkVolumeChange(Sender: TObject);
|
||
procedure BtnAGCModeClick(Sender: TObject);
|
||
procedure TrkAGCChange(Sender: TObject);
|
||
procedure PbFwdPowerPaint(Sender: TObject);
|
||
procedure PbSWRPaint(Sender: TObject);
|
||
procedure PbSpectrumPaint(Sender: TObject);
|
||
procedure PbRulerPaint(Sender: TObject);
|
||
procedure PbWaterfallPaint(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);
|
||
|
||
published
|
||
// Обработчики событий формы — должны быть в published для RTTI/LFM
|
||
procedure FormCreate(Sender: TObject);
|
||
procedure FormDestroy(Sender: TObject);
|
||
procedure FormClose(Sender: TObject; var CloseAction: TCloseAction);
|
||
procedure FormMouseWheel(Sender: TObject; Shift: TShiftState;
|
||
WheelDelta: Integer; MousePos: TPoint;
|
||
var Handled: Boolean);
|
||
end;
|
||
|
||
var
|
||
MainForm: TMainForm;
|
||
|
||
implementation
|
||
|
||
{$R *.lfm}
|
||
|
||
// ===========================================================================
|
||
// Общая функция декодирования типа платы — используется везде
|
||
// ===========================================================================
|
||
|
||
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: Boolean);
|
||
begin
|
||
inherited Create;
|
||
FForm := AForm;
|
||
FFwdW := FW;
|
||
FSWRV := SW;
|
||
FSupplyV := SV;
|
||
FPLLLock := PLL;
|
||
end;
|
||
|
||
procedure TStatusUISync.Execute;
|
||
begin
|
||
TMainForm(FForm).DoUpdateStatus(FFwdW, FSWRV, FSupplyV, FPLLLock);
|
||
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;
|
||
begin
|
||
Result.Volume := FVolume;
|
||
Result.DriveLevel := FDriveLevel;
|
||
Result.ActiveVfo := FActiveVfo;
|
||
Result.NRMode := BtnNR.Tag;
|
||
Result.NBMode := BtnNB.Tag;
|
||
Result.SNBEnabled := BtnSNB.Tag <> 0;
|
||
Result.ANFEnabled := BtnANF.Tag <> 0;
|
||
Result.AGCSlope := 0;
|
||
Result.AGCHangThreshold := 100;
|
||
Result.WfAGCEnabled := FWfAGCEnabled;
|
||
Result.WfNFEnabled := FWfNFEnabled;
|
||
Result.LastBand := FCurrentBand;
|
||
Result.SampleRate := FSampleRate;
|
||
end;
|
||
|
||
procedure TMainForm.SaveCurrentBand;
|
||
begin
|
||
if not FDevConnected then Exit;
|
||
FBandCache[FCurrentBand] := MakeBandSettings;
|
||
FSettings.SaveBand(FDevMAC, FCurrentBand, FBandCache[FCurrentBand]);
|
||
end;
|
||
|
||
procedure TMainForm.RestoreBand(BandIdx: Integer);
|
||
var
|
||
B: TBandSettings;
|
||
i: Integer;
|
||
begin
|
||
if (BandIdx < 0) or (BandIdx >= CFG_BAND_COUNT) then Exit;
|
||
B := FBandCache[BandIdx];
|
||
|
||
// --- Кнопки диапазонов ---
|
||
for i := 0 to BAND_COUNT - 1 do
|
||
StyleButton(BtnBand[i], i = BandIdx);
|
||
// Сбрасываем waterfall AGC/NF при смене диапазона
|
||
FWfHigh := -50.0;
|
||
FWfLow := -120.0;
|
||
// Сбрасываем EMA буфер — иначе старый спектр "просочится" в новый диапазон
|
||
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;
|
||
|
||
// --- CTUN ---
|
||
FCTun := B.CTun;
|
||
StyleButton(BtnCTun, FCTun);
|
||
if FWDSPReady and not FCTun then
|
||
FDSPEngine.SetShift(0.0);
|
||
|
||
// --- Sample Rate / Span ---
|
||
// SampleRate — глобальный, не per-band. Не восстанавливаем из диапазона.
|
||
// Обновляем только FSpanHz для отображения (берём текущий FSampleRate)
|
||
FSpanHz := FSampleRate;
|
||
StyleSpanButton(BtnSpan48k, FSampleRate = 48000);
|
||
StyleSpanButton(BtnSpan96k, FSampleRate = 96000);
|
||
StyleSpanButton(BtnSpan192k, FSampleRate = 192000);
|
||
StyleSpanButton(BtnSpan384k, FSampleRate = 384000);
|
||
StyleSpanButton(BtnSpan768k, FSampleRate = 768000);
|
||
StyleSpanButton(BtnSpan1536k, FSampleRate = 1536000);
|
||
|
||
// --- VFO A: центрируем на новой частоте, сбрасываем shift ---
|
||
FCenterFreq := B.VfoA; // DDC = центр диапазона
|
||
FVfoB := B.VfoB;
|
||
FreqDispB.Frequency := Round(FVfoB);
|
||
|
||
// ApplyVfoA обновит сеть, FreqDisp, shift, перерисует
|
||
ApplyVfoA(Round(B.VfoA));
|
||
end;
|
||
|
||
procedure TMainForm.SaveAllAndExit;
|
||
begin
|
||
if FDevConnected then
|
||
begin
|
||
SaveCurrentBand;
|
||
FSettings.SaveGlobal(FDevMAC, MakeGlobalSettings);
|
||
FSettings.Save;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.FormCreate(Sender: TObject);
|
||
var
|
||
bi_, StartupRate: 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 := 100;
|
||
FRunning := False;
|
||
FTransmitting := False;
|
||
FMuted := False;
|
||
FVolume := 70;
|
||
FCenterFreq := FVfoA;
|
||
FSpanHz := 192000;
|
||
FSampleRate := 192000;
|
||
FCurrentBand := 5; // 20m по умолчанию
|
||
// Waterfall normalization
|
||
FWfHigh := -50.0; // начальная верхняя граница dBm
|
||
// WF avg buffer — инициализируем значением шумового пола
|
||
// FWfAvgBuf инициализируется в ResetSpectrumBuf
|
||
FWfLow := -120.0; // начальная нижняя граница dBm
|
||
FWfAGCEnabled := False;
|
||
FWfNFEnabled := False;
|
||
FDevConnected := False;
|
||
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.Start;
|
||
FSMeterPeak := -130;
|
||
FSMeterMin := -130;
|
||
FSMeterAvg := -130;
|
||
FRXPacketCount := 0;
|
||
FActiveDDC := 0;
|
||
FLastDDCSeq := 0;
|
||
FLastDDCIndex := 0;
|
||
FLastFwdW := 0;
|
||
FLastSWR := 1;
|
||
FDeviceCount := 0;
|
||
FDeviceDialog := TDeviceDialog.Create(Self);
|
||
FDeviceDialog.OnDiscover := BtnDiscoverFromDialog;
|
||
FVfoOverlay := TVfoOverlay.Create(Self);
|
||
FVfoOverlay.OnSelect := OnModeFilterSelect;
|
||
FVfoOverlay.Visible := False;
|
||
FPanelHidden := False;
|
||
FSplitterRatio := 0.40; // 40% спектр, 60% водопад по умолчанию
|
||
FSplitterDrag := False;
|
||
|
||
FSpectrumBitmap := TBitmap.Create;
|
||
FWaterfallBitmap := TBitmap.Create;
|
||
FWaterfallTemp := TBitmap.Create;
|
||
FSpecGradImg := nil;
|
||
FSpecGradBmp := TBitmap.Create;
|
||
FWfIntfImg := nil;
|
||
FWfBitmap := TBitmap.Create;
|
||
FWfBitmapW := 0;
|
||
FWfBitmapH := 0;
|
||
FGridBitmap := TBitmap.Create;
|
||
FGridBitmapW := 0;
|
||
FGridBitmapH := 0;
|
||
FSpPtsLen := 0;
|
||
FRulerBitmap := TBitmap.Create;
|
||
FRulerLastFreq := -1.0;
|
||
FRulerLastVfo := -1.0;
|
||
ResetSpectrumBuf;
|
||
|
||
FNetwork := THPSDRNetwork.Create;
|
||
FNetwork.OnDeviceFound := OnDeviceFound;
|
||
FNetwork.OnHPStatus := OnHPStatusCB;
|
||
FNetwork.OnDDCIQ := OnDDCIQCB;
|
||
FNetwork.OnMicPacket := OnMicPacketCB;
|
||
|
||
BuildUI;
|
||
StyleSpanButton(BtnSpan48k, FSampleRate = 48000);
|
||
StyleSpanButton(BtnSpan96k, FSampleRate = 96000);
|
||
StyleSpanButton(BtnSpan192k, FSampleRate = 192000);
|
||
StyleSpanButton(BtnSpan384k, FSampleRate = 384000);
|
||
StyleSpanButton(BtnSpan768k, FSampleRate = 768000);
|
||
StyleSpanButton(BtnSpan1536k, FSampleRate = 1536000);
|
||
ResizeSMeter;
|
||
ApplyDarkTheme;
|
||
UpdateVfoDisplay;
|
||
UpdateFilterButtons;
|
||
|
||
// DSP Engine — создаём объект, Open вызовется при нажатии START
|
||
// (FDSPEngine.Open загружает libwdsp и занимает ~1-2 сек)
|
||
FWDSPReady := False;
|
||
// BufSize=512: FBufSize=2048 @ 192kHz, FAudioBufSize=512 @ 48kHz.
|
||
// Уменьшение с 1024 вдвое сокращает время построения downsampler в OpenChannel RXA
|
||
// (~1300ms → ~650ms). Латентность: 512/48000 ≈ 10.7ms — допустимо для SDR.
|
||
FDSPEngine := TWDSPEngine.Create(FSampleRate, 48000, 512);
|
||
FDSPEngine.OnAudio := OnAudioReady;
|
||
FDSPEngine.OnSpectrum := OnSpectrumReady;
|
||
|
||
// Audio output — создаём объект сейчас, открываем после показа формы
|
||
// (Pa_Initialize на Linux пишет в stderr до перехвата сигналов FPC)
|
||
FAudioOut := TAudioOutput.Create(48000);
|
||
|
||
FMeterTimer := TTimer.Create(Self);
|
||
FMeterTimer.Interval := 100;
|
||
FMeterTimer.OnTimer := MeterTimerTick;
|
||
FMeterTimer.Enabled := True;
|
||
|
||
|
||
FSpectrumTimer := TTimer.Create(Self);
|
||
FSpectrumTimer.Interval := 50;
|
||
FSpectrumTimer.OnTimer := SpectrumTimerTick;
|
||
FSpectrumTimer.Enabled := True;
|
||
|
||
// Однократный таймер — инициализация аудио после показа формы
|
||
FAfterShowTimer := TTimer.Create(Self);
|
||
FAfterShowTimer.Interval := 200;
|
||
FAfterShowTimer.OnTimer := AfterShowTick;
|
||
FAfterShowTimer.Enabled := True;
|
||
|
||
OnMouseWheel := FormMouseWheel;
|
||
|
||
// Проверяем и при необходимости добавляем исключение в Windows Firewall
|
||
// (UDP входящий трафик для HPSDR Protocol 2)
|
||
{$IFDEF WINDOWS}
|
||
FirewallEnsureAllowed(ParamStr(0), 'OpenHPSDR Transceiver');
|
||
{$ENDIF}
|
||
end;
|
||
|
||
procedure TMainForm.FormDestroy(Sender: TObject);
|
||
begin
|
||
FMeterTimer.Enabled := False;
|
||
FSpectrumTimer.Enabled := False;
|
||
if FNetwork.Running then
|
||
FNetwork.SetRunAndFreq(False, FCenterFreq, FCenterFreq, 0);
|
||
FNetwork.Disconnect;
|
||
FNetwork.Free;
|
||
FAudioOut.Close;
|
||
FAudioOut.Free;
|
||
FDSPEngine.Close;
|
||
FDSPEngine.Free;
|
||
FSpectrumBitmap.Free;
|
||
FWaterfallBitmap.Free;
|
||
FWaterfallTemp.Free;
|
||
FRulerBitmap.Free;
|
||
FWfBitmap.Free;
|
||
FGridBitmap.Free;
|
||
FreeAndNil(FSpecGradImg);
|
||
FSpecGradBmp.Free;
|
||
FreeAndNil(FWfIntfImg);
|
||
|
||
// Сохраняем настройки при закрытии
|
||
if FDevConnected then
|
||
begin
|
||
SaveCurrentBand;
|
||
FSettings.SaveGlobal(FDevMAC, MakeGlobalSettings);
|
||
end;
|
||
// Размер окна сохраняем всегда (не зависит от подключения)
|
||
FSettings.SaveStartupPreview(FVfoA, FVfoB, FSampleRate);
|
||
FSettings.SaveWindowBounds(Left, Top, Width, Height);
|
||
FSettings.Save;
|
||
FSettings.Free;
|
||
FWebServer.Stop;
|
||
FWebServer.Free;
|
||
end;
|
||
|
||
procedure TMainForm.FormClose(Sender: TObject; var CloseAction: TCloseAction);
|
||
begin
|
||
// Останавливаем трансивер как при нажатии STOP
|
||
if FNetwork.Connected then
|
||
begin
|
||
if FRunning then
|
||
begin
|
||
FNetwork.UpdateState(FCenterFreq, FCenterFreq, 0, False, True, True);
|
||
FNetwork.SetRunAndFreq(False, FCenterFreq, FCenterFreq, 0);
|
||
FRunning := False;
|
||
FTransmitting := False;
|
||
if FWDSPReady then FDSPEngine.SetTXRun(False);
|
||
end;
|
||
FNetwork.Disconnect;
|
||
end;
|
||
CloseAction := caFree;
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// Build UI (без анонимных процедур и inline var)
|
||
// ===========================================================================
|
||
|
||
procedure TMainForm.BuildUI;
|
||
const
|
||
BTN_H = 26;
|
||
BTN_SM = 24;
|
||
LEFT_W = 232;
|
||
var
|
||
i, X, Y, W: Integer;
|
||
B: TFlatButton;
|
||
|
||
function MakeBtn(Parent: TWinControl; const Cap: string;
|
||
ALeft, ATop, AW, AH: Integer; Handler: TNotifyEvent): TFlatButton;
|
||
begin
|
||
Result := TFlatButton.Create(Self);
|
||
Result.Parent := Parent;
|
||
Result.Caption := Cap;
|
||
Result.Left := ALeft;
|
||
Result.Top := ATop;
|
||
Result.Width := AW;
|
||
Result.Height := AH;
|
||
Result.OnClick := Handler;
|
||
StyleButton(Result, False);
|
||
end;
|
||
|
||
procedure MakeLbl(Parent: TWinControl; const Cap: string; ALeft, ATop: Integer);
|
||
var
|
||
L: TLabel;
|
||
begin
|
||
L := TLabel.Create(Self);
|
||
L.Parent := Parent;
|
||
L.Caption := Cap;
|
||
L.Left := ALeft;
|
||
L.Top := ATop;
|
||
L.Font.Color := CLR_TEXTDIM;
|
||
L.Font.Name := 'Courier New';
|
||
L.Font.Size := 7;
|
||
end;
|
||
|
||
begin
|
||
// ---- Toolbar ----
|
||
PanelToolbar := TPanel.Create(Self);
|
||
PanelToolbar.Parent := Self;
|
||
PanelToolbar.Align := alTop;
|
||
PanelToolbar.Height := 36;
|
||
PanelToolbar.BevelOuter := bvNone;
|
||
|
||
X := 4;
|
||
BtnDiscover := MakeBtn(PanelToolbar, 'DISCOVER', X, 3, 80, BTN_H, BtnDiscoverClick);
|
||
BtnStartStop := MakeBtn(PanelToolbar, 'START', X+84, 3, 76, BTN_H, BtnStartStopClick);
|
||
|
||
// ---- Status bar ----
|
||
StatusBar1 := TStatusBar.Create(Self);
|
||
StatusBar1.Parent := Self;
|
||
StatusBar1.Align := alBottom;
|
||
StatusBar1.SimplePanel := False;
|
||
with StatusBar1.Panels do
|
||
begin
|
||
Add; Items[0].Width := 150; Items[0].Text := 'IP: --';
|
||
Add; Items[1].Width := 120; Items[1].Text := 'Board: --';
|
||
Add; Items[2].Width := 320; Items[2].Text := 'Supply: -- | FWD: -- | SWR: --';
|
||
Add; Items[3].Width := 250; Items[3].Text := 'RX: waiting...';
|
||
Add; Items[4].Width := 200; Items[4].Text := 'Not connected';
|
||
end;
|
||
|
||
// ---- Left panel ----
|
||
PanelLeft := TPanel.Create(Self);
|
||
PanelLeft.Parent := Self;
|
||
PanelLeft.Align := alLeft;
|
||
PanelLeft.Width := LEFT_W;
|
||
PanelLeft.BevelOuter := bvNone;
|
||
|
||
Y := 2;
|
||
|
||
// VFO A
|
||
PanelVfoA := TPanel.Create(Self);
|
||
PanelVfoA.Parent := PanelLeft;
|
||
PanelVfoA.SetBounds(0, Y, LEFT_W, 56);
|
||
PanelVfoA.BevelOuter := bvNone;
|
||
|
||
LblVfoALabel := TLabel.Create(Self);
|
||
LblVfoALabel.Parent := PanelVfoA;
|
||
LblVfoALabel.Caption := 'VFO-A';
|
||
LblVfoALabel.Left := 4; LblVfoALabel.Top := 2;
|
||
LblVfoALabel.Font.Name := 'Courier New'; LblVfoALabel.Font.Size := 7;
|
||
LblVfoALabel.Font.Color := CLR_TEXTDIM;
|
||
|
||
FreqDispA := TFreqDisplay.Create(Self);
|
||
FreqDispA.Parent := PanelVfoA;
|
||
FreqDispA.SetBounds(0, 14, LEFT_W, 36);
|
||
FreqDispA.FontSize := 20;
|
||
FreqDispA.FontName := 'Courier New';
|
||
FreqDispA.Frequency := Round(FVfoA);
|
||
FreqDispA.ColorNormal := CLR_FREQ;
|
||
FreqDispA.ColorHover := TColor($0040DDFF);
|
||
FreqDispA.ColorDim := CLR_TEXTDIM;
|
||
FreqDispA.OnChange := FreqDispAChanged;
|
||
FreqDispA.OnClick := FreqDispAClick;
|
||
|
||
Inc(Y, 58);
|
||
|
||
// VFO B
|
||
PanelVfoB := TPanel.Create(Self);
|
||
PanelVfoB.Parent := PanelLeft;
|
||
PanelVfoB.SetBounds(0, Y, LEFT_W, 46);
|
||
PanelVfoB.BevelOuter := bvNone;
|
||
|
||
LblVfoBLabel := TLabel.Create(Self);
|
||
LblVfoBLabel.Parent := PanelVfoB;
|
||
LblVfoBLabel.Caption := 'VFO-B';
|
||
LblVfoBLabel.Left := 4; LblVfoBLabel.Top := 2;
|
||
LblVfoBLabel.Font.Name := 'Courier New'; LblVfoBLabel.Font.Size := 7;
|
||
LblVfoBLabel.Font.Color := CLR_TEXTDIM;
|
||
|
||
FreqDispB := TFreqDisplay.Create(Self);
|
||
FreqDispB.Parent := PanelVfoB;
|
||
FreqDispB.SetBounds(0, 14, LEFT_W, 28);
|
||
FreqDispB.FontSize := 14;
|
||
FreqDispB.FontName := 'Courier New';
|
||
FreqDispB.Frequency := Round(FVfoB);
|
||
FreqDispB.ColorNormal := CLR_FREQ_DIM;
|
||
FreqDispB.ColorHover := TColor($0040DDFF);
|
||
FreqDispB.ColorDim := CLR_TEXTDIM;
|
||
FreqDispB.OnChange := FreqDispBChanged;
|
||
FreqDispB.OnClick := FreqDispBClick;
|
||
|
||
Inc(Y, 50);
|
||
|
||
// VFO buttons
|
||
PanelVfoButtons := TPanel.Create(Self);
|
||
PanelVfoButtons.Parent := PanelLeft;
|
||
PanelVfoButtons.SetBounds(0, Y, LEFT_W, BTN_H + 4);
|
||
PanelVfoButtons.BevelOuter := bvNone;
|
||
|
||
BtnVfoSwap := MakeBtn(PanelVfoButtons, 'A<>B', 2, 2, 72, BTN_H, BtnVfoSwapClick);
|
||
BtnVfoACopyB := MakeBtn(PanelVfoButtons, 'A>B', 78, 2, 72, BTN_H, BtnVfoACopyBClick);
|
||
BtnVfoBCopyA := MakeBtn(PanelVfoButtons, 'B>A', 154, 2, 72, BTN_H, BtnVfoBCopyAClick);
|
||
|
||
Inc(Y, BTN_H + 8);
|
||
|
||
// Band selector
|
||
PanelBands := TPanel.Create(Self);
|
||
PanelBands.Parent := PanelLeft;
|
||
PanelBands.SetBounds(0, Y, LEFT_W, 74);
|
||
PanelBands.BevelOuter := bvNone;
|
||
MakeLbl(PanelBands, 'BAND', 4, 2);
|
||
W := (LEFT_W - 6) div 6;
|
||
for i := 0 to BAND_COUNT - 1 do
|
||
begin
|
||
B := MakeBtn(PanelBands, BAND_NAMES[i],
|
||
2 + (i mod 6) * W,
|
||
16 + (i div 6) * 27,
|
||
W - 2, BTN_SM, BtnBandClick);
|
||
B.Tag := i;
|
||
BtnBand[i] := B;
|
||
end;
|
||
|
||
Inc(Y, 76);
|
||
|
||
// Mode selector
|
||
PanelMode := TPanel.Create(Self);
|
||
PanelMode.Parent := PanelLeft;
|
||
PanelMode.SetBounds(0, Y, LEFT_W, 74);
|
||
PanelMode.BevelOuter := bvNone;
|
||
MakeLbl(PanelMode, 'MODE', 4, 2);
|
||
W := (LEFT_W - 6) div 4;
|
||
for i := 0 to MODE_COUNT - 1 do
|
||
begin
|
||
B := MakeBtn(PanelMode, MODE_NAMES[i],
|
||
2 + (i mod 4) * W,
|
||
16 + (i div 4) * 27,
|
||
W - 2, BTN_SM, BtnModeClick);
|
||
B.Tag := i;
|
||
BtnMode[i] := B;
|
||
StyleButton(B, i = FMode);
|
||
end;
|
||
|
||
Inc(Y, 76);
|
||
|
||
// Filter selector (10 кнопок в 2 ряда по 5)
|
||
PanelFilter := TPanel.Create(Self);
|
||
PanelFilter.Parent := PanelLeft;
|
||
PanelFilter.SetBounds(0, Y, LEFT_W, 74);
|
||
PanelFilter.BevelOuter := bvNone;
|
||
MakeLbl(PanelFilter, 'FILTER', 4, 2);
|
||
W := (LEFT_W - 6) div 5;
|
||
for i := 0 to FILT_COUNT - 1 do
|
||
begin
|
||
B := MakeBtn(PanelFilter, '---',
|
||
2 + (i mod 5) * W,
|
||
16 + (i div 5) * 27,
|
||
W - 2, BTN_SM, BtnFilterClick);
|
||
B.Tag := i;
|
||
BtnFilter[i] := B;
|
||
end;
|
||
|
||
Inc(Y, 76);
|
||
|
||
// CTUN button
|
||
BtnCTun := MakeBtn(PanelLeft, 'CTUN', 2, Y, LEFT_W div 3 - 2, BTN_H, BtnCTunClick);
|
||
BtnCTun.Tag := 0;
|
||
StyleButton(BtnCTun, FCTun);
|
||
|
||
Inc(Y, BTN_H + 4);
|
||
|
||
// RX controls
|
||
PanelRX := TPanel.Create(Self);
|
||
PanelRX.Parent := PanelLeft;
|
||
PanelRX.SetBounds(0, Y, LEFT_W, 150);
|
||
PanelRX.BevelOuter := bvNone;
|
||
|
||
// AGC mode — 5 кнопок в ряд
|
||
MakeLbl(PanelRX, 'AGC', 4, 4);
|
||
W := (LEFT_W - 10) div 5;
|
||
BtnAGCMode[0] := MakeBtn(PanelRX,'FAST', 2, 22, W, BTN_SM, BtnAGCModeClick); BtnAGCMode[0].Tag:=0;
|
||
BtnAGCMode[1] := MakeBtn(PanelRX,'MED', W+4, 22, W, BTN_SM, BtnAGCModeClick); BtnAGCMode[1].Tag:=1;
|
||
BtnAGCMode[2] := MakeBtn(PanelRX,'SLOW', 2*W+6, 22, W, BTN_SM, BtnAGCModeClick); BtnAGCMode[2].Tag:=2;
|
||
BtnAGCMode[3] := MakeBtn(PanelRX,'LONG', 3*W+8, 22, W, BTN_SM, BtnAGCModeClick); BtnAGCMode[3].Tag:=3;
|
||
BtnAGCMode[4] := MakeBtn(PanelRX,'OFF', 4*W+10, 22, W, BTN_SM, BtnAGCModeClick); BtnAGCMode[4].Tag:=4;
|
||
for i := 0 to 4 do StyleButton(BtnAGCMode[i], i = FAGCMode);
|
||
|
||
// AGC level slider
|
||
MakeLbl(PanelRX, 'THRESH', 4, 50);
|
||
TrkAGC := TTrackBar.Create(Self);
|
||
TrkAGC.Parent := PanelRX; TrkAGC.Left := 56; TrkAGC.Top := 46;
|
||
TrkAGC.Width := LEFT_W - 96; TrkAGC.Height := 20;
|
||
TrkAGC.Min := 20; TrkAGC.Max := 120; // 20..120 → −20..-120 dBm
|
||
TrkAGC.Position := FAGCTop;
|
||
TrkAGC.TickStyle := tsNone; TrkAGC.Reversed := True;
|
||
TrkAGC.OnChange := TrkAGCChange;
|
||
|
||
LblAGCTop := TLabel.Create(Self);
|
||
LblAGCTop.Parent := PanelRX;
|
||
LblAGCTop.Left := LEFT_W - 38; LblAGCTop.Top := 50;
|
||
LblAGCTop.Caption := Format('-%ddB', [FAGCTop]);
|
||
LblAGCTop.Font.Color := CLR_TEXT; LblAGCTop.Font.Name := 'Courier New';
|
||
LblAGCTop.Font.Size := 7;
|
||
|
||
// VOL
|
||
MakeLbl(PanelRX, 'VOL', 4, 74);
|
||
TrkVolume := TTrackBar.Create(Self);
|
||
TrkVolume.Parent := PanelRX; TrkVolume.Left := 34; TrkVolume.Top := 70;
|
||
TrkVolume.Width := LEFT_W - 38; TrkVolume.Height := 20;
|
||
TrkVolume.Min := 0; TrkVolume.Max := 100;
|
||
TrkVolume.Position := FVolume; TrkVolume.TickStyle := tsNone;
|
||
TrkVolume.OnChange := TrkVolumeChange;
|
||
|
||
// NR NB SNB ANF MUTE
|
||
W := (LEFT_W - 10) div 5;
|
||
BtnNR := MakeBtn(PanelRX, 'NR', 2, 96, W, BTN_SM, BtnNRClick);
|
||
BtnNB := MakeBtn(PanelRX, 'NB', W+4, 96, W, BTN_SM, BtnNBClick);
|
||
BtnSNB := MakeBtn(PanelRX, 'SNB', 2*W+6, 96, W, BTN_SM, BtnSNBClick);
|
||
BtnANF := MakeBtn(PanelRX, 'ANF', 3*W+8, 96, W, BTN_SM, BtnANFClick);
|
||
BtnMute:= MakeBtn(PanelRX, 'MUTE', 4*W+10, 96, W, BTN_SM, BtnMuteClick);
|
||
BtnWfAGC := MakeBtn(PanelRX, 'WF AGC', 2, 122, (LEFT_W - 6) div 2, BTN_SM, BtnWfAGCClick);
|
||
BtnWfNF := MakeBtn(PanelRX, 'WF NF', 4 + (LEFT_W - 6) div 2, 122, (LEFT_W - 6) div 2, BTN_SM, BtnWfNFClick);
|
||
|
||
Inc(Y, 154);
|
||
|
||
// S-Meter
|
||
|
||
Inc(Y, 52);
|
||
|
||
// TX controls
|
||
PanelTX := TPanel.Create(Self);
|
||
PanelTX.Parent := PanelLeft;
|
||
PanelTX.SetBounds(0, Y, LEFT_W, 104);
|
||
PanelTX.BevelOuter := bvNone;
|
||
MakeLbl(PanelTX, 'TX', 4, 2);
|
||
|
||
LblDrv := TLabel.Create(Self);
|
||
LblDrv.Parent := PanelTX; LblDrv.Left := 4; LblDrv.Top := 17;
|
||
LblDrv.Caption := 'DRV'; LblDrv.Font.Color := CLR_TEXTDIM;
|
||
LblDrv.Font.Name := 'Courier New'; LblDrv.Font.Size := 7;
|
||
|
||
TrkDrive := TTrackBar.Create(Self);
|
||
TrkDrive.Parent := PanelTX; TrkDrive.Left := 34; TrkDrive.Top := 14;
|
||
TrkDrive.Width := 150; TrkDrive.Height := 22;
|
||
TrkDrive.Min := 0; TrkDrive.Max := 100; TrkDrive.Position := 50;
|
||
TrkDrive.TickStyle := tsNone; TrkDrive.OnChange := TrkDriveChange;
|
||
|
||
BtnMOX := MakeBtn(PanelTX, 'MOX', 2, 38, 70, 32, BtnMOXClick);
|
||
BtnMOX.Font.Size := 12; BtnMOX.Font.Bold := True;
|
||
|
||
LblFwdPwr := TLabel.Create(Self);
|
||
LblFwdPwr.Parent := PanelTX; LblFwdPwr.Left := 80; LblFwdPwr.Top := 42;
|
||
LblFwdPwr.Caption := '0W'; LblFwdPwr.Font.Color := CLR_METER_ON;
|
||
LblFwdPwr.Font.Name := 'Courier New'; LblFwdPwr.Font.Size := 8;
|
||
|
||
LblSWRVal := TLabel.Create(Self);
|
||
LblSWRVal.Parent := PanelTX; LblSWRVal.Left := 140; LblSWRVal.Top := 42;
|
||
LblSWRVal.Caption := 'SWR:1.0'; LblSWRVal.Font.Color := CLR_TEXTDIM;
|
||
LblSWRVal.Font.Name := 'Courier New'; LblSWRVal.Font.Size := 8;
|
||
|
||
MakeLbl(PanelTX, 'PWR', 4, 76);
|
||
PbFwdPower := TPaintBox.Create(Self);
|
||
PbFwdPower.Parent := PanelTX;
|
||
PbFwdPower.SetBounds(34, 74, LEFT_W - 38, 12);
|
||
PbFwdPower.OnPaint := PbFwdPowerPaint;
|
||
|
||
MakeLbl(PanelTX, 'SWR', 4, 92);
|
||
PbSWR := TPaintBox.Create(Self);
|
||
PbSWR.Parent := PanelTX;
|
||
PbSWR.SetBounds(34, 90, LEFT_W - 38, 12);
|
||
PbSWR.OnPaint := PbSWRPaint;
|
||
|
||
// ---- Right panel ----
|
||
PanelRight := TPanel.Create(Self);
|
||
PanelRight.Parent := Self;
|
||
PanelRight.Align := alClient;
|
||
PanelRight.BevelOuter := bvNone;
|
||
PanelRight.OnResize := RightPanelResize;
|
||
|
||
PanelSpanButtons := TPanel.Create(Self);
|
||
PanelSpanButtons.Parent := PanelRight;
|
||
PanelSpanButtons.Align := alTop;
|
||
PanelSpanButtons.Height := 30;
|
||
PanelSpanButtons.BevelOuter := bvNone;
|
||
|
||
LblSpan := TLabel.Create(Self);
|
||
LblSpan.Parent := PanelSpanButtons; LblSpan.Left := 4; LblSpan.Top := 8;
|
||
LblSpan.Caption := 'SPAN:'; LblSpan.Font.Color := CLR_TEXTDIM;
|
||
LblSpan.Font.Name := 'Courier New'; LblSpan.Font.Size := 7;
|
||
|
||
BtnSpan48k := MakeBtn(PanelSpanButtons, '48k', 50, 3, 46, BTN_SM, BtnSpanClick);
|
||
BtnSpan96k := MakeBtn(PanelSpanButtons, '96k', 99, 3, 46, BTN_SM, BtnSpanClick);
|
||
BtnSpan192k := MakeBtn(PanelSpanButtons, '192k', 148, 3, 52, BTN_SM, BtnSpanClick);
|
||
BtnSpan384k := MakeBtn(PanelSpanButtons, '384k', 203, 3, 52, BTN_SM, BtnSpanClick);
|
||
BtnSpan768k := MakeBtn(PanelSpanButtons, '768k', 258, 3, 52, BTN_SM, BtnSpanClick);
|
||
BtnSpan1536k := MakeBtn(PanelSpanButtons, '1536k', 313, 3, 58, BTN_SM, BtnSpanClick);
|
||
BtnSpan48k.Tag := 48000;
|
||
BtnSpan96k.Tag := 96000;
|
||
BtnSpan192k.Tag := 192000;
|
||
BtnSpan384k.Tag := 384000;
|
||
BtnSpan768k.Tag := 768000;
|
||
BtnSpan1536k.Tag := 1536000;
|
||
StyleSpanButton(BtnSpan48k, False);
|
||
StyleSpanButton(BtnSpan96k, False);
|
||
StyleSpanButton(BtnSpan192k, True);
|
||
StyleSpanButton(BtnSpan384k, False);
|
||
StyleSpanButton(BtnSpan768k, False);
|
||
StyleSpanButton(BtnSpan1536k, False);
|
||
|
||
BtnHidePanel := MakeBtn(PanelRight, '◀', 0, 0, 28, 28, BtnHidePanelClick);
|
||
BtnHidePanel.Font.Size := 10;
|
||
BtnHidePanel.ClrNorm := TColor($00543F18);
|
||
BtnHidePanel.ClrHot := TColor($00624B1D);
|
||
BtnHidePanel.ClrActive := TColor($00624B1D);
|
||
BtnHidePanel.ClrBorder := TColor($00685624);
|
||
BtnHidePanel.ClrText := TColor($00D8E6EE);
|
||
BtnHidePanel.ClrTextAct := TColor($00F0FAFF);
|
||
BtnHidePanel.BringToFront;
|
||
|
||
// Большой S-метр — на главной форме поверх двух панелей
|
||
// S-метр: Parent=Self, перекрывает Toolbar+SpanButtons вертикально
|
||
PanelSMeterRight := TPanel.Create(Self);
|
||
PanelSMeterRight.Parent := Self;
|
||
PanelSMeterRight.BevelOuter := bvNone;
|
||
PanelSMeterRight.Color := CLR_PANEL;
|
||
PbSMeterRight := TPaintBox.Create(Self);
|
||
PbSMeterRight.Parent := PanelSMeterRight;
|
||
PbSMeterRight.Align := alClient;
|
||
PbSMeterRight.OnPaint := PbSMeterRightPaint;
|
||
PbSMeterRight.Color := CLR_PANEL;
|
||
|
||
PbSpectrum := TPaintBox.Create(Self);
|
||
PbSpectrum.Parent := PanelRight;
|
||
PbSpectrum.OnPaint := PbSpectrumPaint;
|
||
PbSpectrum.OnMouseDown := PbSpectrumMouseDown;
|
||
PbSpectrum.OnDblClick := PbSpectrumDblClick;
|
||
PbSpectrum.OnMouseMove := PbSpectrumMouseMove;
|
||
PbSpectrum.OnMouseUp := PbSpectrumMouseUp;
|
||
|
||
PbRuler := TPaintBox.Create(Self);
|
||
PbRuler.Parent := PanelRight;
|
||
PbRuler.OnPaint := PbRulerPaint;
|
||
PbRuler.Cursor := crDefault;
|
||
|
||
// ---- 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 := PbWaterfallPaint;
|
||
PbWaterfall.OnMouseDown := PbWaterfallMouseDown;
|
||
PbWaterfall.OnMouseMove := PbWaterfallMouseMove;
|
||
PbWaterfall.OnMouseUp := PbWaterfallMouseUp;
|
||
|
||
// SPAN кнопки как оверлей на спектре (в стиле web)
|
||
BtnSpan48k.Parent := PanelRight;
|
||
BtnSpan96k.Parent := PanelRight;
|
||
BtnSpan192k.Parent := PanelRight;
|
||
BtnSpan384k.Parent := PanelRight;
|
||
BtnSpan768k.Parent := PanelRight;
|
||
BtnSpan1536k.Parent := PanelRight;
|
||
LblSpan.Visible := False;
|
||
PositionSpanOverlayButtons;
|
||
|
||
// Initial layout
|
||
ResizeSpectrumPanels;
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// Splitter — перетаскивание границы спектр/водопад
|
||
// Обновление размеров происходит только при отпускании мыши (MouseUp),
|
||
// чтобы исключить фризы во время перетаскивания.
|
||
// Во время перетаскивания рисуем только призрак-линию на PanelSplitter.
|
||
// ===========================================================================
|
||
|
||
procedure TMainForm.SplitterMouseDown(Sender: TObject; Button: TMouseButton;
|
||
Shift: TShiftState; X, Y: Integer);
|
||
var
|
||
P: TPoint;
|
||
begin
|
||
if Button <> mbLeft then Exit;
|
||
FSplitterDrag := True;
|
||
FSplitterSH0 := FSpectrumHeight;
|
||
// Y в координатах PanelRight
|
||
P := PanelRight.ScreenToClient(PanelSplitter.ClientToScreen(Point(X, Y)));
|
||
FSplitterDragY0 := P.Y;
|
||
PanelSplitter.Color := TColor($005050A0); // подсветка при захвате
|
||
{$IFDEF WINDOWS}
|
||
SetCapture(PanelSplitter.Handle);
|
||
{$ENDIF}
|
||
end;
|
||
|
||
procedure TMainForm.SplitterMouseMove(Sender: TObject; Shift: TShiftState;
|
||
X, Y: Integer);
|
||
var
|
||
P: TPoint;
|
||
DeltaY: Integer;
|
||
NewSH: Integer;
|
||
TopOff, AvailH: Integer;
|
||
const
|
||
RULER_H = 18;
|
||
SPLITTER_H = 5;
|
||
MIN_SH = 60;
|
||
MIN_WH = 40;
|
||
begin
|
||
if not FSplitterDrag then Exit;
|
||
P := PanelRight.ScreenToClient(PanelSplitter.ClientToScreen(Point(X, Y)));
|
||
DeltaY := P.Y - FSplitterDragY0;
|
||
|
||
TopOff := PanelSpanButtons.Height;
|
||
AvailH := PanelRight.ClientHeight - TopOff - RULER_H - SPLITTER_H;
|
||
if AvailH <= 0 then Exit;
|
||
|
||
NewSH := FSplitterSH0 + DeltaY;
|
||
if NewSH < MIN_SH then NewSH := MIN_SH;
|
||
if NewSH > AvailH - MIN_WH then NewSH := AvailH - MIN_WH;
|
||
|
||
// Только двигаем сплиттер визуально — без пересчёта битмапов
|
||
PanelSplitter.Top := TopOff + NewSH + RULER_H;
|
||
PbWaterfall.Top := TopOff + NewSH + RULER_H + SPLITTER_H;
|
||
PbWaterfall.Height := AvailH - NewSH;
|
||
end;
|
||
|
||
procedure TMainForm.SplitterMouseUp(Sender: TObject; Button: TMouseButton;
|
||
Shift: TShiftState; X, Y: Integer);
|
||
var
|
||
P: TPoint;
|
||
DeltaY: Integer;
|
||
NewSH: Integer;
|
||
TopOff, AvailH: Integer;
|
||
const
|
||
RULER_H = 18;
|
||
SPLITTER_H = 5;
|
||
MIN_SH = 60;
|
||
MIN_WH = 40;
|
||
begin
|
||
if not FSplitterDrag then Exit;
|
||
FSplitterDrag := False;
|
||
{$IFDEF WINDOWS}
|
||
ReleaseCapture;
|
||
{$ENDIF}
|
||
PanelSplitter.Color := TColor($00303030); // обычный цвет
|
||
|
||
P := PanelRight.ScreenToClient(PanelSplitter.ClientToScreen(Point(X, Y)));
|
||
DeltaY := P.Y - FSplitterDragY0;
|
||
|
||
TopOff := PanelSpanButtons.Height;
|
||
AvailH := PanelRight.ClientHeight - TopOff - RULER_H - SPLITTER_H;
|
||
if AvailH <= 0 then Exit;
|
||
|
||
NewSH := FSplitterSH0 + DeltaY;
|
||
if NewSH < MIN_SH then NewSH := MIN_SH;
|
||
if NewSH > AvailH - MIN_WH then NewSH := AvailH - MIN_WH;
|
||
|
||
// Сохраняем новое соотношение и делаем полный пересчёт с перерисовкой
|
||
FSplitterRatio := NewSH / AvailH;
|
||
ResizeSpectrumPanels;
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// Resize handler (normal method instead of anonymous procedure)
|
||
// ===========================================================================
|
||
|
||
procedure TMainForm.RightPanelResize(Sender: TObject);
|
||
begin
|
||
ResizeSpectrumPanels;
|
||
end;
|
||
|
||
procedure TMainForm.ResizeSMeter;
|
||
// PanelSMeterRight на главной форме (Parent=Self).
|
||
// По Y: от самого верха формы до низа PanelSpanButtons (Toolbar+Span = 66px).
|
||
// По X: 40% ширины PanelRight, прижат к правому краю.
|
||
const
|
||
MARGIN = 3;
|
||
var
|
||
FW, FH: Integer;
|
||
RightW: Integer; // ширина PanelRight
|
||
SMW: Integer; // ширина S-метра
|
||
SMLeft: Integer;
|
||
SMTop: Integer;
|
||
SMBot: Integer;
|
||
begin
|
||
if (PanelSMeterRight = nil) or (PanelToolbar = nil) or
|
||
(PanelSpanButtons = nil) or (PanelLeft = nil) then Exit;
|
||
|
||
FW := ClientWidth;
|
||
// Ширина зоны спектра (PanelRight)
|
||
RightW := FW - PanelLeft.Width;
|
||
if RightW < 200 then Exit;
|
||
|
||
// Ширина S-метра = 40% от PanelRight
|
||
SMW := Round(RightW * 0.40);
|
||
|
||
// X: прижат к правому краю формы
|
||
SMLeft := FW - SMW - MARGIN;
|
||
|
||
// Y: от верха формы+margin до низа PanelSpanButtons-margin
|
||
SMTop := MARGIN;
|
||
SMBot := PanelToolbar.Height + PanelSpanButtons.Height - MARGIN;
|
||
|
||
PanelSMeterRight.SetBounds(SMLeft, SMTop, SMW, SMBot - SMTop);
|
||
PanelSMeterRight.BringToFront;
|
||
end;
|
||
|
||
procedure TMainForm.ResizeSpectrumPanels;
|
||
const
|
||
RULER_H = 18;
|
||
SPLITTER_H = 5;
|
||
MIN_SH = 60; // минимальная высота спектра
|
||
MIN_WH = 40; // минимальная высота водопада
|
||
var
|
||
RW, RH, SH, WH, TopOff: Integer;
|
||
AvailH: Integer;
|
||
begin
|
||
if PanelRight = nil then Exit;
|
||
RW := PanelRight.ClientWidth;
|
||
RH := PanelRight.ClientHeight;
|
||
TopOff := PanelSpanButtons.Height;
|
||
// S-метр позиционируется отдельно в ResizeSMeter
|
||
ResizeSMeter;
|
||
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);
|
||
// Сплиттер — между линейкой и водопадом
|
||
if PanelSplitter <> nil then
|
||
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 (FSpectrumBitmap <> nil) and (RW > 0) and (SH > 0) then
|
||
begin
|
||
FSpectrumBitmap.SetSize(RW, SH);
|
||
FSpectrumBitmap.Canvas.Brush.Color := CLR_BG;
|
||
FSpectrumBitmap.Canvas.FillRect(Rect(0, 0, RW, SH));
|
||
FreeAndNil(FSpecGradImg);
|
||
// Сбрасываем кэш фона/сетки и кэш точек кривой
|
||
FGridBitmapW := 0;
|
||
FGridBitmapH := 0;
|
||
FSpPtsLen := 0;
|
||
end;
|
||
if (FWaterfallBitmap <> nil) and (RW > 0) and (WH > 0) then
|
||
begin
|
||
FWaterfallBitmap.SetSize(RW, WH);
|
||
FWaterfallBitmap.Canvas.Brush.Color := clBlack;
|
||
FWaterfallBitmap.Canvas.FillRect(Rect(0, 0, RW, WH));
|
||
// Сбрасываем буфер пикселей водопада
|
||
FWfBitmapW := 0;
|
||
FWfBitmapH := 0;
|
||
SetLength(FWfPixels, 0);
|
||
end;
|
||
if (FRulerBitmap <> nil) and (RW > 0) then
|
||
begin
|
||
FRulerBitmap.SetSize(RW, 18);
|
||
FRulerLastFreq := -1.0;
|
||
FRulerLastVfo := -1.0;
|
||
end;
|
||
|
||
// Немедленно перерисовываем с новыми размерами
|
||
if RW > 0 then
|
||
begin
|
||
DrawSpectrum;
|
||
PbSpectrum.Invalidate;
|
||
if PbRuler <> nil then PbRuler.Invalidate;
|
||
PbWaterfall.Invalidate;
|
||
end;
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// Dark Theme
|
||
// ===========================================================================
|
||
|
||
procedure TMainForm.ApplyDarkTheme;
|
||
procedure DP(P: TPanel);
|
||
begin
|
||
P.Color := CLR_PANEL; P.Font.Color := CLR_TEXT;
|
||
end;
|
||
begin
|
||
Color := CLR_BG; Font.Color := CLR_TEXT;
|
||
DP(PanelToolbar); DP(PanelLeft);
|
||
DP(PanelVfoA); DP(PanelVfoB); DP(PanelVfoButtons);
|
||
DP(PanelBands); DP(PanelMode); DP(PanelFilter);
|
||
DP(PanelRX); DP(PanelTX);
|
||
DP(PanelRight); DP(PanelSpanButtons);
|
||
|
||
|
||
PbSpectrum.Color := CLR_BG;
|
||
PbWaterfall.Color := CLR_BG;
|
||
if PanelSMeterRight <> nil then PanelSMeterRight.Color := CLR_PANEL;
|
||
if PbSMeterRight <> nil then PbSMeterRight.Color := CLR_PANEL;
|
||
PbFwdPower.Color := CLR_BG;
|
||
PbSWR.Color := CLR_BG;
|
||
end;
|
||
|
||
procedure TMainForm.StyleButton(B: TFlatButton; Active: Boolean);
|
||
begin
|
||
B.Active := Active;
|
||
B.ClrNorm := CLR_INACTIVE;
|
||
B.ClrActive := CLR_ACTIVE;
|
||
B.ClrHot := TColor($00323232);
|
||
B.ClrBorder := TColor($00505050);
|
||
B.ClrText := CLR_TEXT;
|
||
B.ClrTextAct := TColor($0000FF88);
|
||
B.Font.Name := 'Courier New';
|
||
B.Font.Size := 8;
|
||
end;
|
||
|
||
procedure TMainForm.StyleSpanButton(B: TFlatButton; Active: Boolean);
|
||
begin
|
||
B.Active := Active;
|
||
B.ClrNorm := TColor($00543F18);
|
||
B.ClrActive := TColor($00624B1D);
|
||
B.ClrHot := TColor($00624B1D);
|
||
B.ClrBorder := TColor($00685624);
|
||
B.ClrText := TColor($00D8E6EE);
|
||
B.ClrTextAct := TColor($00F0FAFF);
|
||
B.Font.Name := 'Courier New';
|
||
B.Font.Size := 7;
|
||
end;
|
||
|
||
procedure TMainForm.PositionSpanOverlayButtons;
|
||
const
|
||
BTN_TOP = 6;
|
||
GAP = 3;
|
||
W48 = 40;
|
||
W96 = 40;
|
||
W192 = 44;
|
||
W384 = 44;
|
||
W768 = 44;
|
||
W1536 = 50;
|
||
HBTN = 18;
|
||
var
|
||
X, NeedW, BaseX, BaseY: Integer;
|
||
begin
|
||
if (PbSpectrum = nil) or (BtnSpan48k = nil) then Exit;
|
||
NeedW := W48 + W96 + W192 + W384 + W768 + W1536 + GAP * 5 + 8;
|
||
if PbSpectrum.Width < NeedW then
|
||
begin
|
||
BtnSpan48k.Visible := False;
|
||
BtnSpan96k.Visible := False;
|
||
BtnSpan192k.Visible := False;
|
||
BtnSpan384k.Visible := False;
|
||
BtnSpan768k.Visible := False;
|
||
BtnSpan1536k.Visible := False;
|
||
Exit;
|
||
end;
|
||
|
||
BaseX := PbSpectrum.Left;
|
||
BaseY := PbSpectrum.Top;
|
||
X := BaseX + 56;
|
||
BtnSpan48k.SetBounds(X, BaseY + BTN_TOP, W48, HBTN); Inc(X, W48 + GAP);
|
||
BtnSpan96k.SetBounds(X, BaseY + BTN_TOP, W96, HBTN); Inc(X, W96 + GAP);
|
||
BtnSpan192k.SetBounds(X, BaseY + BTN_TOP, W192, HBTN); Inc(X, W192 + GAP);
|
||
BtnSpan384k.SetBounds(X, BaseY + BTN_TOP, W384, HBTN); Inc(X, W384 + GAP);
|
||
BtnSpan768k.SetBounds(X, BaseY + BTN_TOP, W768, HBTN); Inc(X, W768 + GAP);
|
||
BtnSpan1536k.SetBounds(X, BaseY + BTN_TOP, W1536, HBTN);
|
||
if BtnHidePanel <> nil then
|
||
BtnHidePanel.SetBounds(BaseX, BaseY + BTN_TOP, 28, HBTN);
|
||
|
||
BtnSpan48k.Visible := True;
|
||
BtnSpan96k.Visible := True;
|
||
BtnSpan192k.Visible := True;
|
||
BtnSpan384k.Visible := True;
|
||
BtnSpan768k.Visible := True;
|
||
BtnSpan1536k.Visible := True;
|
||
|
||
BtnSpan48k.BringToFront;
|
||
BtnSpan96k.BringToFront;
|
||
BtnSpan192k.BringToFront;
|
||
BtnSpan384k.BringToFront;
|
||
BtnSpan768k.BringToFront;
|
||
BtnSpan1536k.BringToFront;
|
||
if BtnHidePanel <> nil then BtnHidePanel.BringToFront;
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// VFO
|
||
// ===========================================================================
|
||
|
||
function TMainForm.FormatFreq(Hz: Double): string;
|
||
var
|
||
Mhz, KHz, Rest: Integer;
|
||
begin
|
||
Mhz := Trunc(Hz / 1000000);
|
||
KHz := Trunc((Hz - Mhz * 1000000) / 1000);
|
||
Rest := Trunc(Hz) mod 1000;
|
||
Result := Format('%3d.%3.3d.%3.3d', [Mhz, KHz, Rest]);
|
||
end;
|
||
|
||
procedure TMainForm.UpdateVfoDisplay;
|
||
begin
|
||
FreqDispA.Frequency := Round(FVfoA);
|
||
FreqDispB.Frequency := Round(FVfoB);
|
||
if FActiveVfo = 0 then
|
||
begin
|
||
// VFO-A активен: яркий зелёный, крупный
|
||
FreqDispA.ColorNormal := CLR_FREQ; FreqDispA.FontSize := 20;
|
||
FreqDispB.ColorNormal := CLR_FREQ_DIM; FreqDispB.FontSize := 14;
|
||
FreqDispA.Height := 36;
|
||
FreqDispB.Height := 28;
|
||
LblVfoALabel.Font.Color := CLR_FREQ;
|
||
LblVfoBLabel.Font.Color := CLR_TEXTDIM;
|
||
end else begin
|
||
// VFO-B активен: яркий зелёный, крупный
|
||
FreqDispA.ColorNormal := CLR_FREQ_DIM; FreqDispA.FontSize := 14;
|
||
FreqDispB.ColorNormal := CLR_FREQ; FreqDispB.FontSize := 20;
|
||
FreqDispA.Height := 28;
|
||
FreqDispB.Height := 36;
|
||
LblVfoALabel.Font.Color := CLR_TEXTDIM;
|
||
LblVfoBLabel.Font.Color := CLR_FREQ;
|
||
end;
|
||
FreqDispA.Invalidate;
|
||
FreqDispB.Invalidate;
|
||
LblVfoALabel.Invalidate;
|
||
LblVfoBLabel.Invalidate;
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// Helpers
|
||
// ===========================================================================
|
||
|
||
// Замена MulDiv (Windows API) — простая целочисленная пропорция
|
||
function TMainForm.ScaleX(X, Total, Width: Integer): Integer;
|
||
begin
|
||
if Total = 0 then Result := 0
|
||
else Result := Round(X / Total * Width);
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// Drawing
|
||
// ===========================================================================
|
||
|
||
|
||
procedure TMainForm.DrawBarMeter(ACanvas: TCanvas; R: TRect;
|
||
Value, MaxVal: Double; BarColor: TColor);
|
||
var
|
||
Pct, BarW: Integer;
|
||
begin
|
||
ACanvas.Brush.Color := TColor($00080808);
|
||
ACanvas.FillRect(R);
|
||
|
||
if MaxVal > 0 then
|
||
Pct := Round(Max(0.0, Min(1.0, Value / MaxVal)) * (R.Right - R.Left - 2))
|
||
else
|
||
Pct := 0;
|
||
|
||
BarW := Pct;
|
||
ACanvas.Brush.Color := BarColor;
|
||
ACanvas.Pen.Color := BarColor;
|
||
ACanvas.FillRect(Rect(R.Left+1, R.Top+1, R.Left+1+BarW, R.Bottom-1));
|
||
|
||
ACanvas.Brush.Style := bsClear;
|
||
ACanvas.Pen.Color := CLR_BORDER;
|
||
ACanvas.Rectangle(R);
|
||
end;
|
||
|
||
procedure TMainForm.ResetWfAvgBuf;
|
||
var i: Integer;
|
||
begin
|
||
for i := 0 to 1023 do FWfAvgBuf[i] := -120.0;
|
||
FWfHigh := -50.0;
|
||
FWfLow := -120.0;
|
||
end;
|
||
|
||
procedure TMainForm.ResetSpectrumBuf;
|
||
var i: Integer;
|
||
begin
|
||
for i := 0 to 1023 do FSpectrumBuf[i] := -130.0;
|
||
for i := 0 to 1023 do FWfAvgBuf[i] := -120.0;
|
||
FWfHigh := -50.0;
|
||
FWfLow := -120.0;
|
||
end;
|
||
|
||
procedure TMainForm.FillDemoSpectrum;
|
||
var
|
||
i: Integer;
|
||
FreqOff, Noise, Sig: Double;
|
||
begin
|
||
for i := 0 to FSpectrumWidth - 1 do
|
||
begin
|
||
if FSpectrumWidth > 1 then
|
||
FreqOff := (i / (FSpectrumWidth - 1) - 0.5) * FSpanHz
|
||
else
|
||
FreqOff := 0;
|
||
Noise := -110 + (Random - 0.5) * 6;
|
||
if Abs(FreqOff) < 2000 then
|
||
Sig := -50 - Abs(FreqOff) / 200
|
||
else
|
||
Sig := -999;
|
||
FSpectrumBuf[i mod 1024] := Max(Noise, Sig);
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.DrawMarkerLine(C: TCanvas; W, H: Integer);
|
||
var
|
||
MX: Integer;
|
||
MarkerFreq: Double;
|
||
MarkerLbl: string;
|
||
begin
|
||
MX := Round(FMarkerX / 1000.0 * W);
|
||
if (MX < 0) or (MX >= W) then Exit;
|
||
|
||
MarkerFreq := (FCenterFreq - FSpanHz / 2) + FMarkerX / 1000.0 * FSpanHz;
|
||
MarkerLbl := FormatFreq(Round(MarkerFreq));
|
||
|
||
C.Pen.Color := TColor($004444FF); // красный (BGR формат)
|
||
C.Pen.Width := 1;
|
||
C.Pen.Style := psSolid;
|
||
C.MoveTo(MX, 0);
|
||
C.LineTo(MX, H);
|
||
|
||
C.Font.Color := TColor($004444FF);
|
||
C.Font.Size := 7;
|
||
C.Font.Name := 'Courier New';
|
||
if MX + 4 + C.TextWidth(MarkerLbl) < W then
|
||
C.TextOut(MX + 4, 4, MarkerLbl)
|
||
else
|
||
C.TextOut(MX - 4 - C.TextWidth(MarkerLbl), 4, MarkerLbl);
|
||
end;
|
||
|
||
// Вспомогательная функция: BGRA32 top-to-bottom описание для TLazIntfImage.
|
||
// Заменяет Init_BPP32_B8G8R8A8_BIO_TTB которая не всегда доступна в старых Lazarus.
|
||
procedure InitRawDesc32(var Desc: TRawImageDescription; AWidth, AHeight: Integer);
|
||
begin
|
||
FillChar(Desc, SizeOf(Desc), 0);
|
||
Desc.Format := ricfRGBA;
|
||
Desc.Width := AWidth;
|
||
Desc.Height := AHeight;
|
||
Desc.Depth := 32;
|
||
Desc.BitOrder := riboBitsInOrder;
|
||
Desc.ByteOrder := riboLSBFirst;
|
||
Desc.LineOrder := riloTopToBottom;
|
||
Desc.BitsPerPixel:= 32;
|
||
Desc.LineEnd := rileDWordBoundary;
|
||
// BGRA: B=bits 0..7, G=8..15, R=16..23, A=24..31
|
||
Desc.BlueShift := 0; Desc.BluePrec := 8;
|
||
Desc.GreenShift := 8; Desc.GreenPrec := 8;
|
||
Desc.RedShift := 16; Desc.RedPrec := 8;
|
||
Desc.AlphaShift := 24; Desc.AlphaPrec := 8;
|
||
end;
|
||
|
||
function MixColorBGR(C1, C2: TColor; T: Double): TColor;
|
||
var
|
||
B1, G1, R1: Integer;
|
||
B2, G2, R2: Integer;
|
||
B, G, R: Integer;
|
||
begin
|
||
if T < 0.0 then T := 0.0 else
|
||
if T > 1.0 then T := 1.0;
|
||
|
||
B1 := (C1 shr 16) and $FF; G1 := (C1 shr 8) and $FF; R1 := C1 and $FF;
|
||
B2 := (C2 shr 16) and $FF; G2 := (C2 shr 8) and $FF; R2 := C2 and $FF;
|
||
|
||
B := Round(B1 + (B2 - B1) * T);
|
||
G := Round(G1 + (G2 - G1) * T);
|
||
R := Round(R1 + (R2 - R1) * T);
|
||
Result := TColor((B shl 16) or (G shl 8) or R);
|
||
end;
|
||
|
||
procedure PaintVerticalGradient(C: TCanvas; W, H: Integer; TopColor, BottomColor: TColor);
|
||
var
|
||
Y: Integer;
|
||
T: Double;
|
||
begin
|
||
if (W <= 0) or (H <= 0) then Exit;
|
||
C.Pen.Style := psClear;
|
||
C.Brush.Style := bsSolid;
|
||
for Y := 0 to H - 1 do
|
||
begin
|
||
T := Y / Max(1, H - 1);
|
||
C.Brush.Color := MixColorBGR(TopColor, BottomColor, T);
|
||
C.FillRect(Rect(0, Y, W, Y + 1));
|
||
end;
|
||
C.Pen.Style := psSolid;
|
||
end;
|
||
|
||
procedure PaintHorizontalFade(C: TCanvas; X1, X2, H: Integer; LeftColor, RightColor: TColor);
|
||
var
|
||
X: Integer;
|
||
T: Double;
|
||
begin
|
||
if (H <= 0) or (X2 <= X1) then Exit;
|
||
C.Pen.Style := psClear;
|
||
C.Brush.Style := bsSolid;
|
||
for X := X1 to X2 - 1 do
|
||
begin
|
||
T := (X - X1) / Max(1, X2 - X1 - 1);
|
||
C.Brush.Color := MixColorBGR(LeftColor, RightColor, T);
|
||
C.FillRect(Rect(X, 0, X + 1, H));
|
||
end;
|
||
C.Pen.Style := psSolid;
|
||
end;
|
||
|
||
procedure TMainForm.DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer);
|
||
// ═══════════════════════════════════════════════════════════════════════
|
||
// Попиксельный градиент через кэшированный FSpecGradImg.
|
||
// Старый подход: H/2 × MoveTo+LineTo = ~150-250 GDI calls = ~3-5ms
|
||
// Новый подход: записываем пиксели в FSpecGradImg + один BitBlt ≈ 1ms
|
||
// ═══════════════════════════════════════════════════════════════════════
|
||
var
|
||
Row, Col: Integer;
|
||
YMin: Integer;
|
||
Alpha: Integer;
|
||
RowPtr: PByte;
|
||
BPPg: Integer;
|
||
RG, GG: Byte;
|
||
Desc: TRawImageDescription;
|
||
begin
|
||
if FSpectrumBitmap = nil then Exit;
|
||
if (W <= 0) or (H <= 0) then Exit;
|
||
|
||
YMin := H;
|
||
for Col := 0 to W - 1 do
|
||
if SpPts[Col].Y < YMin then YMin := SpPts[Col].Y;
|
||
if YMin >= H then Exit;
|
||
|
||
// Создаём/переиспользуем FSpecGradImg
|
||
if (FSpecGradImg = nil) or
|
||
(FSpecGradImg.Width <> W) or (FSpecGradImg.Height <> H) then
|
||
begin
|
||
FreeAndNil(FSpecGradImg);
|
||
FSpecGradImg := TLazIntfImage.Create(W, H);
|
||
InitRawDesc32(Desc, W, H);
|
||
FSpecGradImg.DataDescription := Desc;
|
||
FSpecGradImg.CreateData;
|
||
end;
|
||
|
||
// Очищаем прозрачным
|
||
if FSpecGradImg.PixelData <> nil then
|
||
FillChar(FSpecGradImg.PixelData^, W * H * 4, 0);
|
||
|
||
BPPg := FSpecGradImg.DataDescription.BitsPerPixel div 8;
|
||
if BPPg < 3 then BPPg := 4;
|
||
|
||
for Row := YMin to H - 1 do
|
||
begin
|
||
Alpha := Round((1.0 - Sqr((Row - YMin) / Max(1.0, H - YMin - 1.0))) * 200);
|
||
if Alpha <= 0 then Continue;
|
||
if Alpha > 200 then Alpha := 200;
|
||
GG := Byte(Alpha shr 1); // B-канал
|
||
RG := Byte(Alpha); // G-канал
|
||
|
||
RowPtr := FSpecGradImg.GetDataLineStart(Row);
|
||
if RowPtr = nil then Continue;
|
||
|
||
for Col := 0 to W - 1 do
|
||
begin
|
||
if SpPts[Col].Y <= Row then
|
||
begin
|
||
RowPtr[0] := GG; RowPtr[1] := RG; RowPtr[2] := 0;
|
||
if BPPg >= 4 then RowPtr[3] := Byte(Alpha);
|
||
end
|
||
else
|
||
begin
|
||
RowPtr[0] := 0; RowPtr[1] := 0; RowPtr[2] := 0;
|
||
if BPPg >= 4 then RowPtr[3] := 0;
|
||
end;
|
||
Inc(RowPtr, BPPg);
|
||
end;
|
||
end;
|
||
|
||
// Выводим через кэшированный bitmap → BitBlt на спектрум-bitmap
|
||
FSpecGradBmp.LoadFromIntfImage(FSpecGradImg);
|
||
LCLIntf.BitBlt(FSpectrumBitmap.Canvas.Handle, 0, 0, W, H,
|
||
FSpecGradBmp.Canvas.Handle, 0, 0, $CC0020);
|
||
end;
|
||
|
||
procedure TMainForm.DrawSpectrum;
|
||
var
|
||
C: TCanvas;
|
||
GC: TCanvas; // для рисования в FGridBitmap
|
||
i, Yp, W, H, GX: Integer;
|
||
DBmin, DBmax: Double;
|
||
dB: Double;
|
||
VfoX, X1, X2: Integer;
|
||
Lo_Hz, Hi_Hz, Half: Double;
|
||
AGCy, AGCHangY: Integer;
|
||
SrcF, Frac, dBv: Double;
|
||
S0, S1: Integer;
|
||
InvRange: Double;
|
||
LabelBandW: Integer;
|
||
begin
|
||
if FSpectrumBitmap = nil then Exit;
|
||
W := FSpectrumBitmap.Width;
|
||
H := FSpectrumBitmap.Height;
|
||
if (W <= 0) or (H <= 0) then Exit;
|
||
|
||
C := FSpectrumBitmap.Canvas;
|
||
DBmin := -130.0; DBmax := -20.0;
|
||
InvRange := 1.0 / (DBmax - DBmin);
|
||
|
||
// ── 1. Фон+сетка из кэша ───────────────────────────────────────────────
|
||
if (FGridBitmapW <> W) or (FGridBitmapH <> H) then
|
||
begin
|
||
FGridBitmapW := W;
|
||
FGridBitmapH := H;
|
||
FGridBitmap.SetSize(W, H);
|
||
GC := FGridBitmap.Canvas;
|
||
PaintVerticalGradient(GC, W, H, TColor($0020160B), TColor($006C4F1F));
|
||
LabelBandW := 28;
|
||
GC.Pen.Style := psClear;
|
||
GC.Brush.Style := bsSolid;
|
||
GC.Brush.Color := TColor($00543F18);
|
||
GC.FillRect(Rect(0, 0, LabelBandW, H));
|
||
GC.Pen.Style := psSolid;
|
||
GC.Font.Size := 7;
|
||
GC.Font.Name := 'Courier New';
|
||
GC.Font.Color := TColor($00A8C9D9);
|
||
GC.Brush.Style := bsClear;
|
||
dB := -40.0;
|
||
while dB >= DBmin do
|
||
begin
|
||
Yp := Round((DBmax - dB) * InvRange * H);
|
||
GC.Pen.Color := TColor($00685624);
|
||
GC.Pen.Width := 1;
|
||
GC.MoveTo(0, Yp); GC.LineTo(W-1, Yp);
|
||
GC.TextOut(1, Yp - 9, Format('%4.0f', [dB]));
|
||
dB := dB - 10;
|
||
end;
|
||
for i := 0 to 8 do
|
||
begin
|
||
GX := ScaleX(i, 8, W);
|
||
GC.Pen.Color := TColor($00685624);
|
||
GC.MoveTo(GX, 0); GC.LineTo(GX, H);
|
||
end;
|
||
end;
|
||
LCLIntf.BitBlt(C.Handle, 0, 0, W, H, FGridBitmap.Canvas.Handle, 0, 0, $CC0020);
|
||
|
||
// ── 2. Полоса фильтра — заливка ДО спектра ─────────────────────────────
|
||
Half := FFilterBW / 2;
|
||
// Используем частоту активного VFO (A или B) для позиции маркера и фильтра
|
||
if FActiveVfo = 0 then
|
||
VfoX := Round((FVfoA - FCenterFreq + FSpanHz / 2) / FSpanHz * W)
|
||
else
|
||
VfoX := Round((FVfoB - FCenterFreq + FSpanHz / 2) / FSpanHz * W);
|
||
case FMode of
|
||
0: begin Lo_Hz := -FFilterBW; Hi_Hz := -100; end;
|
||
1: begin Lo_Hz := 100; Hi_Hz := FFilterBW; end;
|
||
else begin Lo_Hz := -Half; Hi_Hz := Half; end;
|
||
end;
|
||
X1 := Max(0, Min(W-1, VfoX + Round(Lo_Hz / FSpanHz * W)));
|
||
X2 := Max(0, Min(W-1, VfoX + Round(Hi_Hz / FSpanHz * W)));
|
||
C.Brush.Color := TColor($006A4E22);
|
||
C.Brush.Style := bsSolid;
|
||
C.Pen.Style := psClear;
|
||
if X2 > X1 then C.FillRect(Rect(X1, 0, X2, H));
|
||
C.Pen.Style := psSolid;
|
||
|
||
// ── 3. AGC линии ────────────────────────────────────────────────────────
|
||
if FWDSPReady then
|
||
begin
|
||
AGCy := Round((DBmax - FDSPEngine.AGCThresh) * InvRange * H);
|
||
if (AGCy >= 0) and (AGCy < H) then
|
||
begin
|
||
C.Pen.Color := TColor($0000AAFF); C.Pen.Width := 1; C.Pen.Style := psDash;
|
||
C.MoveTo(0, AGCy); C.LineTo(W, AGCy);
|
||
C.Pen.Style := psSolid; C.Font.Color := TColor($0000AAFF); C.Font.Size := 6;
|
||
C.TextOut(4, AGCy - 9, 'AGC T');
|
||
end;
|
||
AGCHangY := Round((DBmax - FDSPEngine.AGCHangLevel) * InvRange * H);
|
||
if (AGCHangY >= 0) and (AGCHangY < H) and (Abs(AGCHangY - AGCy) > 4) then
|
||
begin
|
||
C.Pen.Color := TColor($00FFCC00); C.Pen.Width := 1; C.Pen.Style := psDot;
|
||
C.MoveTo(0, AGCHangY); C.LineTo(W, AGCHangY);
|
||
C.Pen.Style := psSolid; C.Font.Color := TColor($00FFCC00); C.Font.Size := 6;
|
||
C.TextOut(4, AGCHangY + 2, 'AGC H');
|
||
end;
|
||
end
|
||
else
|
||
begin
|
||
AGCy := Round((DBmax - (-FAGCTop)) * InvRange * H);
|
||
if (AGCy >= 0) and (AGCy < H) then
|
||
begin
|
||
C.Pen.Color := TColor($0000AAFF); C.Pen.Width := 1; C.Pen.Style := psDash;
|
||
C.MoveTo(0, AGCy); C.LineTo(W, AGCy);
|
||
C.Pen.Style := psSolid; C.Font.Color := TColor($0000AAFF); C.Font.Size := 6;
|
||
C.TextOut(4, AGCy - 9, Format('AGC -%ddB', [FAGCTop]));
|
||
end;
|
||
end;
|
||
|
||
// ── 4. Кривая спектра — кэшируем FSpPts без SetLength каждый кадр ──────
|
||
if FSpPtsLen <> W + 2 then
|
||
begin
|
||
SetLength(FSpPts, W + 2);
|
||
FSpPtsLen := W + 2;
|
||
end;
|
||
for i := 0 to W - 1 do
|
||
begin
|
||
SrcF := i * 1023.0 / Max(1, W - 1);
|
||
S0 := Min(Trunc(SrcF), 1023);
|
||
S1 := Min(S0 + 1, 1023);
|
||
Frac := SrcF - S0;
|
||
dBv := FSpectrumBuf[S0] * (1.0 - Frac) + FSpectrumBuf[S1] * Frac;
|
||
FSpPts[i] := Point(i, Max(0, Min(H-1, Round((DBmax - dBv) * InvRange * H))));
|
||
end;
|
||
FSpPts[W] := Point(W-1, H);
|
||
FSpPts[W+1] := Point(0, H);
|
||
|
||
// ── 5. Градиент ─────────────────────────────────────────────────────────
|
||
C.Brush.Style := bsSolid;
|
||
C.Pen.Style := psClear;
|
||
DrawSpectrumGradient(FSpPts, W, H);
|
||
|
||
// ── 6. Линия спектра ────────────────────────────────────────────────────
|
||
C.Pen.Style := psSolid;
|
||
C.Pen.Color := TColor($0040FF80);
|
||
C.Pen.Width := 1;
|
||
C.Polyline(Slice(FSpPts, W)); // один вызов вместо W раз LineTo
|
||
|
||
// ── 7. Края фильтра + VFO поверх спектра ────────────────────────────────
|
||
C.Pen.Color := TColor($0040FFCC); C.Pen.Width := 1; C.Pen.Style := psSolid;
|
||
C.MoveTo(X1, 0); C.LineTo(X1, H);
|
||
C.MoveTo(X2, 0); C.LineTo(X2, H);
|
||
C.Pen.Color := TColor($0000AAFF); C.Pen.Width := 2;
|
||
C.MoveTo(VfoX, 0); C.LineTo(VfoX, H - 12);
|
||
C.Brush.Color := TColor($0000AAFF); C.Brush.Style := bsSolid; C.Pen.Width := 1;
|
||
C.Polygon([Point(VfoX-5,0), Point(VfoX+5,0), Point(VfoX,8)]);
|
||
|
||
if FMarkerActive then
|
||
DrawMarkerLine(C, W, H);
|
||
end;
|
||
|
||
procedure TMainForm.DrawWaterfall;
|
||
// ═══════════════════════════════════════════════════════════════════════
|
||
// Архитектура минимального CPU:
|
||
// FWfPixels[W*H] — весь водопад в памяти, строка 0 = верх (новейшая).
|
||
// Скролл = Move(FWfPixels[0]→FWfPixels[W], (H-1)*W*4) ~0.3ms
|
||
// Новая строка = инкрементный PLongWord по FWfPixels[0..W-1] ~0.1ms
|
||
// Вывод = TLazIntfImage.PixelData := FWfPixels + LoadFromIntfImage ~1ms
|
||
// Итого ~1.5ms vs старый IntfImage roundtrip ~5ms
|
||
// ═══════════════════════════════════════════════════════════════════════
|
||
var
|
||
W, H, X: Integer;
|
||
dB, frac: Double;
|
||
WatSrcF: Double;
|
||
WatS0, WatS1, V: Integer;
|
||
AvgDB: Double;
|
||
PeakDB: Double;
|
||
NFSum: Double;
|
||
NFCount: Integer;
|
||
WfHigh, WfLow, InvRange: Double;
|
||
Step: Double;
|
||
Px: PLongWord;
|
||
Desc: TRawImageDescription;
|
||
Row, Col: Integer;
|
||
RowPtr: PByte;
|
||
Src: PLongWord;
|
||
BPPi: Integer;
|
||
Pal: LongWord;
|
||
const
|
||
ALPHA_SPEC = 0.25;
|
||
ALPHA_HIGH = 0.02;
|
||
ALPHA_LOW = 0.005;
|
||
begin
|
||
if FWaterfallBitmap = nil then Exit;
|
||
W := FWaterfallBitmap.Width;
|
||
H := FWaterfallBitmap.Height;
|
||
if (W <= 0) or (H <= 0) then Exit;
|
||
|
||
// ── EMA + Peak + NF — один цикл по 1024 бинам ─────────────────────────
|
||
PeakDB := -200.0; NFSum := 0.0; NFCount := 0;
|
||
for X := 0 to 1023 do
|
||
begin
|
||
AvgDB := FWfAvgBuf[X] * (1.0 - ALPHA_SPEC) + FSpectrumBuf[X] * ALPHA_SPEC;
|
||
FWfAvgBuf[X] := AvgDB;
|
||
if AvgDB > PeakDB then PeakDB := AvgDB;
|
||
if AvgDB < FWfLow + 15.0 then begin NFSum := NFSum + AvgDB; Inc(NFCount); end;
|
||
end;
|
||
|
||
// ── AGC High/Low ────────────────────────────────────────────────────────
|
||
if FWfAGCEnabled then
|
||
begin
|
||
if PeakDB > FWfHigh then FWfHigh := PeakDB + 5.0
|
||
else FWfHigh := FWfHigh + ALPHA_HIGH * ((PeakDB + 5.0) - FWfHigh);
|
||
end;
|
||
if FWfNFEnabled and (NFCount > 10) then
|
||
FWfLow := FWfLow + ALPHA_LOW * ((NFSum / NFCount - 5.0) - FWfLow);
|
||
WfHigh := FWfHigh; WfLow := FWfLow;
|
||
if WfHigh < WfLow + 15.0 then WfHigh := WfLow + 15.0;
|
||
if WfHigh > 0.0 then WfHigh := 0.0;
|
||
if WfLow < -160 then WfLow := -160;
|
||
InvRange := 255.0 / (WfHigh - WfLow);
|
||
|
||
// ── Пересоздаём буфер при ресайзе ──────────────────────────────────────
|
||
if (FWfBitmapW <> W) or (FWfBitmapH <> H) then
|
||
begin
|
||
FWfBitmapW := W; FWfBitmapH := H;
|
||
SetLength(FWfPixels, W * H);
|
||
FillDWord(FWfPixels[0], W * H, 0);
|
||
FWfBitmap.SetSize(W, H);
|
||
// Сбрасываем кэш IntfImage — пересоздастся ниже с новым размером
|
||
FreeAndNil(FWfIntfImg);
|
||
end;
|
||
|
||
// ── Скролл: Move строк вниз ─────────────────────────────────────────────
|
||
if H > 1 then
|
||
Move(FWfPixels[0], FWfPixels[W], (H - 1) * SizeOf(LongWord) * W);
|
||
|
||
// ── Новая строка 0 — инкрементный указатель, без умножения X*BPP ────────
|
||
Step := 1023.0 / Max(1, W - 1);
|
||
WatSrcF := 0.0;
|
||
Px := @FWfPixels[0];
|
||
for X := 0 to W - 1 do
|
||
begin
|
||
WatS0 := Trunc(WatSrcF);
|
||
if WatS0 > 1022 then WatS0 := 1022;
|
||
WatS1 := WatS0 + 1;
|
||
frac := WatSrcF - WatS0;
|
||
dB := FWfAvgBuf[WatS0] * (1.0 - frac) + FWfAvgBuf[WatS1] * frac;
|
||
V := Trunc((dB - WfLow) * InvRange);
|
||
if V < 0 then V := 0;
|
||
if V > 255 then V := 255;
|
||
// Палитра задана как RGB, а в буфере нужен BGRA.
|
||
// Меняем местами R/B и ставим alpha=255, чтобы совпадало с web/Thetis-градиентом.
|
||
Pal := WFALL_PALETTE[V];
|
||
Px^ := ((Pal and $000000FF) shl 16) or
|
||
(Pal and $0000FF00) or
|
||
((Pal and $00FF0000) shr 16) or
|
||
$FF000000;
|
||
Inc(Px);
|
||
WatSrcF := WatSrcF + Step;
|
||
end;
|
||
|
||
// ── Записываем FWfPixels в FWfBitmap через кэшированный TLazIntfImage ────
|
||
// FWfIntfImg создаётся один раз на весь lifetime (или при ресайзе).
|
||
// Move(FWfPixels → IntfImg.PixelData) = один memcpy без GDI roundtrip.
|
||
// LoadFromIntfImage = один вызов platformAPI → ~1ms на Linux GTK2.
|
||
if FWfIntfImg = nil then
|
||
begin
|
||
FWfIntfImg := TLazIntfImage.Create(W, H);
|
||
InitRawDesc32(Desc, W, H);
|
||
FWfIntfImg.DataDescription := Desc;
|
||
FWfIntfImg.CreateData;
|
||
end;
|
||
BPPi := FWfIntfImg.DataDescription.BitsPerPixel div 8;
|
||
if (BPPi = 4) and (FWfIntfImg.PixelData <> nil) then
|
||
Move(FWfPixels[0], FWfIntfImg.PixelData^, W * H * 4)
|
||
else
|
||
begin
|
||
Src := @FWfPixels[0];
|
||
for Row := 0 to H - 1 do
|
||
begin
|
||
RowPtr := FWfIntfImg.GetDataLineStart(Row);
|
||
if RowPtr = nil then begin Inc(Src, W); Continue; end;
|
||
for Col := 0 to W - 1 do
|
||
begin
|
||
RowPtr[0] := Byte(Src^ shr 16); // B
|
||
RowPtr[1] := Byte(Src^ shr 8); // G
|
||
RowPtr[2] := Byte(Src^); // R
|
||
if BPPi >= 4 then RowPtr[3] := $FF;
|
||
Inc(Src); Inc(RowPtr, BPPi);
|
||
end;
|
||
end;
|
||
end;
|
||
FWfBitmap.LoadFromIntfImage(FWfIntfImg);
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// Широкий S-метр — точная копия Thetis/PowerSDR стиля
|
||
// Структура (высота H):
|
||
// верх (H*35%): dBm шкала (-120..-20..0), тики, цифровое значение слева
|
||
// середина (H*35%): полоса сигнала (зелёный бар + широкий пик + белый маркер)
|
||
// низ (H*30%): S-шкала (S1..S9..+60), S-значение слева
|
||
// ===========================================================================
|
||
procedure TMainForm.DrawSMeterWide(ACanvas: TCanvas; R: TRect;
|
||
Value, Peak, MinVal: Double;
|
||
out ZX1, ZX2, ZY1, ZY2: Integer);
|
||
const
|
||
DB_MIN = -127.0; // левый край шкалы
|
||
DB_MAX = -13.0; // правый край шкалы (S9+60)
|
||
DB_S9 = -73.0; // граница S9 (зелёная/оранжевая)
|
||
DB_OVR = -43.0; // граница "красной" зоны (+30 over S9)
|
||
|
||
// dBm верхняя шкала
|
||
DBM_MARKS: array[0..5] of Double = (-120,-100,-80,-60,-40,-20);
|
||
DBM_LABELS: array[0..5] of string = ('-120','-100','-80','-60','-40','-20');
|
||
|
||
// S-шкала нижняя: как в VfoOverlay
|
||
S_MARKS_DBM: array[0..7] of Double = (-121,-109,-97,-85,-73,-53,-33,-13);
|
||
S_MARKS_LBL: array[0..7] of string = ('S1','S3','S5','S7','S9','+20','+40','+60');
|
||
|
||
// Цвета (BGR в TColor)
|
||
CLR_SMETER_BG = TColor($00181818); // фон = CLR_PANEL
|
||
CLR_BAR_GREEN = TColor($00091F00); // тёмно-зелёный бар до S9 (RGB 0,31,9)
|
||
CLR_BAR_OVER = TColor($00091528); // тёмно-красный бар после S9 (RGB 40,21,9) — тот же тон что зелёный но красн.
|
||
CLR_PEAK_FILL = TColor($00796F75); // светло-серый заполнитель пика (RGB 117,111,121)
|
||
CLR_PEAK_MARKER = TColor($00F8F8FF); // белый пик-маркер (RGB 255,248,248)
|
||
CLR_BAR_ORANGE = TColor($000A1A28); // оранжевая зона (>S9)
|
||
CLR_BAR_RED = TColor($00000832); // красная зона (>+30)
|
||
CLR_TICK_GREEN = TColor($00708070); // тики зелёной зоны
|
||
CLR_TICK_WHITE = TColor($00E0E0DC); // тики белые (dBm шкала)
|
||
CLR_TICK_BLUE = TColor($0070A090); // тики S9+ зоны
|
||
CLR_LABEL_DBM = TColor($0096D8DC); // цифровое значение dBm (желтоватый)
|
||
CLR_LABEL_S = TColor($00F0F0F0); // S-значение — белый
|
||
CLR_SCALE_DBM = TColor($00D0DCDC); // метки dBm шкалы (бело-серые)
|
||
CLR_SCALE_S_GREEN = TColor($00E0E0E0); // метки S1..S9 — белые
|
||
CLR_SCALE_S_BLUE = TColor($0090D090); // метки +10..+60 — как S1..S9 оригинал (желт-зелён)
|
||
CLR_BORDER = TColor($00303028); // рамки
|
||
|
||
LEFT_INFO = 62; // ширина левой информационной колонки (dBm + S-value)
|
||
TICK_LONG = 5; // длина длинного тика
|
||
TICK_MED = 3; // длина среднего тика
|
||
|
||
var
|
||
W, H: Integer;
|
||
BX, BW: Integer; // начало и ширина зоны шкалы
|
||
Y_UPPER_MID: Integer; // середина верхней зоны (метки dBm)
|
||
Y_BAR_TOP: Integer; // верх полосы сигнала
|
||
Y_BAR_BOT: Integer; // низ полосы сигнала
|
||
Y_LOWER_MID: Integer; // середина нижней зоны (метки S)
|
||
BarEnd: Integer; // правый край текущего сигнала
|
||
PeakLeft: Integer; // левый край peak fill
|
||
PeakRight: Integer; // правый край peak fill (= белый маркер)
|
||
MinX: Integer; // позиция минимума
|
||
S9X, OvrX: Integer; // позиции границ зон
|
||
i, X, TW: Integer;
|
||
Lbl: string;
|
||
SNum: Integer;
|
||
Over: Integer;
|
||
|
||
function DBtoX(dB: Double): Integer; inline;
|
||
begin
|
||
Result := BX + Round((dB - DB_MIN) / (DB_MAX - DB_MIN) * BW);
|
||
if Result < BX then Result := BX;
|
||
if Result > BX + BW then Result := BX + BW;
|
||
end;
|
||
|
||
procedure HLine(Y, X1, X2: Integer; C: TColor);
|
||
begin
|
||
ACanvas.Pen.Color := C;
|
||
ACanvas.MoveTo(X1, Y); ACanvas.LineTo(X2, Y);
|
||
end;
|
||
|
||
procedure VLine(X, Y1, Y2: Integer; C: TColor);
|
||
begin
|
||
ACanvas.Pen.Color := C;
|
||
ACanvas.MoveTo(X, Y1); ACanvas.LineTo(X, Y2);
|
||
end;
|
||
|
||
procedure FillBar(X1, Y1, X2, Y2: Integer; C: TColor);
|
||
begin
|
||
ACanvas.Brush.Color := C;
|
||
ACanvas.Brush.Style := bsSolid;
|
||
ACanvas.Pen.Style := psClear;
|
||
if X2 > X1 then ACanvas.FillRect(Rect(X1, Y1, X2, Y2));
|
||
ACanvas.Pen.Style := psSolid;
|
||
end;
|
||
|
||
begin
|
||
W := R.Right - R.Left;
|
||
H := R.Bottom - R.Top;
|
||
// Инициализируем out-параметры
|
||
ZX1 := 0; ZX2 := 0; ZY1 := 0; ZY2 := 0;
|
||
if (W < 80) or (H < 16) then Exit;
|
||
|
||
// --- Геометрия ---
|
||
BX := R.Left + LEFT_INFO;
|
||
BW := W - LEFT_INFO - 4;
|
||
|
||
Y_UPPER_MID := R.Top + H * 3 div 10; // центр верхней зоны меток
|
||
Y_BAR_TOP := R.Top + H * 36 div 100; // верх бара
|
||
Y_BAR_BOT := R.Top + H * 66 div 100; // низ бара
|
||
Y_LOWER_MID := R.Top + H * 80 div 100; // центр нижней зоны меток
|
||
|
||
S9X := DBtoX(DB_S9);
|
||
OvrX := DBtoX(DB_OVR);
|
||
|
||
// ================================================================
|
||
// ФОН
|
||
// ================================================================
|
||
ACanvas.Brush.Color := CLR_SMETER_BG;
|
||
ACanvas.Brush.Style := bsSolid;
|
||
ACanvas.Pen.Style := psClear;
|
||
ACanvas.FillRect(R);
|
||
ACanvas.Pen.Style := psSolid;
|
||
|
||
// Лёгкий цветной фон зон шкалы в полосе бара
|
||
FillBar(BX, Y_BAR_TOP, S9X, Y_BAR_BOT, TColor($00061006)); // тёмно-зелёный
|
||
FillBar(S9X, Y_BAR_TOP, OvrX, Y_BAR_BOT, TColor($000A1006)); // тёмно-жёлтый
|
||
FillBar(OvrX, Y_BAR_TOP, BX + BW, Y_BAR_BOT, TColor($00060A10)); // тёмно-красный
|
||
|
||
// ================================================================
|
||
// ПОЛОСА СИГНАЛА
|
||
// ================================================================
|
||
BarEnd := DBtoX(Value); // текущий уровень
|
||
PeakLeft := DBtoX(MinVal); // левый край зоны = отслеживаемый минимум
|
||
PeakRight:= DBtoX(Peak); // правый край зоны = отслеживаемый пик
|
||
|
||
// Заполняем out-параметры зоны для alpha-blend (после возврата)
|
||
ZX1 := Max(BX + 1, Min(PeakLeft, PeakRight));
|
||
ZX2 := Min(BX + BW - 1, Max(PeakLeft, PeakRight));
|
||
ZY1 := Y_BAR_TOP + 1;
|
||
ZY2 := Y_BAR_BOT - 1;
|
||
|
||
// Бар: зелёный до S9, красноватый после S9 — в том же тёмном стиле
|
||
if BarEnd > BX then
|
||
begin
|
||
if BarEnd <= S9X then
|
||
// Весь бар в зелёной зоне
|
||
FillBar(BX, Y_BAR_TOP + 1, BarEnd, Y_BAR_BOT - 1, CLR_BAR_GREEN)
|
||
else
|
||
begin
|
||
// Зелёная часть до S9
|
||
FillBar(BX, Y_BAR_TOP + 1, S9X, Y_BAR_BOT - 1, CLR_BAR_GREEN);
|
||
// Красноватая часть после S9
|
||
FillBar(S9X, Y_BAR_TOP + 1, BarEnd, Y_BAR_BOT - 1, CLR_BAR_OVER);
|
||
end;
|
||
end;
|
||
|
||
// Белый маркер пика (2px) — правый край зоны
|
||
if (PeakRight > BX) and (PeakRight <= BX + BW) then
|
||
begin
|
||
ACanvas.Pen.Color := CLR_PEAK_MARKER;
|
||
ACanvas.Pen.Width := 2;
|
||
ACanvas.MoveTo(PeakRight, Y_BAR_TOP);
|
||
ACanvas.LineTo(PeakRight, Y_BAR_BOT);
|
||
ACanvas.Pen.Width := 1;
|
||
end;
|
||
|
||
// Рамка полосы
|
||
ACanvas.Brush.Style := bsClear;
|
||
ACanvas.Pen.Color := CLR_BORDER;
|
||
ACanvas.Rectangle(BX, Y_BAR_TOP, BX + BW, Y_BAR_BOT);
|
||
|
||
// ================================================================
|
||
// ВЕРХНЯЯ ШКАЛА — dBm метки (-120..-100..-20..0)
|
||
// ================================================================
|
||
ACanvas.Font.Name := 'Courier New';
|
||
ACanvas.Font.Size := 6;
|
||
ACanvas.Font.Style := [];
|
||
ACanvas.Brush.Style:= bsClear;
|
||
|
||
for i := 0 to High(DBM_MARKS) do
|
||
begin
|
||
X := DBtoX(DBM_MARKS[i]);
|
||
// Длинный тик вверх от полосы
|
||
VLine(X, Y_BAR_TOP - TICK_LONG, Y_BAR_TOP - 1, CLR_TICK_WHITE);
|
||
// Метка
|
||
Lbl := DBM_LABELS[i];
|
||
TW := ACanvas.TextWidth(Lbl);
|
||
ACanvas.Font.Color := CLR_SCALE_DBM;
|
||
ACanvas.TextOut(X - TW div 2, Y_BAR_TOP - TICK_LONG - ACanvas.TextHeight(Lbl) - 1, Lbl);
|
||
end;
|
||
|
||
// Мелкие тики каждые 10 dB между основными (каждые 5)
|
||
i := -125;
|
||
while i < -13 do
|
||
begin
|
||
X := DBtoX(i);
|
||
VLine(X, Y_BAR_TOP - TICK_MED, Y_BAR_TOP - 1, CLR_TICK_GREEN);
|
||
Inc(i, 5);
|
||
end;
|
||
|
||
// ================================================================
|
||
// НИЖНЯЯ ШКАЛА — S1..S9..+60
|
||
// ================================================================
|
||
for i := 0 to High(S_MARKS_DBM) do
|
||
begin
|
||
X := DBtoX(S_MARKS_DBM[i]);
|
||
// Тик вниз от полосы
|
||
if S_MARKS_DBM[i] >= DB_S9 then
|
||
VLine(X, Y_BAR_BOT + 1, Y_BAR_BOT + TICK_LONG, CLR_TICK_BLUE)
|
||
else
|
||
VLine(X, Y_BAR_BOT + 1, Y_BAR_BOT + TICK_LONG, CLR_TICK_GREEN);
|
||
// Метка
|
||
Lbl := S_MARKS_LBL[i];
|
||
TW := ACanvas.TextWidth(Lbl);
|
||
if S_MARKS_DBM[i] >= DB_S9 then
|
||
ACanvas.Font.Color := CLR_SCALE_S_BLUE
|
||
else
|
||
ACanvas.Font.Color := CLR_SCALE_S_GREEN;
|
||
ACanvas.TextOut(X - TW div 2, Y_BAR_BOT + TICK_LONG + 1, Lbl);
|
||
end;
|
||
|
||
// ================================================================
|
||
// ЛЕВАЯ КОЛОНКА — цифровое значение
|
||
// ================================================================
|
||
// dBm значение (крупно, вверху)
|
||
ACanvas.Font.Size := 8;
|
||
ACanvas.Font.Style := [fsBold];
|
||
ACanvas.Font.Color := CLR_LABEL_DBM;
|
||
Lbl := Format('%6.1f', [Value]);
|
||
ACanvas.TextOut(R.Left + 1, R.Top + 1, Lbl);
|
||
|
||
// 'dBm' мелко под значением
|
||
ACanvas.Font.Size := 6;
|
||
ACanvas.Font.Style := [];
|
||
ACanvas.Font.Color := TColor($00507070);
|
||
ACanvas.TextOut(R.Left + 1, Y_BAR_TOP - ACanvas.TextHeight('dBm') - 1, 'dBm');
|
||
|
||
// S-значение (внизу)
|
||
ACanvas.Font.Size := 8;
|
||
ACanvas.Font.Style := [fsBold];
|
||
ACanvas.Font.Color := CLR_LABEL_S;
|
||
if Value >= -73.0 then
|
||
begin
|
||
Over := Round(Value - (-73.0));
|
||
if Over < 5 then
|
||
Lbl := 'S9'
|
||
else
|
||
begin
|
||
Over := ((Over + 5) div 10) * 10;
|
||
if Over = 0 then Over := 10;
|
||
Lbl := Format('S9+%d', [Over]);
|
||
end;
|
||
end
|
||
else if Value <= -121.0 then
|
||
Lbl := 'S1'
|
||
else
|
||
begin
|
||
Lbl := 'S1';
|
||
for SNum := 1 to 8 do
|
||
if Value >= (-121.0 + (SNum - 1) * 6.0) then
|
||
Lbl := 'S' + IntToStr(SNum);
|
||
end;
|
||
ACanvas.TextOut(R.Left + 1, Y_BAR_BOT + TICK_LONG + 1, Lbl);
|
||
end;
|
||
|
||
// Полупрозрачная белая заливка зоны X1..X2, Y1..Y2.
|
||
// Вызывается ПОСЛЕ DrawSMeterWide — FSmBitmap уже содержит нарисованный фон.
|
||
// Используем TLazIntfImage для попиксельного alpha-blend.
|
||
procedure TMainForm.DrawSMeterZone(ACanvas: TCanvas; R: TRect;
|
||
X1, X2, Y1, Y2: Integer);
|
||
const
|
||
ALPHA = 16384; // ~25% осветления (от 0..65535)
|
||
var
|
||
Img: TLazIntfImage;
|
||
FC: TFPColor;
|
||
IX, IY: Integer;
|
||
begin
|
||
if (X2 <= X1) or (FSmBitmap = nil) then Exit;
|
||
// Получаем IntfImage из off-screen bitmap
|
||
Img := FSmBitmap.CreateIntfImage;
|
||
try
|
||
for IY := Y1 to Y2 - 1 do
|
||
for IX := X1 to X2 - 1 do
|
||
begin
|
||
FC := Img.Colors[IX, IY];
|
||
FC.Red := Min(65535, FC.Red + ALPHA);
|
||
FC.Green := Min(65535, FC.Green + ALPHA);
|
||
FC.Blue := Min(65535, FC.Blue + ALPHA);
|
||
Img.Colors[IX, IY] := FC;
|
||
end;
|
||
// Записываем изменения обратно в bitmap
|
||
FSmBitmap.LoadFromIntfImage(Img);
|
||
finally
|
||
Img.Free;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.PbSMeterRightPaint(Sender: TObject);
|
||
var
|
||
W, H: Integer;
|
||
ZX1, ZX2, ZY1, ZY2: Integer;
|
||
begin
|
||
W := PbSMeterRight.Width;
|
||
H := PbSMeterRight.Height;
|
||
if (W <= 0) or (H <= 0) then Exit;
|
||
|
||
// Создаём/пересоздаём off-screen буфер
|
||
if (FSmBitmap = nil) or (FSmBitmap.Width <> W) or (FSmBitmap.Height <> H) then
|
||
begin
|
||
FreeAndNil(FSmBitmap);
|
||
FSmBitmap := TBitmap.Create;
|
||
FSmBitmap.SetSize(W, H);
|
||
end;
|
||
|
||
// Рисуем основной S-метр в off-screen буфер
|
||
DrawSMeterWide(FSmBitmap.Canvas, Rect(0, 0, W, H),
|
||
FLastSMeter, FSMeterPeak, FSMeterMin,
|
||
ZX1, ZX2, ZY1, ZY2);
|
||
|
||
// Полупрозрачная белая заливка зоны Min..Peak поверх нарисованного
|
||
DrawSMeterZone(FSmBitmap.Canvas, Rect(0, 0, W, H), ZX1, ZX2, ZY1, ZY2);
|
||
|
||
// Выводим на экран одним блоком (без мерцания)
|
||
PbSMeterRight.Canvas.Draw(0, 0, FSmBitmap);
|
||
end;
|
||
|
||
|
||
procedure TMainForm.PbFwdPowerPaint(Sender: TObject);
|
||
begin
|
||
DrawBarMeter(PbFwdPower.Canvas, Rect(0,0,PbFwdPower.Width,PbFwdPower.Height),
|
||
FLastFwdW, 150, CLR_METER_ON);
|
||
end;
|
||
|
||
procedure TMainForm.PbSWRPaint(Sender: TObject);
|
||
begin
|
||
DrawBarMeter(PbSWR.Canvas, Rect(0,0,PbSWR.Width,PbSWR.Height),
|
||
FLastSWR - 1.0, 4.0, CLR_AMBER);
|
||
end;
|
||
|
||
procedure TMainForm.PbSpectrumPaint(Sender: TObject);
|
||
begin
|
||
if (FSpectrumBitmap.Width > 0) and (FSpectrumBitmap.Height > 0) then
|
||
PbSpectrum.Canvas.Draw(0, 0, FSpectrumBitmap)
|
||
else
|
||
begin
|
||
PbSpectrum.Canvas.Brush.Color := CLR_BG;
|
||
PbSpectrum.Canvas.FillRect(Rect(0,0,PbSpectrum.Width,PbSpectrum.Height));
|
||
end;
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Ruler — полоса частотных меток между спектром и водопадом
|
||
// ---------------------------------------------------------------------------
|
||
procedure TMainForm.DrawRuler;
|
||
// ОПТИМИЗАЦИЯ: рисуем в offscreen FRulerBitmap, перерисовываем только когда
|
||
// изменился FCenterFreq. Экономим TextOut+LineTo на каждом тике таймера (20fps).
|
||
var
|
||
C: TCanvas;
|
||
W, H: Integer;
|
||
i, X: Integer;
|
||
FreqStart, FreqHz: Double;
|
||
VfoX: Integer;
|
||
Lbl: string;
|
||
TW: Integer;
|
||
begin
|
||
if PbRuler = nil then Exit;
|
||
W := PbRuler.Width;
|
||
H := PbRuler.Height;
|
||
if (W <= 0) or (H <= 0) then Exit;
|
||
|
||
// Пропускаем перерисовку если ни частота центра, ни активный VFO не изменились
|
||
if (Abs(FCenterFreq - FRulerLastFreq) < 0.5) and
|
||
(Abs(ActiveVfoFreq - FRulerLastVfo) < 0.5) then Exit;
|
||
FRulerLastFreq := FCenterFreq;
|
||
FRulerLastVfo := ActiveVfoFreq;
|
||
|
||
if (FRulerBitmap.Width <> W) or (FRulerBitmap.Height <> H) then
|
||
FRulerBitmap.SetSize(W, H);
|
||
|
||
C := FRulerBitmap.Canvas;
|
||
PaintVerticalGradient(C, W, H, TColor($00372A12), TColor($00705A2A));
|
||
C.Pen.Color := TColor($00685624);
|
||
C.Pen.Width := 1;
|
||
C.MoveTo(0, 0); C.LineTo(W, 0);
|
||
C.MoveTo(0, H-1); C.LineTo(W, H-1);
|
||
C.Font.Name := 'Courier New';
|
||
C.Font.Size := 7;
|
||
C.Brush.Style := bsClear;
|
||
FreqStart := FCenterFreq - FSpanHz / 2;
|
||
for i := 0 to 8 do
|
||
begin
|
||
X := ScaleX(i, 8, W);
|
||
C.Pen.Color := TColor($00685624);
|
||
C.MoveTo(X, 0); C.LineTo(X, H div 2);
|
||
FreqHz := FreqStart + i * FSpanHz / 8;
|
||
Lbl := Format('%.3f', [FreqHz / 1e6]);
|
||
TW := C.TextWidth(Lbl);
|
||
C.Font.Color := TColor($00C8D8E2);
|
||
C.TextOut(X - TW div 2, H div 2 - 1, Lbl);
|
||
end;
|
||
VfoX := Round((ActiveVfoFreq - FCenterFreq + FSpanHz / 2) / FSpanHz * W);
|
||
if (VfoX >= 0) and (VfoX < W) then
|
||
begin
|
||
C.Pen.Color := TColor($0000FF80);
|
||
C.Pen.Width := 2;
|
||
C.MoveTo(VfoX, 0); C.LineTo(VfoX, H - 1);
|
||
C.Pen.Width := 1;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.PbRulerPaint(Sender: TObject);
|
||
begin
|
||
DrawRuler; // обновит FRulerBitmap только если FCenterFreq изменился
|
||
if (FRulerBitmap.Width > 0) and (FRulerBitmap.Height > 0) then
|
||
PbRuler.Canvas.Draw(0, 0, FRulerBitmap);
|
||
end;
|
||
procedure TMainForm.PbWaterfallPaint(Sender: TObject);
|
||
// Отображаем FWfBitmap — он уже содержит готовый кадр из DrawWaterfall.
|
||
// DrawWaterfall делает скролл через Move() в байтах IntfImage (~0.3ms),
|
||
// пишет новую строку, и один раз LoadFromIntfImage (~2ms).
|
||
// Здесь просто Canvas.Draw — данные уже в bitmap, GDI копирует за ~1ms.
|
||
var
|
||
W, H: Integer;
|
||
begin
|
||
W := PbWaterfall.Width;
|
||
H := PbWaterfall.Height;
|
||
if (W <= 0) or (H <= 0) then Exit;
|
||
|
||
if (FWfBitmap <> nil) and (FWfBitmap.Width = W) and (FWfBitmap.Height = H) then
|
||
PbWaterfall.Canvas.Draw(0, 0, FWfBitmap)
|
||
else
|
||
begin
|
||
PbWaterfall.Canvas.Brush.Color := clBlack;
|
||
PbWaterfall.Canvas.FillRect(Rect(0, 0, W, H));
|
||
end;
|
||
|
||
if FMarkerActive then
|
||
DrawMarkerLine(PbWaterfall.Canvas, W, H);
|
||
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);
|
||
|
||
if PbSMeterRight <> nil then PbSMeterRight.Invalidate;
|
||
end;
|
||
|
||
procedure TMainForm.SpectrumTimerTick(Sender: TObject);
|
||
var
|
||
NowTick: QWord;
|
||
ElapsedTick: QWord;
|
||
begin
|
||
// Синхронизируем размеры если PaintBox изменился (после ресайза)
|
||
if (PbSpectrum.Width <> FSpectrumWidth) or
|
||
(PbSpectrum.Height <> FSpectrumHeight) or
|
||
(FSpectrumWidth = 0) then
|
||
ResizeSpectrumPanels;
|
||
ResizeSMeter;
|
||
if FSpectrumWidth <= 0 then Exit;
|
||
|
||
// Проверка таймаута — трансивер не отвечает
|
||
if FRunning and FNetwork.Running then
|
||
begin
|
||
NowTick := GetTickCount64;
|
||
ElapsedTick := NowTick - FRXStartTime;
|
||
// Ждём первый пакет 5 секунд после старта
|
||
if (FRXLastPktTime = 0) and (ElapsedTick > 5000) then
|
||
begin
|
||
BtnStartStop.Caption := 'START';
|
||
StyleButton(BtnStartStop, False);
|
||
FRunning := False;
|
||
FNetwork.SetRunAndFreq(False, FCenterFreq, FCenterFreq, 0);
|
||
StatusBar1.Panels[4].Text := 'ТРАНСИВЕР НЕДОСТУПЕН';
|
||
BtnMOX.Enabled := False;
|
||
Exit;
|
||
end;
|
||
// После первого пакета — следим чтобы поток не прерывался более 3 сек
|
||
if (FRXLastPktTime > 0) and ((NowTick - FRXLastPktTime) > 3000) then
|
||
begin
|
||
BtnStartStop.Caption := 'START';
|
||
StyleButton(BtnStartStop, False);
|
||
FRunning := False;
|
||
FNetwork.SetRunAndFreq(False, FCenterFreq, FCenterFreq, 0);
|
||
StatusBar1.Panels[4].Text := 'ПОТЕРЯ СВЯЗИ С ТРАНСИВЕРОМ';
|
||
BtnMOX.Enabled := False;
|
||
Exit;
|
||
end;
|
||
end;
|
||
|
||
if FRunning then
|
||
begin
|
||
// Обновляем StatusBar из таймера — без Synchronize в сетевом потоке
|
||
StatusBar1.Panels[3].Text :=
|
||
Format('RX DDC%d | seq=%d | pkts=%d',
|
||
[FLastDDCIndex, FLastDDCSeq, FRXPacketCount]);
|
||
|
||
if FWDSPReady then
|
||
begin
|
||
FDSPEngine.UpdateSpectrum;
|
||
FLastSMeter := FDSPEngine.GetSMeterDBm;
|
||
FDSPEngine.SetSpectrumWidth(FSpectrumWidth); // актуальная ширина для SetAGC/SetAGCTop
|
||
FDSPEngine.UpdateAGCLines(FSpectrumWidth);
|
||
end;
|
||
// Обновляем оверлей если видим
|
||
if Assigned(FVfoOverlay) and FVfoOverlay.Visible then
|
||
begin
|
||
FVfoOverlay.UpdateSMeter(FLastSMeter);
|
||
FVfoOverlay.UpdateVfo(FVfoA);
|
||
PositionVfoOverlay;
|
||
end;
|
||
// Рисуем только при новых спектральных данных/изменении UI (FSpectrumDirty).
|
||
// Это резко снижает CPU в холостом режиме.
|
||
if FSpectrumDirty then
|
||
begin
|
||
DrawSpectrum;
|
||
PbSpectrum.Invalidate;
|
||
|
||
// Водопад рисуем на каждом новом спектральном кадре для плавности.
|
||
DrawWaterfall;
|
||
PbWaterfall.Invalidate;
|
||
|
||
if (Abs(FCenterFreq - FRulerLastFreq) >= 0.5) or
|
||
(Abs(FVfoA - FRulerLastVfo) >= 0.5) then
|
||
PbRuler.Invalidate;
|
||
FSpectrumDirty := False;
|
||
end;
|
||
end
|
||
else
|
||
begin
|
||
// Не подключены — показываем чистый экран (без анимации)
|
||
if not FNetwork.Connected then
|
||
begin
|
||
// Очищаем до фона один раз при отключении
|
||
// (просто не вызываем Draw* — бitmaps остаются как были)
|
||
// Но при первом старте покажем пустой фон:
|
||
if FSpectrumBitmap.Width <> FSpectrumWidth then
|
||
begin
|
||
FSpectrumBitmap.SetSize(FSpectrumWidth, FSpectrumHeight);
|
||
ResetSpectrumBuf;
|
||
DrawSpectrum;
|
||
PbSpectrum.Invalidate;
|
||
end;
|
||
end
|
||
else
|
||
begin
|
||
// Подключены, но не запущены — статичный спектр (шум)
|
||
DrawSpectrum;
|
||
PbSpectrum.Invalidate;
|
||
FSpectrumDirty := False;
|
||
end;
|
||
end;
|
||
|
||
// Если не в режиме RUN, но есть "грязный" кадр (drag/marker/UI), перерисуем.
|
||
if FSpectrumDirty and (not FRunning) then
|
||
begin
|
||
DrawSpectrum;
|
||
DrawWaterfall;
|
||
PbSpectrum.Invalidate;
|
||
if (Abs(FCenterFreq - FRulerLastFreq) >= 0.5) or
|
||
(Abs(FVfoA - FRulerLastVfo) >= 0.5) then
|
||
PbRuler.Invalidate;
|
||
PbWaterfall.Invalidate;
|
||
FSpectrumDirty := False;
|
||
end;
|
||
|
||
// Пушим текущее состояние в веб-клиенты (если есть)
|
||
if Assigned(FWebServer) then
|
||
FWebServer.PushSpectrum(
|
||
FSpectrumBuf, 1024,
|
||
FWfAvgBuf,
|
||
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);
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// Network callbacks → sync helpers
|
||
// ===========================================================================
|
||
|
||
procedure TMainForm.OnDeviceFound(const Dev: THPSDRDevice);
|
||
var
|
||
BoardName, Entry: string;
|
||
Sync: TDeviceFoundSync;
|
||
M: TThreadMethod;
|
||
begin
|
||
BoardName := BoardTypeName(Dev.BoardType);
|
||
Entry := Format('%s %s FW:%d DDC:%d',
|
||
[Dev.IPAddress, BoardName,
|
||
Dev.FirmwareVersion, Dev.NumDDCs]);
|
||
Sync := TDeviceFoundSync.Create(Self, Dev, Entry);
|
||
try
|
||
M := Sync.Execute;
|
||
TThread.Synchronize(nil, M);
|
||
finally
|
||
Sync.Free;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.DoAddDevice(const Dev: THPSDRDevice; const Entry: string);
|
||
var
|
||
Idx: Integer;
|
||
begin
|
||
Idx := Length(FDevices);
|
||
SetLength(FDevices, Idx + 1);
|
||
FDevices[Idx].Dev := Dev;
|
||
FDevices[Idx].Display := Entry;
|
||
Inc(FDeviceCount);
|
||
|
||
if Assigned(FDeviceDialog) then
|
||
FDeviceDialog.AddDiscovered(Dev.IPAddress, Entry, Dev.BoardType);
|
||
|
||
StatusBar1.Panels[4].Text := Format('Found: %s', [Dev.IPAddress]);
|
||
end;
|
||
|
||
procedure TMainForm.OnHPStatusCB(const Status: THighPriorityStatus);
|
||
var
|
||
ExcPwr, FwdPwr, RevPwr: Word;
|
||
SupplyV, FwdW, SWRV: Double;
|
||
Sync: TStatusUISync;
|
||
M: TThreadMethod;
|
||
begin
|
||
ExcPwr := (Status.ExciterPwr0Hi shl 8) or Status.ExciterPwr0Lo;
|
||
FwdPwr := (Status.FwdPwrAlex0Hi shl 8) or Status.FwdPwrAlex0Lo;
|
||
RevPwr := (Status.RevPwrAlex0Hi shl 8) or Status.RevPwrAlex0Lo;
|
||
SupplyV := ADCToSupplyVolts((Status.SupplyVoltsHi shl 8) or Status.SupplyVoltsLo);
|
||
// FLastSMeter обновляется только из WDSP (GetSMeterDBm) в SpectrumTimerTick —
|
||
// ExciterPwr это мощность TX, не уровень принятого сигнала.
|
||
FwdW := ADCToWatts100(FwdPwr);
|
||
if FwdW > 0 then
|
||
SWRV := 1 + Sqrt(RevPwr / FwdPwr)
|
||
else
|
||
SWRV := 1.0;
|
||
Sync := TStatusUISync.Create(Self, FwdW, SWRV, SupplyV,
|
||
(Status.StatusBits and HPS_PLL_LOCKED) <> 0);
|
||
try
|
||
M := Sync.Execute;
|
||
TThread.Synchronize(nil, M);
|
||
finally
|
||
Sync.Free;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.DoUpdateStatus(FwdW, SWRV, SupplyV: Double; PLLLock: Boolean);
|
||
var
|
||
PLLStr: string;
|
||
begin
|
||
FLastFwdW := FwdW;
|
||
FLastSWR := SWRV;
|
||
LblFwdPwr.Caption := Format('%.0fW', [FwdW]);
|
||
LblSWRVal.Caption := Format('SWR:%.1f', [SWRV]);
|
||
if PLLLock then PLLStr := 'PLL OK' else PLLStr := 'PLL?';
|
||
StatusBar1.Panels[2].Text :=
|
||
Format('Supply: %.1fV | FWD: %.0fW | SWR: %.1f | %s',
|
||
[SupplyV, FwdW, SWRV, PLLStr]);
|
||
// Panels[3] зарезервирована для RX stats (DDC seq/pkt count)
|
||
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
|
||
// TODO: WDSP TXA
|
||
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;
|
||
end;
|
||
|
||
procedure TMainForm.BtnDiscoverClick(Sender: TObject);
|
||
begin
|
||
// Открываем диалог выбора устройства
|
||
if not Assigned(FDeviceDialog) then
|
||
FDeviceDialog := TDeviceDialog.Create(Self);
|
||
FDeviceDialog.OnDiscover := BtnDiscoverFromDialog;
|
||
FDeviceDialog.ShowModal;
|
||
// Результат диалога не обрабатываем здесь — START сам спросит если нужно
|
||
end;
|
||
|
||
procedure TMainForm.BtnDiscoverFromDialog(Sender: TObject);
|
||
// Запускается когда пользователь нажимает DISCOVER внутри диалога
|
||
begin
|
||
SetLength(FDevices, 0);
|
||
FDeviceCount := 0;
|
||
StatusBar1.Panels[4].Text := 'Discovering...';
|
||
FNetwork.DirectIP := '';
|
||
TDiscoverThread.Create(FNetwork, Self);
|
||
end;
|
||
|
||
procedure TMainForm.BtnStartStopClick(Sender: TObject);
|
||
var
|
||
Idx, i: Integer;
|
||
Dev: THPSDRDevice;
|
||
GenPkt: TGeneralPacket;
|
||
DUCPkt: TDUCSpecificPacket;
|
||
G_Settings: TGlobalSettings;
|
||
PreG: TGlobalSettings;
|
||
PreBands: array[0..CFG_BAND_COUNT-1] of TBandSettings;
|
||
PreloadRate: Integer;
|
||
FoundByIP: Boolean;
|
||
AutoIP: string;
|
||
begin
|
||
if FNetwork.Connected then
|
||
begin
|
||
// --- STOP ---
|
||
if FRunning then
|
||
begin
|
||
FNetwork.UpdateState(FCenterFreq, FCenterFreq, 0, False, True, True);
|
||
FNetwork.SetRunAndFreq(False, FCenterFreq, FCenterFreq, 0);
|
||
FRunning := False;
|
||
FTransmitting := False;
|
||
if FWDSPReady then FDSPEngine.SetTXRun(False);
|
||
StyleButton(BtnMOX, False);
|
||
end;
|
||
FRXPacketCount := 0;
|
||
FActiveDDC := 0;
|
||
StatusBar1.Panels[3].Text := 'RX: waiting...';
|
||
ResetSpectrumBuf;
|
||
DrawSpectrum;
|
||
PbSpectrum.Invalidate;
|
||
FNetwork.Disconnect;
|
||
BtnStartStop.Caption := 'START';
|
||
StyleButton(BtnStartStop, False);
|
||
StatusBar1.Panels[4].Text := 'Not connected';
|
||
StatusBar1.Panels[0].Text := 'IP: --';
|
||
StatusBar1.Panels[1].Text := 'Board: --';
|
||
BtnMOX.Enabled := False;
|
||
Exit;
|
||
end;
|
||
|
||
// --- START ---
|
||
AutoIP := '';
|
||
FPendingBoardType := 0;
|
||
if Assigned(FDeviceDialog) then
|
||
AutoIP := FDeviceDialog.GetAutoStartIP;
|
||
|
||
if AutoIP <> '' then
|
||
begin
|
||
FPendingIP := AutoIP;
|
||
FPendingBoardType := FDeviceDialog.GetAutoStartBoardType;
|
||
end
|
||
else
|
||
begin
|
||
if not Assigned(FDeviceDialog) then
|
||
FDeviceDialog := TDeviceDialog.Create(Self);
|
||
FDeviceDialog.OnDiscover := BtnDiscoverFromDialog;
|
||
if FDeviceDialog.ShowModal <> mrOk then Exit;
|
||
if not FDeviceDialog.DialogResult.Accepted then Exit;
|
||
FPendingIP := FDeviceDialog.DialogResult.IPAddress;
|
||
FPendingBoardType := FDeviceDialog.GetSavedBoardType(
|
||
FDeviceDialog.DialogResult.SavedIdx);
|
||
end;
|
||
|
||
// Создаём запись устройства из IP + BoardType из диалога если есть
|
||
FillChar(Dev, SizeOf(Dev), 0);
|
||
Dev.IPAddress := FPendingIP;
|
||
Dev.BoardType := FPendingBoardType;
|
||
|
||
// Подбираем sample rate до открытия WDSP, чтобы не делать двойной Open:
|
||
// 1) Open на 192k, 2) сразу ChangeSampleRate+Open ещё раз.
|
||
// Если устройство уже было обнаружено (есть MAC) и есть сохранённый профиль,
|
||
// создаём WDSPEngine сразу с нужной частотой.
|
||
PreloadRate := FSampleRate;
|
||
FoundByIP := False;
|
||
for i := 0 to High(FDevices) do
|
||
if SameText(FDevices[i].Dev.IPAddress, Dev.IPAddress) then
|
||
begin
|
||
FoundByIP := True;
|
||
if FSettings.LoadDevice(FDevices[i].Dev.MAC, PreG, PreBands) and (PreG.SampleRate > 0) then
|
||
PreloadRate := PreG.SampleRate;
|
||
Break;
|
||
end;
|
||
if FoundByIP and (PreloadRate > 0) then
|
||
begin
|
||
FSampleRate := PreloadRate;
|
||
FSpanHz := PreloadRate;
|
||
if (not FWDSPReady) and (Assigned(FDSPEngine)) and (FDSPEngine.SampleRate <> PreloadRate) then
|
||
RecreateDSPEngine(PreloadRate);
|
||
end;
|
||
|
||
// --- Открываем WDSP асинхронно чтобы не блокировать UI ---
|
||
FPendingDev := Dev;
|
||
BtnStartStop.Enabled := False;
|
||
StatusBar1.Panels[3].Text := 'Opening WDSP...';
|
||
if FWDSPReady then
|
||
// WDSP уже открыт — сразу подключаемся
|
||
DoConnectDevice(Dev)
|
||
else
|
||
// Открываем в фоновом потоке
|
||
TWDSPOpenThread.Create(Self);
|
||
end;
|
||
|
||
procedure TMainForm.OnWDSPOpenDone(Success: Boolean);
|
||
begin
|
||
// Вызывается из TWDSPOpenThread.SyncDone — уже в главном потоке
|
||
FWDSPReady := Success;
|
||
if not Success then
|
||
begin
|
||
StatusBar1.Panels[3].Text := 'WDSP open failed';
|
||
BtnStartStop.Enabled := True;
|
||
BtnStartStop.Caption := 'START';
|
||
StyleButton(BtnStartStop, False);
|
||
ShowMessage('WDSP not loaded.' + LineEnding +
|
||
IfThen(FDSPEngine.LastError <> '', FDSPEngine.LastError, 'Unknown WDSP init error.') + LineEnding +
|
||
'Put wdsp.dll рядом с ewsdr.exe (той же разрядности, что и приложение).');
|
||
Exit;
|
||
end;
|
||
DoConnectDevice(FPendingDev);
|
||
end;
|
||
|
||
procedure TMainForm.DoConnectDevice(const Dev: THPSDRDevice);
|
||
var
|
||
GenPkt: TGeneralPacket;
|
||
DUCPkt: TDUCSpecificPacket;
|
||
G_Settings: TGlobalSettings;
|
||
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);
|
||
FVolume := G_Settings.Volume;
|
||
FDriveLevel := G_Settings.DriveLevel;
|
||
FActiveVfo := G_Settings.ActiveVfo;
|
||
FCurrentBand := G_Settings.LastBand;
|
||
// SampleRate — глобальный, загружаем до RestoreBand
|
||
if G_Settings.SampleRate > 0 then
|
||
begin
|
||
FSampleRate := G_Settings.SampleRate;
|
||
FSpanHz := FSampleRate;
|
||
end;
|
||
BtnNR.Tag := EnsureRange(G_Settings.NRMode, 0, 4); UpdateNRButton;
|
||
BtnNB.Tag := EnsureRange(G_Settings.NBMode, 0, 2); UpdateNBButton;
|
||
BtnSNB.Tag := Ord(G_Settings.SNBEnabled); UpdateSNBButton;
|
||
BtnANF.Tag := Ord(G_Settings.ANFEnabled); UpdateANFButton;
|
||
FWfAGCEnabled := G_Settings.WfAGCEnabled; StyleButton(BtnWfAGC, FWfAGCEnabled);
|
||
FWfNFEnabled := G_Settings.WfNFEnabled; StyleButton(BtnWfNF, FWfNFEnabled);
|
||
RestoreBand(FCurrentBand);
|
||
|
||
StatusBar1.Panels[0].Text := 'IP: ' + FNetwork.Device.IPAddress;
|
||
StatusBar1.Panels[1].Text := 'Board: ' + BoardTypeName(FNetwork.Device.BoardType);
|
||
|
||
// General packet
|
||
FillChar(GenPkt, SizeOf(GenPkt), 0);
|
||
GenPkt.Command := CMD_GENERAL;
|
||
GenPkt.Flags37 := $08;
|
||
GenPkt.Flags38 := $01;
|
||
GenPkt.PAConfig := $01;
|
||
if FNetwork.Device.BoardType = 5 then
|
||
GenPkt.AlexEnable := $03
|
||
else
|
||
GenPkt.AlexEnable := $01;
|
||
FNetwork.SendGeneralPacket(GenPkt);
|
||
|
||
if FNetwork.Device.BoardType in [3, 4, 5] then
|
||
FActiveDDC := 2
|
||
else
|
||
FActiveDDC := 0;
|
||
|
||
FNetwork.ConfigureDDCs(1, FSampleRate div 1000, 0);
|
||
|
||
// Синхронизируем samplerate WDSP если нужно
|
||
if FWDSPReady and (FDSPEngine.SampleRate <> FSampleRate) then
|
||
begin
|
||
FWDSPReady := False;
|
||
FDSPEngine.ChangeSampleRate(FSampleRate);
|
||
FWDSPReady := FDSPEngine.Initialized;
|
||
end;
|
||
|
||
if FWDSPReady then
|
||
begin
|
||
FDSPEngine.SetMode(FMode);
|
||
FDSPEngine.SetVolume(FVolume / 100.0);
|
||
ApplyModeFilter;
|
||
ApplyNoiseFilterButtonsToDSP;
|
||
// ChangeSampleRate пересоздаёт WDSP-канал — восстанавливаем все настройки
|
||
FDSPEngine.SetAGCTop(FAGCTop);
|
||
FDSPEngine.SetAGC(TWDSPAGCMode(FAGCMode), 50.0);
|
||
// SetAGC вызывает UpdateAGCLines внутри — линии на спектре обновятся
|
||
end;
|
||
|
||
// DUC specific
|
||
FillChar(DUCPkt, SizeOf(DUCPkt), 0);
|
||
DUCPkt.NumDACs := 1;
|
||
DUCPkt.SidetoneLevel := 50;
|
||
DUCPkt.SidetoneFreqLo := 600;
|
||
DUCPkt.KeyerSpeed := 20;
|
||
DUCPkt.KeyerWeight := 50;
|
||
DUCPkt.DUC0RateHi := Hi(192);
|
||
DUCPkt.DUC0RateLo := Lo(192);
|
||
DUCPkt.DUC0Bits := 24;
|
||
FNetwork.SendDUCSpecific(DUCPkt);
|
||
|
||
FNetwork.UpdateState(FCenterFreq, FCenterFreq, FDriveLevel, False, True, True);
|
||
FNetwork.SetRunAndFreq(True, FCenterFreq, FCenterFreq, FDriveLevel);
|
||
FRunning := True;
|
||
FRXStartTime := GetTickCount64;
|
||
FRXLastPktTime := 0;
|
||
|
||
// Принудительно обновляем AGC линии — гарантированно после всех SetAGC вызовов
|
||
if FWDSPReady then
|
||
FDSPEngine.UpdateAGCLines(FSpectrumWidth); // ещё не получили ни одного пакета
|
||
|
||
BtnStartStop.Caption := 'STOP';
|
||
StyleButton(BtnStartStop, True);
|
||
StatusBar1.Panels[4].Text := 'RUNNING ' + FormatFreq(FVfoA);
|
||
BtnMOX.Enabled := True;
|
||
end;
|
||
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// ActivateVfo — переключаем активный VFO и перестраиваем приёмник.
|
||
// Idx=0 → VFO-A активен, Idx=1 → VFO-B активен.
|
||
// Перестройка всегда происходит на частоту активного VFO (не на оффсет).
|
||
// CTUN при этом сбрасывается чтобы не было путаницы с оффсетом.
|
||
// ---------------------------------------------------------------------------
|
||
// ---------------------------------------------------------------------------
|
||
// FreqToBandIdx — возвращает индекс диапазона для частоты Hz, или -1
|
||
// ---------------------------------------------------------------------------
|
||
function FreqToBandIdx(Hz: Double): Integer;
|
||
const
|
||
// Границы диапазонов [Low, High] в Гц (приблизительные)
|
||
BAND_LO: array[0..10] of Double = (
|
||
1800000, 3500000, 5330000, 7000000, 10100000,
|
||
14000000, 18068000, 21000000, 24890000, 28000000, 50000000);
|
||
BAND_HI: array[0..10] of Double = (
|
||
2000000, 4000000, 5410000, 7300000, 10150000,
|
||
14350000, 18168000, 21450000, 24990000, 29700000, 54000000);
|
||
var
|
||
i: Integer;
|
||
begin
|
||
Result := -1;
|
||
for i := 0 to 10 do
|
||
if (Hz >= BAND_LO[i]) and (Hz <= BAND_HI[i]) then
|
||
begin
|
||
Result := i;
|
||
Exit;
|
||
end;
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// ActiveVfoFreq — частота активного VFO
|
||
// ---------------------------------------------------------------------------
|
||
function TMainForm.ActiveVfoFreq: Int64;
|
||
begin
|
||
if FActiveVfo = 0 then Result := Round(FVfoA)
|
||
else Result := Round(FVfoB);
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// ActivateVfo — переключаем активный VFO и перестраиваем приёмник.
|
||
// Idx=0 → VFO-A активен, Idx=1 → VFO-B активен.
|
||
// Перестройка на частоту выбранного VFO. CTUN не трогаем.
|
||
// ---------------------------------------------------------------------------
|
||
procedure TMainForm.ActivateVfo(Idx: Integer);
|
||
var
|
||
ActiveFreq: Int64;
|
||
NewBand: Integer;
|
||
Offset: Double;
|
||
HalfSpan: Double;
|
||
FreqVisible: Boolean;
|
||
begin
|
||
FActiveVfo := Idx;
|
||
|
||
// Частота нового активного VFO
|
||
if FActiveVfo = 0 then
|
||
ActiveFreq := Round(FVfoA)
|
||
else
|
||
ActiveFreq := Round(FVfoB);
|
||
|
||
// Обновляем визуальный дисплей
|
||
UpdateVfoDisplay;
|
||
|
||
if FCTun then
|
||
begin
|
||
// CTUN включён: проверяем, видна ли новая частота на текущем спектре
|
||
HalfSpan := FSpanHz / 2;
|
||
Offset := ActiveFreq - FCenterFreq;
|
||
FreqVisible := (Offset > -HalfSpan) and (Offset < HalfSpan);
|
||
|
||
if FreqVisible then
|
||
begin
|
||
// Частота видна в окне — просто меняем shift, DDC не трогаем
|
||
if FWDSPReady then FDSPEngine.SetShift(Offset);
|
||
// Сеть не трогаем — DDC (FCenterFreq) остаётся прежним
|
||
end
|
||
else
|
||
begin
|
||
// Частота за пределами спектра — центрируем окно и сбрасываем CTUN
|
||
FCTun := False;
|
||
StyleButton(BtnCTun, False);
|
||
FBandCache[FCurrentBand].CTun := False;
|
||
FCenterFreq := ActiveFreq;
|
||
if FWDSPReady then FDSPEngine.SetShift(0.0);
|
||
if FNetwork.Connected and FNetwork.Running then
|
||
begin
|
||
FNetwork.UpdateState(FCenterFreq, FCenterFreq, FDriveLevel, FTransmitting, True, True);
|
||
FNetwork.SendFullHP;
|
||
end;
|
||
end;
|
||
end
|
||
else
|
||
begin
|
||
// CTUN выключен: перестраиваем DDC на новую частоту
|
||
FCenterFreq := ActiveFreq;
|
||
if FWDSPReady then FDSPEngine.SetShift(0.0);
|
||
if FNetwork.Connected and FNetwork.Running then
|
||
begin
|
||
FNetwork.UpdateState(FCenterFreq, FCenterFreq, FDriveLevel, FTransmitting, True, True);
|
||
FNetwork.SendFullHP;
|
||
end;
|
||
end;
|
||
|
||
// Переключаем кнопку диапазона по новой частоте
|
||
NewBand := FreqToBandIdx(ActiveFreq);
|
||
if (NewBand >= 0) and (NewBand <> FCurrentBand) then
|
||
begin
|
||
StyleButton(BtnBand[FCurrentBand], False);
|
||
FCurrentBand := NewBand;
|
||
StyleButton(BtnBand[FCurrentBand], True);
|
||
end;
|
||
|
||
// Инвалидируем кэш линейки
|
||
FRulerLastFreq := -1.0;
|
||
FRulerLastVfo := -1.0;
|
||
|
||
DrawSpectrum;
|
||
PbSpectrum.Invalidate;
|
||
DrawWaterfall;
|
||
PbWaterfall.Invalidate;
|
||
if PbRuler <> nil then PbRuler.Invalidate;
|
||
end;
|
||
|
||
procedure TMainForm.FreqDispAClick(Sender: TObject);
|
||
begin
|
||
// Клик на VFO-A → активируем A (перестройка на FVfoA)
|
||
if FActiveVfo <> 0 then
|
||
ActivateVfo(0);
|
||
end;
|
||
|
||
procedure TMainForm.FreqDispBClick(Sender: TObject);
|
||
begin
|
||
// Клик на VFO-B → активируем B (перестройка на FVfoB)
|
||
if FActiveVfo <> 1 then
|
||
ActivateVfo(1);
|
||
end;
|
||
|
||
procedure TMainForm.FreqDispAChanged(Sender: TObject; NewFreq: Int64);
|
||
begin
|
||
if FActiveVfo = 0 then
|
||
ApplyVfoA(NewFreq)
|
||
else
|
||
FVfoA := NewFreq; // просто сохраняем, приёмник не перестраиваем
|
||
end;
|
||
|
||
procedure TMainForm.FreqDispBChanged(Sender: TObject; NewFreq: Int64);
|
||
var
|
||
BandIdx: Integer;
|
||
begin
|
||
FVfoB := NewFreq;
|
||
if FActiveVfo = 1 then
|
||
begin
|
||
// VFO-B активен: перестраиваем приёмник
|
||
FCenterFreq := FVfoB;
|
||
if FWDSPReady then FDSPEngine.SetShift(0.0);
|
||
if FNetwork.Connected and FNetwork.Running then
|
||
begin
|
||
FNetwork.UpdateState(FCenterFreq, FCenterFreq, FDriveLevel, FTransmitting, True, True);
|
||
FNetwork.SendFullHP;
|
||
end;
|
||
// Обновляем диапазон
|
||
BandIdx := FreqToBandIdx(FVfoB);
|
||
if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then
|
||
begin
|
||
StyleButton(BtnBand[FCurrentBand], False);
|
||
FCurrentBand := BandIdx;
|
||
StyleButton(BtnBand[FCurrentBand], True);
|
||
end;
|
||
FRulerLastFreq := -1.0; FRulerLastVfo := -1.0;
|
||
DrawSpectrum;
|
||
PbSpectrum.Invalidate;
|
||
if PbRuler <> nil then PbRuler.Invalidate;
|
||
end;
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// UpdateFilterButtons — обновляет подписи кнопок фильтра под текущий режим
|
||
// ---------------------------------------------------------------------------
|
||
procedure TMainForm.UpdateFilterButtons;
|
||
var
|
||
i: Integer;
|
||
Names: array[0..FILT_COUNT-1] of string;
|
||
DefIdx: Integer;
|
||
begin
|
||
case FMode of
|
||
0, 1: begin
|
||
for i := 0 to FILT_COUNT-1 do Names[i] := FILT_SSB_NAMES[i];
|
||
DefIdx := FILT_SSB_DEF;
|
||
end;
|
||
3, 4: begin
|
||
for i := 0 to FILT_COUNT-1 do Names[i] := FILT_CW_NAMES[i];
|
||
DefIdx := FILT_CW_DEF;
|
||
end;
|
||
5: begin
|
||
for i := 0 to FILT_COUNT-1 do Names[i] := FILT_FM_NAMES[i];
|
||
DefIdx := FILT_FM_DEF;
|
||
end;
|
||
else begin
|
||
for i := 0 to FILT_COUNT-1 do Names[i] := FILT_AM_NAMES[i];
|
||
DefIdx := FILT_AM_DEF;
|
||
end;
|
||
end;
|
||
|
||
FFilter := DefIdx;
|
||
case FMode of
|
||
0, 1: FFilterBW := FILT_SSB_BW[DefIdx];
|
||
3, 4: FFilterBW := FILT_CW_BW[DefIdx];
|
||
5: FFilterBW := FILT_FM_BW[DefIdx];
|
||
else FFilterBW := FILT_AM_BW[DefIdx];
|
||
end;
|
||
|
||
for i := 0 to FILT_COUNT-1 do
|
||
begin
|
||
BtnFilter[i].Caption := Names[i];
|
||
StyleButton(BtnFilter[i], i = FFilter);
|
||
end;
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// ApplyVfoA — единая точка смены частоты VFO A
|
||
//
|
||
// CTUN ВЫКЛ (классический режим):
|
||
// FCenterFreq = FVfoA — спектр центрирован на VFO, маркер всегда в центре
|
||
//
|
||
// CTUN ВКЛ (piHPSDR/Thetis режим):
|
||
// FCenterFreq статичен — спектр/водопад не двигаются
|
||
// FVfoA движется по дисплею (маркер гуляет)
|
||
// Перецентрирование — только когда КРАЙ ПОЛОСЫ ФИЛЬТРА выходит за край дисплея
|
||
// (по образцу OpenHPSDR/PowerSDR: "re-centering occurs as the edge of the passband
|
||
// hits the edge of the display")
|
||
// ---------------------------------------------------------------------------
|
||
procedure TMainForm.ApplyVfoA(NewFreq: Int64);
|
||
var
|
||
Offset: Double;
|
||
FiltLo: Double;
|
||
FiltHi: Double;
|
||
HalfSpan: Double;
|
||
Scrolled: Boolean;
|
||
BandIdx: Integer;
|
||
begin
|
||
FVfoA := NewFreq;
|
||
Scrolled := False;
|
||
|
||
if not FCTun then
|
||
begin
|
||
// ---- CTUN OFF ----
|
||
// DDC = VFO, сдвига нет
|
||
FCenterFreq := FVfoA;
|
||
if FWDSPReady then
|
||
FDSPEngine.SetShift(0.0);
|
||
end
|
||
else
|
||
begin
|
||
// ---- CTUN ON ----
|
||
// DDC (FCenterFreq) стоит на месте.
|
||
// SetRXAShiftFreq сдвигает спектр внутри WDSP так чтобы
|
||
// демодулятор принимал сигнал на FVfoA, а не на FCenterFreq.
|
||
// Shift = FVfoA - FCenterFreq (в Гц)
|
||
Offset := FVfoA - FCenterFreq;
|
||
if FWDSPReady then
|
||
FDSPEngine.SetShift(Offset);
|
||
|
||
// Проверяем не вышла ли полоса фильтра за край дисплея
|
||
HalfSpan := FSpanHz / 2;
|
||
case FMode of
|
||
0: begin FiltLo := Offset - FFilterBW; FiltHi := Offset - 100; end;
|
||
1: begin FiltLo := Offset + 100; FiltHi := Offset + FFilterBW; end;
|
||
else begin FiltLo := Offset - FFilterBW/2; FiltHi := Offset + FFilterBW/2; end;
|
||
end;
|
||
|
||
if (FiltHi > HalfSpan) or (FiltLo < -HalfSpan) then
|
||
begin
|
||
// Полоса вышла за край → прокручиваем центр
|
||
if FiltHi > HalfSpan then
|
||
FCenterFreq := FVfoA - HalfSpan * 0.5
|
||
else
|
||
FCenterFreq := FVfoA + HalfSpan * 0.5;
|
||
// Пересчитываем сдвиг после прокрутки
|
||
if FWDSPReady then
|
||
FDSPEngine.SetShift(FVfoA - FCenterFreq);
|
||
// DDC перестраивается на новый FCenterFreq
|
||
Scrolled := True;
|
||
end;
|
||
end;
|
||
|
||
// Обновляем VFO дисплей
|
||
FreqDispA.Frequency := Round(FVfoA);
|
||
|
||
// Обновляем кнопку диапазона если частота перешла в другой диапазон
|
||
BandIdx := FreqToBandIdx(FVfoA);
|
||
if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then
|
||
begin
|
||
StyleButton(BtnBand[FCurrentBand], False);
|
||
FCurrentBand := BandIdx;
|
||
StyleButton(BtnBand[FCurrentBand], True);
|
||
end;
|
||
|
||
// DDC: передаём FCenterFreq (при CTUN OFF = FVfoA, при CTUN ON = фиксирован)
|
||
if FNetwork.Connected and FNetwork.Running then
|
||
begin
|
||
FNetwork.UpdateState(FCenterFreq, FCenterFreq, FDriveLevel, FTransmitting, True, True);
|
||
FNetwork.SendFullHP;
|
||
end;
|
||
|
||
// Перерисовываем спектр всегда (маркер и полоса двигаются)
|
||
// Если идёт drag — не вызываем Draw напрямую, таймер подхватит
|
||
if FSpecDrag then
|
||
FSpectrumDirty := True
|
||
else
|
||
begin
|
||
DrawSpectrum;
|
||
PbSpectrum.Invalidate;
|
||
if Scrolled then
|
||
begin
|
||
DrawWaterfall;
|
||
PbWaterfall.Invalidate;
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// CTUN toggle
|
||
// ---------------------------------------------------------------------------
|
||
procedure TMainForm.BtnCTunClick(Sender: TObject);
|
||
begin
|
||
FCTun := not FCTun;
|
||
StyleButton(BtnCTun, FCTun);
|
||
FBandCache[FCurrentBand].CTun := FCTun;
|
||
if not FCTun then
|
||
begin
|
||
// Выключили CTUN: центрируемся на VFO, shift=0
|
||
FCenterFreq := FVfoA;
|
||
if FWDSPReady then
|
||
FDSPEngine.SetShift(0.0);
|
||
if FNetwork.Connected and FNetwork.Running then
|
||
begin
|
||
FNetwork.UpdateState(FCenterFreq, FCenterFreq, FDriveLevel, FTransmitting, True, True);
|
||
FNetwork.SendFullHP;
|
||
end;
|
||
end
|
||
else
|
||
begin
|
||
// Включили CTUN: фиксируем текущее положение дисплея
|
||
// FCenterFreq остаётся как есть — спектр не прыгает
|
||
end;
|
||
DrawSpectrum;
|
||
DrawWaterfall;
|
||
PbSpectrum.Invalidate;
|
||
PbWaterfall.Invalidate;
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Клик/драг по спектру/водопаду
|
||
// ---------------------------------------------------------------------------
|
||
|
||
// Перевод X пикселя в частоту
|
||
function PixelToFreq(PixelX, PanelWidth: Integer;
|
||
CenterFreq, SpanHz: Double): Double;
|
||
begin
|
||
Result := CenterFreq + (PixelX / PanelWidth - 0.5) * SpanHz;
|
||
end;
|
||
|
||
procedure TMainForm.DoSpectrumClick(PixelX: Integer; PanelWidth: Integer);
|
||
var
|
||
ClickFreq: Int64;
|
||
StepHz: Int64;
|
||
BandIdx: Integer;
|
||
begin
|
||
if PanelWidth <= 0 then Exit;
|
||
StepHz := 100;
|
||
ClickFreq := Round(FCenterFreq + (PixelX / PanelWidth - 0.5) * FSpanHz);
|
||
ClickFreq := (ClickFreq div StepHz) * StepHz;
|
||
if FActiveVfo = 0 then
|
||
ApplyVfoA(ClickFreq)
|
||
else
|
||
begin
|
||
FVfoB := ClickFreq;
|
||
FreqDispB.Frequency := Round(FVfoB);
|
||
FCenterFreq := ClickFreq;
|
||
if FWDSPReady then FDSPEngine.SetShift(0.0);
|
||
if FNetwork.Connected and FNetwork.Running then
|
||
begin
|
||
FNetwork.UpdateState(FCenterFreq, FCenterFreq, FDriveLevel, FTransmitting, True, True);
|
||
FNetwork.SendFullHP;
|
||
end;
|
||
BandIdx := FreqToBandIdx(FVfoB);
|
||
if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then
|
||
begin
|
||
StyleButton(BtnBand[FCurrentBand], False);
|
||
FCurrentBand := BandIdx;
|
||
StyleButton(BtnBand[FCurrentBand], True);
|
||
end;
|
||
FRulerLastFreq := -1.0; FRulerLastVfo := -1.0;
|
||
DrawSpectrum;
|
||
PbSpectrum.Invalidate;
|
||
if PbRuler <> nil then PbRuler.Invalidate;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.DoSpectrumDrag(PixelX: Integer; PanelWidth: Integer);
|
||
var
|
||
dPix: Integer;
|
||
dFreq: Double;
|
||
begin
|
||
if PanelWidth <= 0 then Exit;
|
||
dPix := PixelX - FSpecDragX0;
|
||
dFreq := dPix / PanelWidth * FSpanHz;
|
||
if FCTun then
|
||
begin
|
||
// CTUN ON: drag двигает окно просмотра (FCenterFreq/DDC)
|
||
// VFO остаётся, shift обновляется = VFO - новый FCenterFreq
|
||
FCenterFreq := FSpecDragFreq - dFreq;
|
||
if FWDSPReady then
|
||
begin
|
||
if FActiveVfo = 0 then
|
||
FDSPEngine.SetShift(FVfoA - FCenterFreq)
|
||
else
|
||
FDSPEngine.SetShift(FVfoB - FCenterFreq);
|
||
end;
|
||
if FNetwork.Connected and FNetwork.Running then
|
||
begin
|
||
FNetwork.UpdateState(FCenterFreq, FCenterFreq, FDriveLevel, FTransmitting, True, True);
|
||
FNetwork.SendFullHP;
|
||
end;
|
||
FSpectrumDirty := True;
|
||
end
|
||
else
|
||
begin
|
||
// CTUN OFF: drag двигает VFO, FCenterFreq следует через Apply
|
||
if FActiveVfo = 0 then
|
||
ApplyVfoA(Round(FSpecDragFreq - dFreq))
|
||
else
|
||
begin
|
||
FVfoB := FSpecDragFreq - dFreq;
|
||
FreqDispB.Frequency := Round(FVfoB);
|
||
FCenterFreq := FVfoB;
|
||
if FWDSPReady then FDSPEngine.SetShift(0.0);
|
||
if FNetwork.Connected and FNetwork.Running then
|
||
begin
|
||
FNetwork.UpdateState(FCenterFreq, FCenterFreq, FDriveLevel, FTransmitting, True, True);
|
||
FNetwork.SendFullHP;
|
||
end;
|
||
FSpectrumDirty := True;
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
// Спектр
|
||
procedure TMainForm.PbSpectrumMouseDown(Sender: TObject; Button: TMouseButton;
|
||
Shift: TShiftState; X, Y: Integer);
|
||
begin
|
||
if Button = mbLeft then
|
||
begin
|
||
// Левая кнопка: сбрасываем маркер если активен
|
||
if FMarkerActive then
|
||
begin
|
||
FMarkerActive := 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 FMarkerActive and (Abs(Round(X / PbSpectrum.Width * 1000) - FMarkerX) < 8) then
|
||
begin
|
||
// Клик рядом с текущим маркером — убираем
|
||
FMarkerActive := False;
|
||
end
|
||
else
|
||
begin
|
||
// Ставим маркер в новое место
|
||
FMarkerActive := True;
|
||
FMarkerX := 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 FMarkerActive then
|
||
begin
|
||
FMarkerX := 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 FMarkerActive then
|
||
begin
|
||
FMarkerActive := 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 FMarkerActive and (Abs(Round(X / PbWaterfall.Width * 1000) - FMarkerX) < 8) then
|
||
FMarkerActive := False
|
||
else
|
||
begin
|
||
FMarkerActive := True;
|
||
FMarkerX := 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 FMarkerActive then
|
||
begin
|
||
FMarkerX := Round(X / PbWaterfall.Width * 1000);
|
||
FSpectrumDirty := True;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.PbWaterfallMouseUp(Sender: TObject; Button: TMouseButton;
|
||
Shift: TShiftState; X, Y: Integer);
|
||
begin
|
||
if Button = mbLeft then
|
||
begin
|
||
if (Abs(X - FSpecDragX0) < 4) then
|
||
DoSpectrumClick(X, PbWaterfall.Width);
|
||
FSpecDrag := False;
|
||
PbWaterfall.Cursor := crDefault;
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.BtnBandClick(Sender: TObject);
|
||
var
|
||
NewBand: Integer;
|
||
begin
|
||
NewBand := (Sender as TFlatButton).Tag;
|
||
if (NewBand < 0) or (NewBand >= BAND_COUNT) then Exit;
|
||
if NewBand = FCurrentBand then Exit; // уже на этом диапазоне
|
||
|
||
// Сохраняем настройки текущего диапазона перед сменой
|
||
if FDevConnected then
|
||
begin
|
||
SaveCurrentBand;
|
||
FSettings.Save;
|
||
end;
|
||
|
||
// Переключаем диапазон
|
||
FCurrentBand := NewBand;
|
||
|
||
// Восстанавливаем настройки нового диапазона
|
||
RestoreBand(FCurrentBand);
|
||
end;
|
||
|
||
procedure TMainForm.ApplyModeFilter;
|
||
var
|
||
Lo, Hi, Half: Integer;
|
||
begin
|
||
Half := FFilterBW div 2;
|
||
case FMode of
|
||
0: begin Lo := -FFilterBW; Hi := -100; end; // LSB: верхний срез -100 Гц
|
||
1: begin Lo := 100; Hi := FFilterBW; end; // USB
|
||
2: begin Lo := -Half; Hi := Half; end; // DSB
|
||
3: begin Lo := -Half; Hi := Half; end; // CWL
|
||
4: begin Lo := -Half; Hi := Half; end; // CWU
|
||
5: begin Lo := -Half; Hi := Half; end; // FM
|
||
6: begin Lo := -Half; Hi := Half; end; // AM
|
||
7: begin Lo := -Half; Hi := Half; end; // SAM
|
||
else Lo := -Half; Hi := Half;
|
||
end;
|
||
if FWDSPReady then
|
||
FDSPEngine.SetFilter(Lo, Hi);
|
||
end;
|
||
|
||
procedure TMainForm.BtnModeClick(Sender: TObject);
|
||
var
|
||
i, N: Integer;
|
||
begin
|
||
N := (Sender as TFlatButton).Tag;
|
||
FMode := N;
|
||
for i := 0 to MODE_COUNT - 1 do StyleButton(BtnMode[i], i = FMode);
|
||
if FWDSPReady then
|
||
FDSPEngine.SetMode(N);
|
||
UpdateFilterButtons; // обновляем подписи и дефолт фильтра под новый режим
|
||
ApplyModeFilter;
|
||
end;
|
||
|
||
procedure TMainForm.BtnFilterClick(Sender: TObject);
|
||
var
|
||
i, N: Integer;
|
||
begin
|
||
N := (Sender as TFlatButton).Tag;
|
||
FFilter := N;
|
||
// Устанавливаем полосу по текущему режиму
|
||
case FMode of
|
||
0, 1: FFilterBW := FILT_SSB_BW[N];
|
||
3, 4: FFilterBW := FILT_CW_BW[N];
|
||
5: FFilterBW := FILT_FM_BW[N];
|
||
else FFilterBW := FILT_AM_BW[N]; // DSB, AM, SAM
|
||
end;
|
||
for i := 0 to FILT_COUNT - 1 do StyleButton(BtnFilter[i], i = FFilter);
|
||
ApplyModeFilter;
|
||
end;
|
||
|
||
procedure TMainForm.BtnVfoSwapClick(Sender: TObject);
|
||
var
|
||
Tmp: Double;
|
||
BandIdx: Integer;
|
||
ActiveFreq: Int64;
|
||
begin
|
||
// Меняем частоты местами
|
||
Tmp := FVfoA; FVfoA := FVfoB; FVfoB := Tmp;
|
||
|
||
// Определяем новую частоту активного VFO
|
||
if FActiveVfo = 0 then ActiveFreq := Round(FVfoA)
|
||
else ActiveFreq := Round(FVfoB);
|
||
|
||
// Перестраиваем приёмник
|
||
FCenterFreq := ActiveFreq;
|
||
if FWDSPReady then FDSPEngine.SetShift(0.0);
|
||
if FNetwork.Connected and FNetwork.Running then
|
||
begin
|
||
FNetwork.UpdateState(FCenterFreq, FCenterFreq, FDriveLevel, FTransmitting, True, True);
|
||
FNetwork.SendFullHP;
|
||
end;
|
||
|
||
// Обновляем кнопку диапазона
|
||
BandIdx := FreqToBandIdx(ActiveFreq);
|
||
if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then
|
||
begin
|
||
StyleButton(BtnBand[FCurrentBand], False);
|
||
FCurrentBand := BandIdx;
|
||
StyleButton(BtnBand[FCurrentBand], True);
|
||
end;
|
||
|
||
FRulerLastFreq := -1.0; FRulerLastVfo := -1.0;
|
||
UpdateVfoDisplay;
|
||
DrawSpectrum;
|
||
PbSpectrum.Invalidate;
|
||
if PbRuler <> nil then PbRuler.Invalidate;
|
||
end;
|
||
|
||
procedure TMainForm.BtnVfoACopyBClick(Sender: TObject);
|
||
begin
|
||
// A>B: копируем частоту A в B (VFO-B получает частоту VFO-A)
|
||
FVfoB := FVfoA;
|
||
if FActiveVfo = 1 then
|
||
begin
|
||
// B активен — перестраиваем приёмник на новую частоту B
|
||
FCenterFreq := Round(FVfoB);
|
||
if FWDSPReady then FDSPEngine.SetShift(0.0);
|
||
if FNetwork.Connected and FNetwork.Running then
|
||
begin
|
||
FNetwork.UpdateState(FCenterFreq, FCenterFreq, FDriveLevel, FTransmitting, True, True);
|
||
FNetwork.SendFullHP;
|
||
end;
|
||
FRulerLastFreq := -1.0; FRulerLastVfo := -1.0;
|
||
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;
|
||
|
||
procedure TMainForm.BtnMOXClick(Sender: TObject);
|
||
begin
|
||
FTransmitting := not FTransmitting;
|
||
if FTransmitting then
|
||
begin
|
||
BtnMOX.ClrNorm := TColor($00000044);
|
||
BtnMOX.ClrText := TColor($000000FF);
|
||
BtnMOX.ClrTextAct := TColor($000000FF);
|
||
BtnMOX.Active := True;
|
||
end else
|
||
StyleButton(BtnMOX, False);
|
||
if FWDSPReady then FDSPEngine.SetTXRun(FTransmitting);
|
||
end;
|
||
|
||
procedure TMainForm.BtnMuteClick(Sender: TObject);
|
||
begin
|
||
FMuted := not FMuted;
|
||
StyleButton(BtnMute, FMuted);
|
||
if FMuted then BtnMute.Caption := 'UNMUTE'
|
||
else BtnMute.Caption := 'MUTE';
|
||
if FWDSPReady then FDSPEngine.SetMute(FMuted);
|
||
end;
|
||
|
||
procedure TMainForm.UpdateNRButton;
|
||
begin
|
||
case BtnNR.Tag of
|
||
1: BtnNR.Caption := 'NR';
|
||
2: BtnNR.Caption := 'NR2';
|
||
3: BtnNR.Caption := 'NR3';
|
||
4: BtnNR.Caption := 'NR4';
|
||
else
|
||
BtnNR.Caption := 'NR';
|
||
end;
|
||
StyleButton(BtnNR, BtnNR.Tag <> 0);
|
||
BtnNR.Invalidate;
|
||
end;
|
||
|
||
procedure TMainForm.UpdateNBButton;
|
||
begin
|
||
case BtnNB.Tag of
|
||
1: BtnNB.Caption := 'NB';
|
||
2: BtnNB.Caption := 'NB2';
|
||
else
|
||
BtnNB.Caption := 'NB';
|
||
end;
|
||
StyleButton(BtnNB, BtnNB.Tag <> 0);
|
||
BtnNB.Invalidate;
|
||
end;
|
||
|
||
procedure TMainForm.UpdateSNBButton;
|
||
begin
|
||
BtnSNB.Caption := 'SNB';
|
||
StyleButton(BtnSNB, BtnSNB.Tag <> 0);
|
||
BtnSNB.Invalidate;
|
||
end;
|
||
|
||
procedure TMainForm.UpdateANFButton;
|
||
begin
|
||
BtnANF.Caption := 'ANF';
|
||
StyleButton(BtnANF, BtnANF.Tag <> 0);
|
||
BtnANF.Invalidate;
|
||
end;
|
||
|
||
procedure TMainForm.ApplyNoiseFilterButtonsToDSP;
|
||
begin
|
||
if not FWDSPReady then Exit;
|
||
FDSPEngine.SetNRMode(BtnNR.Tag);
|
||
FDSPEngine.SetNBMode(BtnNB.Tag);
|
||
FDSPEngine.SetSNB(BtnSNB.Tag <> 0);
|
||
FDSPEngine.SetANF(BtnANF.Tag <> 0);
|
||
end;
|
||
|
||
procedure TMainForm.BtnNRClick(Sender: TObject);
|
||
begin
|
||
BtnNR.Tag := (BtnNR.Tag + 1) mod 5;
|
||
UpdateNRButton;
|
||
if FWDSPReady then FDSPEngine.SetNRMode(BtnNR.Tag);
|
||
end;
|
||
|
||
procedure TMainForm.BtnNBClick(Sender: TObject);
|
||
begin
|
||
BtnNB.Tag := (BtnNB.Tag + 1) mod 3;
|
||
UpdateNBButton;
|
||
if FWDSPReady then FDSPEngine.SetNBMode(BtnNB.Tag);
|
||
end;
|
||
|
||
procedure TMainForm.BtnSNBClick(Sender: TObject);
|
||
begin
|
||
BtnSNB.Tag := 1 - BtnSNB.Tag;
|
||
UpdateSNBButton;
|
||
if FWDSPReady then FDSPEngine.SetSNB(BtnSNB.Tag <> 0);
|
||
end;
|
||
|
||
procedure TMainForm.BtnANFClick(Sender: TObject);
|
||
begin
|
||
BtnANF.Tag := 1 - BtnANF.Tag;
|
||
UpdateANFButton;
|
||
if FWDSPReady then FDSPEngine.SetANF(BtnANF.Tag <> 0);
|
||
end;
|
||
|
||
|
||
procedure TMainForm.BtnWfAGCClick(Sender: TObject);
|
||
begin
|
||
FWfAGCEnabled := not FWfAGCEnabled;
|
||
StyleButton(BtnWfAGC, FWfAGCEnabled);
|
||
if not FWfAGCEnabled then
|
||
FWfHigh := -50.0; // сбрасываем к умолчанию при выключении
|
||
end;
|
||
|
||
procedure TMainForm.BtnWfNFClick(Sender: TObject);
|
||
begin
|
||
FWfNFEnabled := not FWfNFEnabled;
|
||
StyleButton(BtnWfNF, FWfNFEnabled);
|
||
if not FWfNFEnabled then
|
||
FWfLow := -120.0;
|
||
end;
|
||
|
||
procedure TMainForm.BtnHidePanelClick(Sender: TObject);
|
||
begin
|
||
FPanelHidden := not FPanelHidden;
|
||
if FPanelHidden then
|
||
begin
|
||
PanelLeft.Width := 0;
|
||
BtnHidePanel.Caption := '▶';
|
||
FVfoOverlay.Parent := PanelRight;
|
||
FVfoOverlay.Width := 260;
|
||
FVfoOverlay.Height := 130;
|
||
FVfoOverlay.SetState(FMode, FFilterBW, FVfoA, FLastSMeter);
|
||
FVfoOverlay.Visible := True;
|
||
FVfoOverlay.BringToFront;
|
||
PositionVfoOverlay;
|
||
end
|
||
else
|
||
begin
|
||
PanelLeft.Width := 232;
|
||
BtnHidePanel.Caption := '◀';
|
||
FVfoOverlay.Visible := False;
|
||
end;
|
||
ResizeSpectrumPanels;
|
||
ResizeSMeter;
|
||
end;
|
||
|
||
procedure TMainForm.PositionVfoOverlay;
|
||
var
|
||
VfoX, OW, SpTop: Integer;
|
||
begin
|
||
if not Assigned(FVfoOverlay) or not FVfoOverlay.Visible then Exit;
|
||
OW := FVfoOverlay.Width;
|
||
SpTop := PbSpectrum.Top; // смещение PbSpectrum внутри PanelRight
|
||
|
||
if PbSpectrum.Width > 0 then
|
||
VfoX := Round((FVfoA - FCenterFreq + FSpanHz / 2) / FSpanHz * PbSpectrum.Width)
|
||
else
|
||
VfoX := PbSpectrum.Width div 2;
|
||
|
||
// Всегда справа от VFO-линии
|
||
FVfoOverlay.Left := Min(PanelRight.Width - OW - 4, VfoX + 6);
|
||
FVfoOverlay.Top := SpTop + 6;
|
||
end;
|
||
|
||
procedure TMainForm.PbSpectrumDblClick(Sender: TObject);
|
||
begin
|
||
// Двойной клик — ничего не делаем (оверлей всегда виден при скрытой панели)
|
||
end;
|
||
|
||
procedure TMainForm.OnModeFilterSelect(Mode: Integer; FilterBW: Integer);
|
||
begin
|
||
FMode := Mode;
|
||
FFilterBW := FilterBW;
|
||
if FWDSPReady then
|
||
begin
|
||
FDSPEngine.SetMode(FMode);
|
||
ApplyModeFilter;
|
||
end;
|
||
if Assigned(FVfoOverlay) and FVfoOverlay.Visible then
|
||
begin
|
||
FVfoOverlay.SetState(FMode, FFilterBW, FVfoA, FLastSMeter);
|
||
PositionVfoOverlay; // перепозиционируем при смене LSB↔USB
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.BtnSpanClick(Sender: TObject);
|
||
var
|
||
NewRate: Integer;
|
||
DDCRate: Word;
|
||
begin
|
||
NewRate := (Sender as TFlatButton).Tag;
|
||
if NewRate = FSampleRate then Exit;
|
||
|
||
FSampleRate := NewRate;
|
||
FSpanHz := NewRate;
|
||
DDCRate := NewRate div 1000;
|
||
|
||
// Обновляем кнопки сразу — UI отзывчив
|
||
StyleSpanButton(BtnSpan48k, FSampleRate = 48000);
|
||
StyleSpanButton(BtnSpan96k, FSampleRate = 96000);
|
||
StyleSpanButton(BtnSpan192k, FSampleRate = 192000);
|
||
StyleSpanButton(BtnSpan384k, FSampleRate = 384000);
|
||
StyleSpanButton(BtnSpan768k, FSampleRate = 768000);
|
||
StyleSpanButton(BtnSpan1536k, FSampleRate = 1536000);
|
||
Application.ProcessMessages;
|
||
|
||
FBandCache[FCurrentBand].SpanHz := FSpanHz; // для обратной совместимости
|
||
// SampleRate — глобальный: сохраняем в global settings
|
||
if FDevConnected then
|
||
FSettings.SaveGlobal(FDevMAC, MakeGlobalSettings);
|
||
|
||
// 1. Останавливаем WDSP и сбрасываем очередь старых пакетов
|
||
if FWDSPReady then
|
||
begin
|
||
FWDSPReady := False;
|
||
FDSPEngine.ChangeSampleRate(NewRate); // Close + FlushQueue + Open
|
||
end;
|
||
|
||
// 2. Отправляем новый rate трансиверу
|
||
// (после остановки DSP — новые пакеты сразу идут с правильным rate)
|
||
if FNetwork.Connected then
|
||
FNetwork.ConfigureDDCs(1, DDCRate, 0);
|
||
|
||
// 3. Восстанавливаем настройки DSP
|
||
FWDSPReady := FDSPEngine.Initialized;
|
||
if FWDSPReady then
|
||
begin
|
||
FDSPEngine.SetMode(FMode);
|
||
FDSPEngine.SetVolume(FVolume / 100.0);
|
||
ApplyModeFilter;
|
||
ApplyNoiseFilterButtonsToDSP;
|
||
// ChangeSampleRate пересоздаёт WDSP-канал — восстанавливаем AGC
|
||
FDSPEngine.SetAGCTop(FAGCTop);
|
||
FDSPEngine.SetAGC(TWDSPAGCMode(FAGCMode), 50.0);
|
||
FDSPEngine.UpdateAGCLines(FSpectrumWidth);
|
||
if FNetwork.Connected then
|
||
FDSPEngine.SetShift(FVfoA - FCenterFreq);
|
||
end;
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// Веб-интерфейс: callbacks от TWebServer (вызываются из WS-потока)
|
||
// Все изменения состояния выполняем через TThread.Synchronize чтобы
|
||
// не трогать UI и WDSP из чужого потока.
|
||
// ===========================================================================
|
||
|
||
procedure TMainForm.WebOnFreq(Hz: Double);
|
||
begin
|
||
FWebSyncFreq := Hz;
|
||
FWebSyncM := SyncWebFreq;
|
||
TThread.Synchronize(nil, FWebSyncM);
|
||
end;
|
||
|
||
procedure TMainForm.SyncWebFreq;
|
||
begin
|
||
FVfoA := FWebSyncFreq;
|
||
if not FCTun then FCenterFreq := FVfoA;
|
||
UpdateVfoDisplay;
|
||
if FWDSPReady then
|
||
begin
|
||
FDSPEngine.SetShift(FVfoA - FCenterFreq);
|
||
if FRunning then
|
||
FNetwork.SetRunAndFreq(True, FCenterFreq, FCenterFreq, FDriveLevel);
|
||
end;
|
||
end;
|
||
|
||
procedure TMainForm.WebOnMode(Mode: Integer);
|
||
begin
|
||
FWebSyncInt := Mode;
|
||
FWebSyncM := SyncWebMode;
|
||
TThread.Synchronize(nil, FWebSyncM);
|
||
end;
|
||
|
||
procedure TMainForm.SyncWebMode;
|
||
var i: Integer;
|
||
begin
|
||
if (FWebSyncInt < 0) or (FWebSyncInt >= MODE_COUNT) then Exit;
|
||
FMode := FWebSyncInt;
|
||
for i := 0 to MODE_COUNT - 1 do StyleButton(BtnMode[i], i = FMode);
|
||
if FWDSPReady then FDSPEngine.SetMode(FMode);
|
||
UpdateFilterButtons;
|
||
ApplyModeFilter;
|
||
FBandCache[FCurrentBand].Mode := FMode;
|
||
end;
|
||
|
||
procedure TMainForm.WebOnFilter(BW: Integer);
|
||
begin
|
||
FWebSyncInt := BW;
|
||
FWebSyncM := SyncWebFilter;
|
||
TThread.Synchronize(nil, FWebSyncM);
|
||
end;
|
||
|
||
procedure TMainForm.SyncWebFilter;
|
||
var
|
||
best, dist, i, j: Integer;
|
||
BW_arr: array[0..FILT_COUNT-1] of Integer;
|
||
Idx: Integer;
|
||
begin
|
||
// Negative value = -(idx+1) means filter index was passed directly
|
||
if FWebSyncInt < 0 then
|
||
begin
|
||
Idx := (-FWebSyncInt) - 1;
|
||
if (Idx >= 0) and (Idx < FILT_COUNT) then
|
||
begin
|
||
FFilter := Idx;
|
||
case FMode of
|
||
0,1: FFilterBW := FILT_SSB_BW[Idx];
|
||
3,4: FFilterBW := FILT_CW_BW[Idx];
|
||
5: FFilterBW := FILT_FM_BW[Idx];
|
||
else FFilterBW := FILT_AM_BW[Idx];
|
||
end;
|
||
for j := 0 to FILT_COUNT-1 do StyleButton(BtnFilter[j], j = FFilter);
|
||
ApplyModeFilter;
|
||
FBandCache[FCurrentBand].FilterBW := FFilterBW;
|
||
end;
|
||
Exit;
|
||
end;
|
||
// Positive value = bandwidth in Hz, find closest filter
|
||
FFilterBW := FWebSyncInt;
|
||
case FMode of
|
||
0,1: for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_SSB_BW[i];
|
||
3,4: for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_CW_BW[i];
|
||
5: for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_FM_BW[i];
|
||
else for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_AM_BW[i];
|
||
end;
|
||
best := 0; dist := MaxInt;
|
||
for i := 0 to FILT_COUNT-1 do
|
||
if Abs(BW_arr[i] - FWebSyncInt) < dist then
|
||
begin
|
||
dist := Abs(BW_arr[i] - FWebSyncInt);
|
||
best := i;
|
||
end;
|
||
FFilter := best;
|
||
for j := 0 to FILT_COUNT-1 do StyleButton(BtnFilter[j], j = FFilter);
|
||
ApplyModeFilter;
|
||
FBandCache[FCurrentBand].FilterBW := FFilterBW;
|
||
end;
|
||
|
||
procedure TMainForm.WebOnAGC(Mode: Integer);
|
||
begin
|
||
FWebSyncInt := Mode;
|
||
FWebSyncM := SyncWebAGC;
|
||
TThread.Synchronize(nil, FWebSyncM);
|
||
end;
|
||
|
||
procedure TMainForm.SyncWebAGC;
|
||
var i: Integer;
|
||
begin
|
||
if (FWebSyncInt < 0) or (FWebSyncInt > 4) then Exit;
|
||
FAGCMode := FWebSyncInt;
|
||
for i := 0 to 4 do StyleButton(BtnAGCMode[i], i = FAGCMode);
|
||
if FWDSPReady then
|
||
FDSPEngine.SetAGC(TWDSPAGCMode(FAGCMode), 50.0);
|
||
FBandCache[FCurrentBand].AGCMode := FAGCMode;
|
||
end;
|
||
|
||
procedure TMainForm.WebOnAGCTop(DB: Integer);
|
||
begin
|
||
FWebSyncInt := DB;
|
||
FWebSyncM := SyncWebAGCTop;
|
||
TThread.Synchronize(nil, FWebSyncM);
|
||
end;
|
||
|
||
procedure TMainForm.SyncWebAGCTop;
|
||
begin
|
||
FAGCTop := Max(20, Min(120, FWebSyncInt));
|
||
TrkAGC.Position := FAGCTop;
|
||
LblAGCTop.Caption := Format('%ddB', [FAGCTop]);
|
||
if FWDSPReady then
|
||
begin
|
||
FDSPEngine.SetAGCTop(FAGCTop);
|
||
FDSPEngine.SetAGC(TWDSPAGCMode(FAGCMode), 50.0);
|
||
end;
|
||
FBandCache[FCurrentBand].AGCTop := FAGCTop;
|
||
end;
|
||
|
||
procedure TMainForm.WebOnBand(Idx: Integer);
|
||
begin
|
||
FWebSyncInt := Idx;
|
||
FWebSyncM := SyncWebBand;
|
||
TThread.Synchronize(nil, FWebSyncM);
|
||
end;
|
||
|
||
procedure TMainForm.SyncWebBand;
|
||
begin
|
||
if (FWebSyncInt < 0) or (FWebSyncInt >= BAND_COUNT) then Exit;
|
||
if FWebSyncInt = FCurrentBand then Exit;
|
||
if FDevConnected then begin SaveCurrentBand; FSettings.Save; end;
|
||
FCurrentBand := FWebSyncInt;
|
||
RestoreBand(FCurrentBand);
|
||
end;
|
||
|
||
procedure TMainForm.WebOnSpan(Hz: Integer);
|
||
begin
|
||
FWebSyncInt := Hz;
|
||
FWebSyncM := SyncWebSpan;
|
||
TThread.Synchronize(nil, FWebSyncM);
|
||
end;
|
||
|
||
procedure TMainForm.SyncWebSpan;
|
||
var
|
||
ValidSpans: array[0..5] of Integer;
|
||
Found: Boolean;
|
||
i: Integer;
|
||
Btn: TFlatButton;
|
||
begin
|
||
ValidSpans[0]:=48000; ValidSpans[1]:=96000; ValidSpans[2]:=192000;
|
||
ValidSpans[3]:=384000; ValidSpans[4]:=768000; ValidSpans[5]:=1536000;
|
||
Found := False;
|
||
for i := 0 to 5 do
|
||
if ValidSpans[i] = FWebSyncInt then begin Found := True; Break; end;
|
||
if not Found then Exit;
|
||
if FWebSyncInt = FSampleRate then Exit;
|
||
Btn := nil;
|
||
if FWebSyncInt = 48000 then Btn := BtnSpan48k
|
||
else if FWebSyncInt = 96000 then Btn := BtnSpan96k
|
||
else if FWebSyncInt = 192000 then Btn := BtnSpan192k
|
||
else if FWebSyncInt = 384000 then Btn := BtnSpan384k
|
||
else if FWebSyncInt = 768000 then Btn := BtnSpan768k
|
||
else if FWebSyncInt = 1536000 then Btn := BtnSpan1536k;
|
||
if Assigned(Btn) then BtnSpanClick(Btn);
|
||
end;
|
||
|
||
procedure TMainForm.WebOnVolume(V: Integer);
|
||
begin
|
||
FWebSyncInt := V;
|
||
FWebSyncM := SyncWebVolume;
|
||
TThread.Synchronize(nil, FWebSyncM);
|
||
end;
|
||
|
||
procedure TMainForm.SyncWebVolume;
|
||
begin
|
||
FVolume := Max(0, Min(100, FWebSyncInt));
|
||
TrkVolume.Position := FVolume;
|
||
if FWDSPReady then FDSPEngine.SetVolume(FVolume / 100.0);
|
||
end;
|
||
|
||
procedure TMainForm.WebOnWfAGC(On_: Boolean);
|
||
begin
|
||
FWebSyncBool := On_;
|
||
FWebSyncM := SyncWebWfAGC;
|
||
TThread.Synchronize(nil, FWebSyncM);
|
||
end;
|
||
|
||
procedure TMainForm.SyncWebWfAGC;
|
||
begin
|
||
FWfAGCEnabled := FWebSyncBool;
|
||
StyleButton(BtnWfAGC, FWfAGCEnabled);
|
||
end;
|
||
|
||
procedure TMainForm.WebOnWfNF(On_: Boolean);
|
||
begin
|
||
FWebSyncBool := On_;
|
||
FWebSyncM := SyncWebWfNF;
|
||
TThread.Synchronize(nil, FWebSyncM);
|
||
end;
|
||
|
||
procedure TMainForm.SyncWebWfNF;
|
||
begin
|
||
FWfNFEnabled := FWebSyncBool;
|
||
StyleButton(BtnWfNF, FWfNFEnabled);
|
||
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;
|
||
|
||
procedure TMainForm.TrkDriveChange(Sender: TObject);
|
||
begin
|
||
FDriveLevel := Round(TrkDrive.Position / 100.0 * 255);
|
||
if FWDSPReady then
|
||
FDSPEngine.SetDriveLevel(TrkDrive.Position / 100.0);
|
||
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;
|
||
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
|
||
// Храним RAW данные от WDSP (всегда 1024 точки).
|
||
// Интерполяция на ширину экрана делается в DrawSpectrum/DrawWaterfall.
|
||
// Вызывается из DSP-потока — Single-запись атомарна.
|
||
N := Min(Count, 1024);
|
||
for i := 0 to N - 1 do
|
||
FSpectrumBuf[i] := Pixels[i];
|
||
FSpectrumDirty := True;
|
||
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;
|
||
end;
|
||
|
||
|
||
end.
|