Files
ewsdr/MainForm.pas
T
ew8bakandClaude Sonnet 4.6 203d9d0f05 Fix FM step change from web UI; add FM grid to web spectrum/ruler
Web UI: stepSel.onchange now sends fm_step command to server when in FM
mode; option values stay in Hz so tuneStep works unchanged.
WebServer: add fm_step command handler and OnFMStep callback.
MainForm: wire WebOnFMStep -> SyncWebFMStep -> SetFMStep + SaveCurrentBand.

Web spectrum/ruler: vertical FM-step grid lines in drawSpec; FM-aligned
ticks and labels in drawRuler with auto label-density (short ticks between
labeled lines, measureText-based labelMult to prevent overlap).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 22:52:33 +03:00

7002 lines
246 KiB
ObjectPascal
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
unit MainForm;
{
OpenHPSDR Transceiver - Main Form
Lazarus / FPC 3.2+ (нет inline var, нет анонимных процедур)
ИСПРАВЛЕНИЯ:
- (Web NR/NB/ANF fix) BtnNRClick, BtnNBClick, BtnANFClick переписаны
без использования «Sender as TFlatButton» — теперь обращаются напрямую
к BtnNR / BtnNB / BtnANF. Это устраняет EAccessViolation / EInvalidCast
при вызове из SyncWebNR/SyncWebNB/SyncWebANF через TThread.Synchronize,
где Sender передавался как nil. Исключение в UI-потоке приводило к
разрыву WebSocket-соединения и «отключению» веб-клиента.
- SyncWebNR / SyncWebNB / SyncWebANF: передают реальную кнопку (BtnNR /
BtnNB / BtnANF) вместо nil — защита на случай изменения обработчиков.
}
{$IFDEF FPC}
{$MODE Delphi}
{$ENDIF}
interface
uses
Classes, SysUtils, StrUtils, FreqDisplay, DeviceForm, VfoOverlay, FlatButton, FlatSlider, AppTheme, Forms, Controls, Graphics, Dialogs,
StdCtrls, ExtCtrls, ComCtrls, Buttons, Menus, Math, Types,
LCLIntf, LCLType, GraphType,
HPSDRProtocol, HPSDRNetwork,
WDSP, WDSPEngine, AudioOutput, AudioInput,
Settings,
WebServer,
CATEngine, CATSerial, CATTcp,
SpectrumView,
WinFirewall;
const
CLR_BG = TColor($00101010);
CLR_PANEL = TColor($00181818);
CLR_BORDER = TColor($00303030);
CLR_FREQ = TColor($0000FF00);
CLR_FREQ_DIM = TColor($00007800);
CLR_AMBER = TColor($0000AAFF);
CLR_ACTIVE = TColor($000C3010);
CLR_ACTIVE_BDR = TColor($00387838);
CLR_INACTIVE = TColor($001A1A1A);
CLR_TEXT = TColor($00CCCCCC);
CLR_TEXTDIM = TColor($00666666);
CLR_METER_ON = TColor($0000CC44);
CLR_METER_OVR = TColor($000000CC);
CLR_SPECTRUM = TColor($0044FF44);
WDSP_WISDOM_FILE = 'wdspWisdom00';
BAND_COUNT = 11;
MODE_COUNT = 8;
FILT_COUNT = 10;
// Группы фильтров по типу модуляции
// SSB: LSB/USB
FILT_SSB_NAMES: array[0..FILT_COUNT-1] of string =
('5.0k','4.4k','3.8k','3.3k','2.9k','2.7k','2.4k','2.1k','1.8k','1.0k');
FILT_SSB_BW: array[0..FILT_COUNT-1] of Integer =
(5000, 4400, 3800, 3300, 2900, 2700, 2400, 2100, 1800, 1000);
FILT_SSB_DEF = 5; // 2.7k
// CW: CWL/CWU
FILT_CW_NAMES: array[0..FILT_COUNT-1] of string =
('1.0k','800','600','500','400','250','150','100','50','25');
FILT_CW_BW: array[0..FILT_COUNT-1] of Integer =
(1000, 800, 600, 500, 400, 250, 150, 100, 50, 25);
FILT_CW_DEF = 3; // 500
// DSB
FILT_DSB_NAMES: array[0..FILT_COUNT-1] of string =
('16k','12k','10k','8.0k','6.6k','5.2k','4.0k','3.1k','2.9k','2.4k');
FILT_DSB_BW: array[0..FILT_COUNT-1] of Integer =
(16000, 12000, 10000, 8000, 6600, 5200, 4000, 3100, 2900, 2400);
FILT_DSB_DEF = 3; // 8.0k
// AM/SAM
FILT_AM_NAMES: array[0..FILT_COUNT-1] of string =
('20k','18k','16k','12k','10k','9.0k','8.0k','7.0k','6.0k','5.0k');
FILT_AM_BW: array[0..FILT_COUNT-1] of Integer =
(20000, 18000, 16000, 12000, 10000, 9000, 8000, 7000, 6000, 5000);
FILT_AM_DEF = 4; // 10k
// FM: NFM (2.5 kHz deviation / 11 kHz BW) and FM (5.0 kHz deviation / 16 kHz BW)
FILT_FM_COUNT = 2;
FILT_FM_NAMES: array[0..1] of string = ('NFM', 'FM');
FILT_FM_BW: array[0..1] of Integer = (11000, 16000);
FILT_FM_DEV: array[0..1] of Double = (2500.0, 5000.0);
FILT_FM_DEF = 0; // NFM
// CTCSS tones (38 standard tones, no None — toggle via CTCSS button)
CTCSS_COUNT = 38;
CTCSS_TONES: array[0..37] of Double = (
67.0, 71.9, 74.4, 77.0, 79.7, 82.5, 85.4, 88.5, 91.5, 94.8,
97.4, 100.0, 103.5, 107.2, 110.9, 114.8, 118.8, 123.0, 127.3, 131.8,
136.5, 141.3, 146.2, 151.4, 156.7, 162.2, 167.9, 173.8, 179.9, 186.2,
192.8, 203.5, 210.7, 218.1, 225.7, 233.6, 241.8, 250.3);
CTCSS_NAMES: array[0..37] of string = (
'67.0', '71.9', '74.4', '77.0', '79.7', '82.5', '85.4', '88.5',
'91.5', '94.8', '97.4', '100.0', '103.5', '107.2', '110.9', '114.8',
'118.8', '123.0', '127.3', '131.8', '136.5', '141.3', '146.2', '151.4',
'156.7', '162.2', '167.9', '173.8', '179.9', '186.2', '192.8', '203.5',
'210.7', '218.1', '225.7', '233.6', '241.8', '250.3');
// FM Step tuning
FM_STEP_COUNT = 4;
FM_STEP_HZ: array[0..3] of Integer = (6250, 12500, 20000, 25000);
FM_STEP_NAMES: array[0..3] of string = ('6.25k', '12.5k', '20k', '25k');
FM_STEP_DEF = 3; // 25 kHz
BAND_NAMES: array[0..BAND_COUNT-1] of string = (
'160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m');
BAND_FREQ: array[0..BAND_COUNT-1] of Double = (
1900000, 3750000, 5357000, 7100000, 10125000,
14200000, 18120000, 21200000, 24940000, 28500000, 50150000);
MODE_NAMES: array[0..MODE_COUNT-1] of string = (
'LSB','USB','DSB','CWL','CWU','FM','AM','SAM');
SMETER_WIDTH_RATIO = 0.32;
SMETER_MARGIN = 3;
TOP_SMETER_GAP = 8;
TOP_VFO_FONT = 25;
type
TDeviceItem = record
Dev: THPSDRDevice;
Display: string;
end;
// Синхронизирующий объект для OnDeviceFound
TDeviceFoundSync = class
private
FForm: TObject; // TMainForm, через forward ref
FDev: THPSDRDevice;
FEntry: string;
public
constructor Create(AForm: TObject; const D: THPSDRDevice; const E: string);
procedure Execute;
end;
// Синхронизирующий объект для HP Status
TStatusUISync = class
private
FForm: TObject;
FFwdW: Double;
FSWRV: Double;
FSupplyV: Double;
FPLLLock: Boolean;
FHWPTT: Boolean;
public
constructor Create(AForm: TObject; FW, SW, SV: Double; PLL, HWPTT: Boolean);
procedure Execute;
end;
// Синхронизирующий объект для DDC IQ
TDDCSeqSync = class
private
FForm: TObject;
FDDCIdx: Integer;
FSeq: LongWord;
public
constructor Create(AForm: TObject; Idx: Integer; Seq: LongWord);
procedure Execute;
end;
{ TMainForm }
TMainForm = class(TForm)
private
// ---- Network ----
FNetwork: THPSDRNetwork;
FDevices: array of TDeviceItem;
FDeviceCount: Integer;
FDeviceDialog: TDeviceDialog;
FPendingIP: string; // IP устройства для подключения
FVfoOverlay: TVfoOverlay; // накладка SmartSDR-стиль на спектре
FPanelHidden: Boolean; // True = левая панель скрыта
FPendingBoardType: Integer; // BoardType устройства для подключения
// ---- DSP + Audio ----
FDSPEngine: TWDSPEngine;
FAudioOut: TAudioOutput;
FAudioIn: TAudioInput;
FAudioOutDevName: string; // текущее имя выходного устройства
FAudioInDevName: string; // текущее имя входного устройства (TX mic)
FTXSettings: TTXSettings; // полный TX-конфиг (загружается per-MAC)
FAlexSettings: TAlexSettings; // per-band antenna/routing config (Alex board)
FXvtrSettings: TXvtrSettings; // transverter slots (per-device)
FCurrentXvtr: Integer; // -1 = HF, 0..CFG_XVTR_COUNT-1 = active XVTR
FWDSPReady: Boolean; // True когда WDSP открыт и работает
// ---- State ----
FVfoA: Double;
FVfoB: Double;
FActiveVfo: Integer;
FMode: Integer;
FFilter: Integer;
FAGCMode: Integer; // 0=FAST 1=MED 2=SLOW 3=LONG 4=OFF
FAGCTop: Integer; // AGC level dBm, 20..120
FCTun: Boolean; // Center Tune: True=спектр стоит, маркер двигается
FFilterBW: Integer; // текущая полоса фильтра в Гц
// мышь на спектре/водопаде
FSpecDrag: Boolean;
FSpecDragX0: Integer; // X при нажатии
FSpecDragFreq: Double; // FCenterFreq при нажатии
FSpectrumDirty: Boolean; // таймер должен перерисовать спектр/водопад
FDriveLevel: Byte;
FPAMaxPower: Double;
FPABandCal: array[0..BAND_COUNT-1] of Double; // калибровка 38.8..100.0 на диапазон
FRunning: Boolean;
FTransmitting: Boolean;
FSplitTxB: Boolean;
// DUC IQ packetizer: TXA отдаёт блок FTXOutBufSize пар, который
// не обязан быть кратен 240 (DUC packet). Хвост переносим в следующий
// вызов OnTXIQReady, иначе зануление давало 47 Гц гул.
FDUCPendingI: array[0..239] of Integer;
FDUCPendingQ: array[0..239] of Integer;
FDUCPendingCount: Integer;
// DUP: при TX продолжаем показывать RX1-водопад/панораму, а TX-фильтр
// и TX-линии рисуем поверх. Аппаратной командой не является.
FDisplayDuplex: Boolean;
// TUN: режим тоновой подстройки. WDSP TXA PostGen генерирует чистый
// тон, Drive подменяется на FTXSettings.TUNLevel (через Cal[band]).
FTuning: Boolean;
FHWPTTActive: Boolean; // True = hardware PTT нажата
FMuted: Boolean;
FVolume: Integer;
FAtten: Integer; // RX step attenuator index: 0=0dB 1=10dB 2=20dB
FLastSMeter: Double;
FSMeterPeak: Double; // верхняя граница светлой зоны
FSMeterMin: Double; // нижняя граница светлой зоны
FSMeterAvg: Double; // сглаженное среднее (EMA)
FRXPacketCount: LongWord; // счётчик принятых IQ пакетов
FRXStartTime: QWord; // GetTickCount64 момента нажатия START
FRXLastPktTime: QWord; // GetTickCount64 последнего принятого пакета
FActiveDDC: Integer; // DDC index для текущей платы (0 или 2)
FLastDDCSeq: LongWord; // последний seq (пишется из сетевого потока)
FLastDDCIndex: Integer; // последний DDC index
FLastFwdW: Double;
FLastSWR: Double;
// Эти значения приходят с HP Status пакетами (50..200 раз/с). UI обновляется
// из MeterTimerTick (10 Гц), чтобы цифры/полоски не дёргались.
FLastSupplyV: Double;
FLastPLLLock: Boolean;
// ---- Spectrum / Waterfall view ----
FSpecView: TSpectrumView;
FSpectrumWidth: Integer; // актуальная ширина для FDSPEngine и resize-детектора
FSpectrumHeight: Integer;
FWaterfallHeight:Integer;
FShowSpectrum: Boolean;
FShowWaterfall: Boolean;
FDisplayFPS: Integer;
FWaterfallDirty: Boolean;
// Waterfall settings (kept here for MakeGlobalSettings, buttons, settings dialog)
FWfAGCEnabled: Boolean;
FWfNFEnabled: Boolean;
FDitherEnabled: Boolean;
FRandomEnabled: Boolean;
FFMDeviation: Double;
FFMCTCSSOn: Boolean;
FFMCTCSSToneIdx: Integer;
FFMSQOn: Boolean;
FFMSQLevel: Integer;
FFMStepOn: Boolean;
FFMStepIdx: Integer;
FWfManualHigh: Double;
FWfManualLow: Double;
FWfAGCOffset: Double;
// Spectrum display settings (for settings save/load)
FSpecRefLevel: Double;
FSpecRange: Double;
FSpecGridStep: Double;
// TX-specific grid (Thetis-style: при TX рисуем спектр в своих границах).
// Применяются к FSpecView когда FTransmitting=True; иначе используются RX-значения.
FTXSpecRefLevel: Double;
FTXSpecRange: Double;
FTXSpecGridStep: Double;
// DSP data buffers (copy of last DSP output, for WebServer.PushSpectrum)
FSpectrumBuf: array[0..1023] of Single;
FWaterfallBuf: array[0..1023] of Single;
FSpectrumBufCount: Integer;
FWaterfallBufCount:Integer;
// Waterfall frame timing
FWaterfallFrameInterval: Integer;
FWaterfallFrameCounter: Integer;
// ---- Splitter ----
FSplitterDrag: Boolean;
FSplitterDragY0: Integer;
FSplitterSH0: Integer;
FSplitterRatio: Double;
// --- Settings ---
FSettings: TSettingsManager;
FWebServer: TWebServer;
FWebEnabled: Boolean;
FWebPort: Integer;
FWebBindAddr: string;
FWebUser: string;
FWebPass: string;
FSavedMicSource: TTXMicSource; // сохраняется при переключении на txmsWeb
FWebMicActive: Boolean; // True если текущий TX идёт через txmsWeb
// --- CAT ---
FCATEngine: TCATEngine;
FCATSerial: TCATSerialManager;
FCATTcp: TCATTcpServer;
FCATSyncFreq: Double;
FCATLastGlobal: TGlobalSettings; // текущие CAT-настройки (для сохранения)
// Временные поля для передачи параметров в Synchronize-методы
FWebSyncFreq: Double;
FWebSyncInt: Integer;
FWebSyncBool: Boolean;
FWebSyncM: TThreadMethod;
FCurrentBand: Integer; // текущий активный диапазон 0..10
FSettingsForm: TObject; // TSettingsForm (cast при использовании)
FDevMAC: array[0..5] of Byte; // MAC подключённого трансивера
FPendingDev: THPSDRDevice; // устройство ожидающее открытия WDSP
FBandCache: array[0..CFG_BAND_COUNT-1] of TBandSettings; // кэш диапазонов
FDevConnected: Boolean; // True после первого подключения
FLightTheme: Boolean; // True = светлая тема
FFreqMhzDigits: Integer; // 3=999MHz, 4=9.999GHz, 5=99.999GHz
FCenterFreq: Double; // центр спектра = LO (DDC). При CTUN ON не меняется при кручении VFO
FSpanHz: Double;
FSampleRate: Integer; // текущий DDC sample rate (Hz)
// ---- Timers ----
FMeterTimer: TTimer;
FSpectrumTimer: TTimer;
FAfterShowTimer:TTimer; // однократный таймер для пост-инициализации
// ---- Toolbar ----
PanelToolbar: TPanel;
BtnDiscover: TFlatButton;
BtnStartStop: TFlatButton; // START / STOP (connect+run в одном)
BtnSettings: TFlatButton; // открыть диалог настроек
PanelSep: TPanel; // 1px разделитель под тулбаром
// ---- Left panel ----
PanelLeft: TPanel;
PanelVfoA: TPanel;
LblVfoALabel: TLabel;
FreqDispA: TFreqDisplay;
BtnTopVfoASelect: TFlatButton;
BtnTopVfoATX: TFlatButton;
PanelVfoB: TPanel;
LblVfoBLabel: TLabel;
FreqDispB: TFreqDisplay;
BtnTopVfoBSelect: TFlatButton;
BtnTopVfoBTX: TFlatButton;
PanelVfoButtons: TPanel;
BtnVfoSwap: TFlatButton;
BtnVfoACopyB: TFlatButton;
BtnVfoBCopyA: TFlatButton;
PanelTopVfoGroup: TPanel;
PanelTopVfoA: TPanel;
PanelTopVfoB: TPanel;
PanelBands: TPanel;
BtnBand: array[0..BAND_COUNT-1] of TFlatButton;
// Динамические кнопки XVTR (создаются/удаляются по мере того,
// как пользователь enabled/disabled слоты в Settings → Transverter).
BtnXvtrBand: array[0..CFG_XVTR_COUNT-1] of TFlatButton;
PanelMode: TPanel;
BtnMode: array[0..MODE_COUNT-1] of TFlatButton;
PanelFilter: TPanel;
BtnFilter: array[0..FILT_COUNT-1] of TFlatButton;
BtnCTun: TFlatButton;
BtnDUP: TFlatButton;
PanelFMSQ: TPanel;
BtnFMSQ: TFlatButton;
TrkFMSQ: TFlatSlider;
LblFMSQ: TLabel;
PanelFMCTCSS: TPanel;
BtnFMCTCSS: TFlatButton;
BtnFMCTCSSTone: TFlatButton;
PanelFMCTCSSPop: TPanel;
FBtnCTCSSTones: array[0..CTCSS_COUNT - 1] of TFlatButton;
PanelFMStep: TPanel;
BtnFMStep: TFlatButton;
BtnFMStepSel: TFlatButton;
PanelFMStepPop: TPanel;
FBtnFMSteps: array[0..FM_STEP_COUNT - 1] of TFlatButton;
PanelRX: TPanel;
BtnAGCMode: array[0..4] of TFlatButton; // FAST MED SLOW LONG OFF
LblAGCTop: TLabel; // показывает значение уровня
TrkAGC: TFlatSlider;
TrkVolume: TFlatSlider;
BtnNR: TFlatButton;
BtnNB: TFlatButton;
BtnSNB: TFlatButton;
BtnANF: TFlatButton;
BtnMute: TFlatButton;
PanelSMeterRight: TPanel;
PbSMeterRight: TPaintBox;
PanelTX: TPanel;
LblDrv: TLabel;
TrkDrive: TFlatSlider;
BtnMOX: TFlatButton;
BtnTUN: TFlatButton;
// ---- Right panel ----
PanelRight: TPanel;
PanelSpanButtons: TPanel;
LblSpan: TLabel;
BtnSpan48k: TFlatButton;
BtnSpan96k: TFlatButton;
BtnSpan192k: TFlatButton;
BtnSpan384k: TFlatButton;
BtnSpan768k: TFlatButton;
BtnSpan1536k: TFlatButton;
BtnHidePanel: TFlatButton; // скрыть/показать левую панель
PbSpectrum: TPaintBox;
PbRuler: TPaintBox; // полоса частотных меток между спектром и водопадом
PanelSplitter: TPanel; // перетаскиваемый разделитель спектр/водопад
PbWaterfall: TPaintBox;
// ---- Status bar ----
StatusBar1: TStatusBar;
// ---- Helpers ----
procedure BuildUI;
procedure ApplyDarkTheme;
procedure SetLightTheme(V: Boolean);
procedure StyleButton(B: TFlatButton; Active: Boolean = False);
procedure StyleSpanButton(B: TFlatButton; Active: Boolean = False);
procedure PositionSpanOverlayButtons;
procedure UpdateVfoDisplay;
function FormatFreq(Hz: Double): string;
procedure LayoutTopVfoBlock;
procedure ResizeSMeter;
procedure ResizeSpectrumPanels;
procedure InvalidateGridCache;
procedure SyncSpecViewFreq;
procedure RecreateDSPEngine(ASampleRate: Integer);
function EnsureWDSPWisdom: Boolean;
// Network callbacks
procedure OnDeviceFound(const Dev: THPSDRDevice);
procedure OnHPStatusCB(const Status: THighPriorityStatus);
procedure OnDDCIQCB(DDCIndex: Integer; const Data: TDDCIQPacket);
procedure OnWDSPOpenDone(Success: Boolean);
procedure DoConnectDevice(const Dev: THPSDRDevice);
procedure OnMicPacketCB(const Data: TMicDataPacket);
// DSP/Audio callbacks (вызываются из рабочих потоков)
procedure OnAudioReady(const Left, Right: array of Single; Count: Integer);
procedure OnSpectrumReady(const Pixels: array of Single; Count: Integer);
procedure OnWaterfallReady(const Pixels: array of Single; Count: Integer);
// Public UI update (called from sync objects)
procedure DoAddDevice(const Dev: THPSDRDevice; const Entry: string);
procedure DoUpdateStatus(FwdW, SWRV, SupplyV: Double; PLLLock, HWPTT: Boolean);
procedure UpdateTXMeters;
procedure DoUpdateDDCSeq(DDCIdx: Integer; Seq: LongWord);
procedure DoUpdateDDCSeqOrNoDevice(DDCIdx: Integer; Seq: LongWord);
// Event handlers
procedure BtnDiscoverClick(Sender: TObject);
procedure BtnDiscoverFromDialog(Sender: TObject);
procedure BtnStartStopClick(Sender: TObject);
procedure FreqDispAChanged(Sender: TObject; NewFreq: Int64);
procedure ApplyVfoA(NewFreq: Int64);
procedure FreqDispAClick(Sender: TObject);
procedure FreqDispBClick(Sender: TObject);
procedure BtnTopVfoASelectClick(Sender: TObject);
procedure BtnTopVfoATXClick(Sender: TObject);
procedure BtnTopVfoBSelectClick(Sender: TObject);
procedure BtnTopVfoBTXClick(Sender: TObject);
procedure ActivateVfo(Idx: Integer);
function ActiveVfoFreq: Int64;
// Настройки
procedure SaveCurrentBand;
procedure RestoreBand(BandIdx: Integer);
procedure SaveAllAndExit;
procedure RestoreWindowBounds;
function MakeGlobalSettings: TGlobalSettings;
function MakeBandSettings: TBandSettings;
procedure FreqDispBChanged(Sender: TObject; NewFreq: Int64);
procedure BtnBandClick(Sender: TObject);
procedure ApplyModeFilter;
procedure BtnModeClick(Sender: TObject);
procedure BtnFilterClick(Sender: TObject);
procedure BtnCTunClick(Sender: TObject);
procedure BtnDUPClick(Sender: TObject);
procedure ApplyDUP(Active: Boolean);
procedure BtnTUNClick(Sender: TObject);
procedure ApplyTUN(Active: Boolean);
procedure UpdateFilterButtons;
procedure BtnFMSQClick(Sender: TObject);
procedure TrkFMSQChange(Sender: TObject);
procedure ApplyFMSquelch;
procedure BtnFMCTCSSClick(Sender: TObject);
procedure BtnFMCTCSSToneClick(Sender: TObject);
procedure BtnFMToneSelectClick(Sender: TObject);
procedure SetFMCTCSSTone(Idx: Integer);
procedure CloseCTCSSPopup;
procedure SetFMStep(Idx: Integer);
procedure CloseFMStepPopup;
procedure BtnFMStepClick(Sender: TObject);
procedure BtnFMStepSelClick(Sender: TObject);
procedure BtnFMStepSelectClick(Sender: TObject);
procedure PbSpectrumMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure PbSpectrumMouseMove(Sender: TObject; Shift: TShiftState;
X, Y: Integer);
procedure PbSpectrumMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure PbWaterfallMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure PbWaterfallMouseMove(Sender: TObject; Shift: TShiftState;
X, Y: Integer);
procedure PbWaterfallMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure DoSpectrumClick(PixelX: Integer; PanelWidth: Integer);
procedure DoSpectrumDrag(PixelX: Integer; PanelWidth: Integer);
procedure BtnVfoSwapClick(Sender: TObject);
procedure BtnVfoACopyBClick(Sender: TObject);
procedure BtnVfoBCopyAClick(Sender: TObject);
procedure BtnMOXClick(Sender: TObject);
procedure BtnMuteClick(Sender: TObject);
procedure UpdateNRButton;
procedure UpdateNBButton;
procedure UpdateSNBButton;
procedure UpdateANFButton;
procedure ApplyNoiseFilterButtonsToDSP;
procedure BtnNRClick(Sender: TObject);
procedure BtnNBClick(Sender: TObject);
procedure BtnSNBClick(Sender: TObject);
procedure BtnANFClick(Sender: TObject);
procedure BtnSpanClick(Sender: TObject);
// Веб-интерфейс: callbacks от TWebServer
procedure WebOnFreq(Hz: Double);
procedure WebOnMode(Mode: Integer);
procedure WebOnFilter(BW: Integer);
procedure WebOnAGC(Mode: Integer);
procedure WebOnAGCTop(DB: Integer);
procedure WebOnBand(Idx: Integer);
procedure WebOnSpan(Hz: Integer);
procedure WebOnVolume(V: Integer);
procedure WebOnWfAGC(On_: Boolean);
procedure WebOnWfNF(On_: Boolean);
procedure WebOnRun(On_: Boolean);
procedure WebOnMute(On_: Boolean);
procedure WebOnCtun(On_: Boolean);
procedure WebOnNR(Mode: Integer);
procedure WebOnNB(Mode: Integer);
procedure WebOnSNB(On_: Boolean);
procedure WebOnANF(On_: Boolean);
procedure WebOnFreqB(Hz: Double);
procedure WebOnActiveVfo(Idx: Integer);
procedure WebOnCenter(Hz: Double);
procedure WebOnMOX(On_: Boolean);
procedure WebOnDrive(V: Integer);
procedure WebOnFreqA(Hz: Double);
procedure WebOnAttn(Idx: Integer);
procedure WebOnTun(On_: Boolean);
procedure WebOnFMStep(Idx: Integer);
procedure WebOnMic(Samples: PSingle; Count: Integer);
procedure WebOnXvtrBand(Idx: Integer);
// Synchronize-обёртки (выполняются в UI-потоке)
procedure SyncWebFreq;
procedure SyncWebMode;
procedure SyncWebFilter;
procedure SyncWebAGC;
procedure SyncWebAGCTop;
procedure SyncWebBand;
procedure SyncWebXvtrBand;
procedure SyncWebSpan;
procedure SyncWebVolume;
procedure SyncWebWfAGC;
procedure SyncWebWfNF;
procedure SyncWebRun;
procedure SyncWebMute;
procedure SyncWebCtun;
procedure SyncWebNR;
procedure SyncWebNB;
procedure SyncWebSNB;
procedure SyncWebANF;
procedure SyncWebFreqB;
procedure SyncWebActiveVfo;
procedure SyncWebCenter;
procedure SyncWebMOX;
procedure SyncWebDrive;
procedure SyncWebFreqA;
procedure SyncWebAttn;
procedure SyncWebTun;
procedure SyncWebFMStep;
// --- CAT callbacks -------------------------------------------------------
function CATGetVfoA: Double;
function CATGetVfoB: Double;
function CATGetMode: Integer;
function CATGetActiveVfo: Integer;
function CATGetAGCMode: Integer;
function CATGetVolume: Integer;
function CATGetDriveLevel: Integer;
function CATGetFilterIdx: Integer;
function CATGetFilterBW: Integer;
function CATGetNRMode: Integer;
function CATGetNBMode: Integer;
function CATGetSNB: Boolean;
function CATGetANF: Boolean;
function CATGetTX: Boolean;
function CATGetRunning: Boolean;
function CATGetSMeter: Double;
function CATGetBand: Integer;
procedure CATSetVfoA(V: Double);
procedure CATSetVfoB(V: Double);
procedure CATSetFilterIdx(V: Integer);
procedure CATDoBandUp;
procedure CATDoBandDown;
procedure CATDoTuneUp;
procedure CATDoTuneDown;
procedure SyncCATVfoA;
procedure SyncCATVfoB;
procedure SyncCATBandUp;
procedure SyncCATBandDown;
procedure SyncCATTuneUp;
procedure SyncCATTuneDown;
procedure InitCATEngine;
procedure CATApplySettings(const G: TGlobalSettings);
procedure OnCATSettingsChange(
const SerEnabled: array of Boolean;
const SerPort: array of string;
const SerBaud, SerDataBits, SerStopBits, SerParity: array of Integer;
TcpEnabled: Boolean; TcpPort: Integer);
procedure OnAlexSettingsChange(const A: TAlexSettings);
procedure OnXvtrSettingsChange(const X: TXvtrSettings);
procedure PushXvtrToWeb;
// XVTR helpers
function FindXvtrIdxForFreq(FreqHz: Double): Integer;
function XvtrTranslate(VisibleHz: Double): Double;
procedure ApplyXvtrToNetwork;
procedure RebuildXvtrButtons;
procedure RelayoutBelowBands;
procedure BtnXvtrBandClick(Sender: TObject);
procedure ActivateXvtrBand(Idx: Integer);
procedure DeactivateXvtr;
// -------------------------------------------------------------------------
procedure BtnHidePanelClick(Sender: TObject);
procedure BtnSettingsClick(Sender: TObject);
procedure PositionVfoOverlay;
procedure OnModeFilterSelect(Mode: Integer; FilterBW: Integer);
procedure PbSpectrumDblClick(Sender: TObject);
procedure TrkDriveChange(Sender: TObject);
procedure TrkVolumeChange(Sender: TObject);
function CalcDriveByte: Byte;
function ActiveTXFreqHz: Double;
procedure ApplyMOX(Active: Boolean);
procedure OnTXIQReady(const Buf: array of Double; Count: Integer);
procedure OnPASettingsChange(MaxPower: Double; const BandCal: array of Double);
procedure OnTXSettingsChange(const T: TTXSettings);
procedure BtnAGCModeClick(Sender: TObject);
procedure TrkAGCChange(Sender: TObject);
procedure MeterTimerTick(Sender: TObject);
procedure SpectrumTimerTick(Sender: TObject);
procedure RightPanelResize(Sender: TObject);
procedure AfterShowTick(Sender: TObject);
// Splitter handlers
procedure SplitterMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure SplitterMouseMove(Sender: TObject; Shift: TShiftState;
X, Y: Integer);
procedure SplitterMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
public
// Методы для SettingsForm — немедленное применение настроек
procedure ApplyDisplayParams(FFTSize, WinType, SpecDet, SpecAvgMode: Integer;
SpecAvgTimeMS: Double);
procedure ApplyWaterfallParams(WfDet, WfAvgMode: Integer;
WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double);
procedure ApplyWfAGCNF(WfAGC, WfNF: Boolean);
procedure ApplyADCSettings(Dither, Random: Boolean);
procedure ApplyWebSettings(Enabled: Boolean; Port: Integer;
const BindAddr, User, Pass: string);
procedure ApplyGridParams(RefLevel, Range, GridStep: Double);
// Пушит активные grid-параметры (RX или TX в зависимости от FTransmitting)
// в FSpecView и сбрасывает кэш сетки. Вызывается при смене RX↔TX и при
// правке любого из соответствующих полей (FSpec*, FTXSpec*).
procedure ApplySpecViewGridFromState;
procedure ApplyAudioDevice(DevIndex: Integer; const DevName: string);
procedure ApplyAudioInputDevice(DevIndex: Integer; const DevName: string);
// TX settings: применить FTXSettings к WDSP и переслать DUC Specific
// (mic-биты Boost/Bias/LineIn/PTT попадают в byte 50 DUCSpecific).
procedure ApplyTXSettingsToDSP;
procedure SendDUCSpecificFromSettings;
function PullSoundCardMic(MaxN: Integer): Integer;
function BuildMicLineSelectByte: Byte;
procedure ApplyVisibility(ShowSpectrum, ShowWaterfall: Boolean);
procedure ApplyFPS(FPS: Integer);
procedure ApplyFreqMhzDigits(Digits: Integer);
published
// Обработчики событий формы — должны быть в published для RTTI/LFM
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure FormClose(Sender: TObject; var CloseAction: TCloseAction);
procedure FormMouseWheel(Sender: TObject; Shift: TShiftState;
WheelDelta: Integer; MousePos: TPoint;
var Handled: Boolean);
end;
TWisdomBuildThread = class(TThread)
private
FDirectory: string;
FError: string;
protected
procedure Execute; override;
public
constructor Create(const ADirectory: string);
property ErrorText: string read FError;
end;
TWisdomProgressDialog = class(TForm)
private
FInfoLabel: TLabel;
FProgress: TProgressBar;
FTimer: TTimer;
FThread: TWisdomBuildThread;
FPhase: Integer;
procedure TimerTick(Sender: TObject);
public
constructor Create(AOwner: TComponent; AThread: TWisdomBuildThread); reintroduce;
end;
var
MainForm: TMainForm;
implementation
uses SettingsForm;
{$R *.lfm}
constructor TWisdomBuildThread.Create(const ADirectory: string);
begin
inherited Create(False);
FreeOnTerminate := False;
FDirectory := ADirectory;
FError := '';
end;
procedure TWisdomBuildThread.Execute;
var
DirA: AnsiString;
begin
try
if not Assigned(@WDSPwisdom) then Exit;
DirA := AnsiString(FDirectory);
WDSPwisdom(PAnsiChar(DirA));
except
on E: Exception do
FError := E.ClassName + ': ' + E.Message;
end;
end;
constructor TWisdomProgressDialog.Create(AOwner: TComponent;
AThread: TWisdomBuildThread);
begin
inherited CreateNew(AOwner);
FThread := AThread;
FPhase := 0;
Caption := 'WDSP Wisdom';
Width := 520;
Height := 140;
Position := poScreenCenter;
BorderStyle := bsDialog;
BorderIcons := [];
Color := CLR_BG;
Font.Name := 'Courier New';
Font.Size := 9;
Font.Color := CLR_TEXT;
FInfoLabel := TLabel.Create(Self);
FInfoLabel.Parent := Self;
FInfoLabel.SetBounds(16, 16, 480, 40);
FInfoLabel.AutoSize := False;
FInfoLabel.WordWrap := True;
FInfoLabel.Font.Color := CLR_TEXT;
FInfoLabel.Caption :=
'Creating FFTW wisdom for WDSP. This is done once and may take a while on the first run.';
FProgress := TProgressBar.Create(Self);
FProgress.Parent := Self;
FProgress.SetBounds(16, 72, 480, 22);
FProgress.Min := 0;
FProgress.Max := 100;
FProgress.Position := 0;
FTimer := TTimer.Create(Self);
FTimer.Interval := 250;
FTimer.OnTimer := TimerTick;
FTimer.Enabled := True;
end;
procedure TWisdomProgressDialog.TimerTick(Sender: TObject);
var
P: PAnsiChar;
S: string;
begin
FPhase := (FPhase + 7) mod 101;
FProgress.Position := FPhase;
if Assigned(@wisdom_get_status) then
begin
P := wisdom_get_status;
if P <> nil then
begin
S := Trim(string(AnsiString(P)));
if S <> '' then
FInfoLabel.Caption := S;
end;
end;
if Assigned(FThread) and FThread.Finished then
begin
FTimer.Enabled := False;
ModalResult := mrOk;
end;
end;
// ===========================================================================
// Общая функция декодирования типа платы — используется везде
// ===========================================================================
function BoardTypeName(BoardType: Integer): string;
begin
case BoardType of
1: Result := 'HERMES (ANAN-10/100)';
2: Result := 'HERMES-E (ANAN-10E/100B)';
3: Result := 'ANGELIA (ANAN-100D)';
4: Result := 'ORION (ANAN-200D)';
5: Result := 'ORION MkII (ANAN-7000/8000)';
6: Result := 'HERMES-LITE 2';
10: Result := 'SATURN (G2)';
else Result := Format('Unknown Board #%d', [BoardType]);
end;
end;
// ===========================================================================
// Sync helpers
// ===========================================================================
constructor TDeviceFoundSync.Create(AForm: TObject;
const D: THPSDRDevice; const E: string);
begin
inherited Create;
FForm := AForm;
FDev := D;
FEntry := E;
end;
procedure TDeviceFoundSync.Execute;
begin
TMainForm(FForm).DoAddDevice(FDev, FEntry);
end;
constructor TStatusUISync.Create(AForm: TObject;
FW, SW, SV: Double; PLL, HWPTT: Boolean);
begin
inherited Create;
FForm := AForm;
FFwdW := FW;
FSWRV := SW;
FSupplyV := SV;
FPLLLock := PLL;
FHWPTT := HWPTT;
end;
procedure TStatusUISync.Execute;
begin
TMainForm(FForm).DoUpdateStatus(FFwdW, FSWRV, FSupplyV, FPLLLock, FHWPTT);
end;
constructor TDDCSeqSync.Create(AForm: TObject; Idx: Integer; Seq: LongWord);
begin
inherited Create;
FForm := AForm;
FDDCIdx := Idx;
FSeq := Seq;
end;
procedure TDDCSeqSync.Execute;
begin
TMainForm(FForm).DoUpdateDDCSeqOrNoDevice(FDDCIdx, FSeq);
end;
// ===========================================================================
// FormCreate / FormDestroy
// ===========================================================================
// ===========================================================================
// Settings helpers
// ===========================================================================
procedure TMainForm.RestoreWindowBounds;
var
L, T, Wd, Ht: Integer;
begin
FSettings.LoadWindowBounds(L, T, Wd, Ht);
if (Wd > 400) and (Ht > 300) then
begin
Left := L;
Top := T;
Width := Wd;
Height := Ht;
end;
end;
function TMainForm.MakeBandSettings: TBandSettings;
begin
Result.VfoA := FVfoA;
Result.VfoB := FVfoB;
Result.Mode := FMode;
Result.FilterIdx := FFilter;
Result.FilterBW := FFilterBW;
Result.AGCMode := FAGCMode;
Result.AGCTop := FAGCTop;
Result.CTun := FCTun;
Result.SpanHz := FSpanHz;
Result.FMSQOn := FFMSQOn;
Result.FMSQLevel := FFMSQLevel;
Result.CTCSSOn := FFMCTCSSOn;
Result.CTCSSToneIdx := FFMCTCSSToneIdx;
Result.FMStepOn := FFMStepOn;
Result.FMStepIdx := FFMStepIdx;
// Waterfall AGC/NF — глобальные (не диапазонные)
end;
function TMainForm.MakeGlobalSettings: TGlobalSettings;
var i: Integer;
begin
Result.Volume := FVolume;
Result.DriveLevel := TrkDrive.Position;
Result.PAMaxPower := FPAMaxPower;
for i := 0 to BAND_COUNT - 1 do
Result.PABandCal[i] := FPABandCal[i];
Result.ActiveVfo := FActiveVfo;
Result.NRMode := BtnNR.Tag;
Result.NBMode := BtnNB.Tag;
Result.SNBEnabled := BtnSNB.Tag <> 0;
Result.ANFEnabled := BtnANF.Tag <> 0;
Result.AGCSlope := 0;
Result.AGCHangThreshold := 100;
Result.WfAGCEnabled := FWfAGCEnabled;
Result.WfNFEnabled := FWfNFEnabled;
Result.DitherEnabled := FDitherEnabled;
Result.RandomEnabled := FRandomEnabled;
Result.LastBand := FCurrentBand;
Result.LastXvtr := FCurrentXvtr;
Result.SampleRate := FSampleRate;
// Display settings
if FWDSPReady then
begin
Result.FFTSize := FDSPEngine.FFTSize;
Result.WindowType := FDSPEngine.WindowType;
Result.SpecDetector := FDSPEngine.SpecDetector;
Result.SpecAvgMode := FDSPEngine.SpecAvgMode;
Result.SpecAvgTimeMS := FDSPEngine.SpecAvgTimeMS;
Result.WfDetector := FDSPEngine.WfDetector;
Result.WfAvgMode := FDSPEngine.WfAvgMode;
Result.WfAvgTimeMS := FDSPEngine.WfAvgTimeMS;
end
else
begin
Result.FFTSize := 131072;
Result.WindowType := 2;
Result.SpecDetector := 0;
Result.SpecAvgMode := 3;
Result.SpecAvgTimeMS := 30.0;
Result.WfDetector := 0;
Result.WfAvgMode := 3;
Result.WfAvgTimeMS := 120.0;
end;
Result.WfManualHigh := FWfManualHigh;
Result.WfManualLow := FWfManualLow;
Result.WfAGCOffset := FWfAGCOffset;
Result.SpecRefLevel := FSpecRefLevel;
Result.SpecRange := FSpecRange;
Result.SpecGridStep := FSpecGridStep;
Result.AudioSampleRate := FAudioOut.SampleRate;
Result.ShowSpectrum := FShowSpectrum;
Result.ShowWaterfall := FShowWaterfall;
Result.DisplayDuplex := FDisplayDuplex;
Result.DisplayFPS := FDisplayFPS;
Result.LightTheme := FLightTheme;
Result.FreqMhzDigits := FFreqMhzDigits;
// Audio device names
Result.AudioOutDevice := FAudioOutDevName;
Result.AudioInDevice := FAudioInDevName;
// CAT settings — carried from the last loaded/saved device config
Result.CATSerialEnabled := FCATLastGlobal.CATSerialEnabled;
Result.CATSerialPort := FCATLastGlobal.CATSerialPort;
Result.CATSerialBaud := FCATLastGlobal.CATSerialBaud;
Result.CATSerialDataBits := FCATLastGlobal.CATSerialDataBits;
Result.CATSerialStopBits := FCATLastGlobal.CATSerialStopBits;
Result.CATSerialParity := FCATLastGlobal.CATSerialParity;
Result.CATTcpEnabled := FCATLastGlobal.CATTcpEnabled;
Result.CATTcpPort := FCATLastGlobal.CATTcpPort;
end;
procedure TMainForm.SaveCurrentBand;
begin
if not FDevConnected then Exit;
// В XVTR-режиме НЕ трогаем HF band-cache: FBandCache[FCurrentBand]
// относится к HF-диапазону, на котором пользователь был ДО активации
// трансвертера, и FVfoA сейчас на XVTR-частоте (144.x, 432.x и т.д.).
// Запись в FBandCache[FCurrentBand] стерла бы корректное HF-состояние.
// Вместо этого сохраняем LastFreq в XVTR-настройках.
if FCurrentXvtr >= 0 then
begin
if (FCurrentXvtr < CFG_XVTR_COUNT) then
begin
FXvtrSettings.Entries[FCurrentXvtr].LastFreq := FVfoA;
FXvtrSettings.Entries[FCurrentXvtr].LastMode := FMode;
FXvtrSettings.Entries[FCurrentXvtr].LastFilterIdx := FFilter;
FXvtrSettings.Entries[FCurrentXvtr].LastFMSQOn := FFMSQOn;
FXvtrSettings.Entries[FCurrentXvtr].LastFMSQLevel := FFMSQLevel;
FXvtrSettings.Entries[FCurrentXvtr].LastCTCSSOn := FFMCTCSSOn;
FXvtrSettings.Entries[FCurrentXvtr].LastCTCSSToneIdx := FFMCTCSSToneIdx;
FXvtrSettings.Entries[FCurrentXvtr].LastFMStepOn := FFMStepOn;
FXvtrSettings.Entries[FCurrentXvtr].LastFMStepIdx := FFMStepIdx;
FXvtrSettings.Entries[FCurrentXvtr].LastCTun := FCTun;
FXvtrSettings.Entries[FCurrentXvtr].LastAGCMode := FAGCMode;
FXvtrSettings.Entries[FCurrentXvtr].LastAGCTop := FAGCTop;
end;
FSettings.SaveXvtr(FDevMAC, FXvtrSettings);
Exit;
end;
FBandCache[FCurrentBand] := MakeBandSettings;
FSettings.SaveBand(FDevMAC, FCurrentBand, FBandCache[FCurrentBand]);
end;
procedure TMainForm.RestoreBand(BandIdx: Integer);
var
B: TBandSettings;
i: Integer;
begin
if (BandIdx < 0) or (BandIdx >= CFG_BAND_COUNT) then Exit;
B := FBandCache[BandIdx];
// --- Кнопки диапазонов ---
for i := 0 to BAND_COUNT - 1 do
StyleButton(BtnBand[i], i = BandIdx);
// Сбрасываем waterfall thresholds при смене диапазона
FSpecView.ResetWfAvgBuf;
// --- Режим: сначала устанавливаем FMode, потом кнопки и WDSP ---
FMode := B.Mode;
for i := 0 to MODE_COUNT - 1 do
StyleButton(BtnMode[i], i = FMode);
if FWDSPReady then
FDSPEngine.SetMode(FMode);
// --- Фильтр ---
FFilter := B.FilterIdx;
FFilterBW := B.FilterBW;
UpdateFilterButtons; // обновляет кнопки фильтра
ApplyModeFilter; // применяет Lo/Hi в WDSP с учётом нового FMode
// --- AGC ---
FAGCMode := B.AGCMode;
FAGCTop := B.AGCTop;
for i := 0 to 4 do
StyleButton(BtnAGCMode[i], i = FAGCMode);
TrkAGC.Position := FAGCTop;
LblAGCTop.Caption := Format('%ddB', [FAGCTop]);
if FWDSPReady then
begin
FDSPEngine.SetAGCTop(FAGCTop);
FDSPEngine.SetAGC(TWDSPAGCMode(FAGCMode), 50.0);
end;
FSpecView.AGCTop := FAGCTop;
// --- FM Squelch + CTCSS ---
FFMSQOn := B.FMSQOn;
FFMSQLevel := B.FMSQLevel;
// UI update happens inside UpdateFilterButtons (called above) when mode=FM
FFMCTCSSOn := B.CTCSSOn;
if BtnFMCTCSS <> nil then StyleButton(BtnFMCTCSS, FFMCTCSSOn);
SetFMCTCSSTone(B.CTCSSToneIdx);
ApplyFMSquelch;
FFMStepOn := B.FMStepOn;
SetFMStep(B.FMStepIdx);
if BtnFMStep <> nil then StyleButton(BtnFMStep, FFMStepOn);
// --- CTUN ---
FCTun := B.CTun;
StyleButton(BtnCTun, FCTun);
if FWDSPReady and not FCTun then
FDSPEngine.SetShift(0.0);
// --- Sample Rate / Span ---
// SampleRate — глобальный, не per-band. Не восстанавливаем из диапазона.
// Обновляем только FSpanHz для отображения (берём текущий FSampleRate)
FSpanHz := FSampleRate;
StyleSpanButton(BtnSpan48k, FSampleRate = 48000);
StyleSpanButton(BtnSpan96k, FSampleRate = 96000);
StyleSpanButton(BtnSpan192k, FSampleRate = 192000);
StyleSpanButton(BtnSpan384k, FSampleRate = 384000);
StyleSpanButton(BtnSpan768k, FSampleRate = 768000);
StyleSpanButton(BtnSpan1536k, FSampleRate = 1536000);
// --- VFO A: центрируем на новой частоте, сбрасываем shift ---
FCenterFreq := B.VfoA; // DDC = центр диапазона
FVfoB := B.VfoB;
FreqDispB.Frequency := Round(FVfoB);
// ApplyVfoA обновит сеть, FreqDisp, shift, перерисует
ApplyVfoA(Round(B.VfoA));
end;
procedure TMainForm.SaveAllAndExit;
begin
if FDevConnected then
begin
SaveCurrentBand;
FSettings.SaveGlobal(FDevMAC, MakeGlobalSettings);
if FDevConnected then
FSettings.SaveTX(FDevMAC, FTXSettings);
// Запомнить LastFreq XVTR для следующего запуска
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) then
FXvtrSettings.Entries[FCurrentXvtr].LastFreq := FVfoA;
FSettings.SaveXvtr(FDevMAC, FXvtrSettings);
FSettings.Save;
end;
end;
procedure TMainForm.FormCreate(Sender: TObject);
var
bi_, StartupRate, i: Integer;
WebCfg: TWebSettings;
StartupVfoA, StartupVfoB: Double;
begin
FVfoA := 14200000;
FVfoB := 7100000;
FActiveVfo := 0;
FMode := 1;
FFilter := 5;
FAGCMode := 1; // MEDIUM
FAGCTop := 90; // 90 dBm
// default: зависит от режима, будет сброшен в UpdateFilterButtons
FCTun := False;
FFilterBW := 2700;
FSpecDrag := False;
FDriveLevel := 0;
FPAMaxPower := 100.0;
for i := 0 to BAND_COUNT - 1 do FPABandCal[i] := 100.0;
FRunning := False;
FTransmitting := False;
FSplitTxB := False;
FDUCPendingCount := 0;
FDisplayDuplex := False;
FTuning := False;
FMuted := False;
FVolume := 70;
FCenterFreq := FVfoA;
FSpanHz := 192000;
FSampleRate := 192000;
FCurrentBand := 5; // 20m по умолчанию
// Waterfall normalization
FWfManualHigh := -80.0;
FWfManualLow := -130.0;
FWfAGCOffset := 0.0;
FWfAGCEnabled := True;
FWfNFEnabled := False;
FDitherEnabled := True;
FRandomEnabled := True;
FFMDeviation := FILT_FM_DEV[FILT_FM_DEF];
FFMCTCSSOn := False;
FFMCTCSSToneIdx := 0;
FFMSQOn := False;
FFMSQLevel := 30;
FFMStepOn := True;
FFMStepIdx := FM_STEP_DEF;
FSpectrumBufCount := 1024;
FWaterfallBufCount := 1024;
FWaterfallFrameInterval := 2;
FWaterfallFrameCounter := 0;
FDevConnected := False;
// Spectrum grid settings defaults
FSpecRefLevel := -20.0;
FSpecRange := 110.0;
FSpecGridStep := 10.0;
// TX grid defaults (синхронно с TSettingsManager.DefaultTX)
FTXSpecRefLevel := 0.0;
FTXSpecRange := 80.0;
FTXSpecGridStep := 10.0;
FSettingsForm := nil;
FillChar(FDevMAC, SizeOf(FDevMAC), 0);
// Инициализируем кэш диапазонов умолчаниями
for bi_ := 0 to CFG_BAND_COUNT - 1 do
TSettingsManager.DefaultBand(bi_, FBandCache[bi_]);
FCurrentBand := 5;
// Загружаем JSON настройки
FSettings := TSettingsManager.Create;
FSettings.Load;
if FSettings.LoadStartupPreview(StartupVfoA, StartupVfoB, StartupRate) then
begin
FVfoA := StartupVfoA;
FVfoB := StartupVfoB;
FCenterFreq := FVfoA;
if StartupRate > 0 then
begin
FSampleRate := StartupRate;
FSpanHz := StartupRate;
end;
end;
// Восстанавливаем размер/позицию окна при старте (до подключения устройства)
RestoreWindowBounds;
// S-метр позиционируем после рестора размера окна
// (будет пересчитан в первом тике SpectrumTimerTick)
FLastSMeter := -130;
// Загружаем настройки веб-сервера из JSON и создаём сервер
FSettings.LoadWebSettings(WebCfg);
FWebEnabled := WebCfg.Enabled;
FWebPort := WebCfg.Port;
FWebBindAddr := WebCfg.BindAddr;
FWebUser := WebCfg.User;
FWebPass := WebCfg.Pass;
FWebServer := TWebServer.Create(FWebUser, FWebPass, Word(FWebPort), FWebBindAddr);
FWebServer.OnFreq := WebOnFreq;
FWebServer.OnMode := WebOnMode;
FWebServer.OnFilter := WebOnFilter;
FWebServer.OnAGC := WebOnAGC;
FWebServer.OnAGCTop := WebOnAGCTop;
FWebServer.OnBand := WebOnBand;
FWebServer.OnSpan := WebOnSpan;
FWebServer.OnVolume := WebOnVolume;
FWebServer.OnWfAGC := WebOnWfAGC;
FWebServer.OnWfNF := WebOnWfNF;
FWebServer.OnRun := WebOnRun;
FWebServer.OnMute := WebOnMute;
FWebServer.OnCtun := WebOnCtun;
FWebServer.OnNR := WebOnNR;
FWebServer.OnNB := WebOnNB;
FWebServer.OnSNB := WebOnSNB;
FWebServer.OnANF := WebOnANF;
FWebServer.OnFreqB := WebOnFreqB;
FWebServer.OnActiveVfo := WebOnActiveVfo;
FWebServer.OnCenter := WebOnCenter;
FWebServer.OnMOX := WebOnMOX;
FWebServer.OnDrive := WebOnDrive;
FWebServer.OnFreqA := WebOnFreqA;
FWebServer.OnAttn := WebOnAttn;
FWebServer.OnTun := WebOnTun;
FWebServer.OnFMStep := WebOnFMStep;
FWebServer.OnWebMic := WebOnMic;
FWebServer.OnXvtrBand := WebOnXvtrBand;
if FWebEnabled then FWebServer.Start;
PushXvtrToWeb;
FillChar(FCATLastGlobal, SizeOf(FCATLastGlobal), 0);
FCATLastGlobal.CATTcpPort := 19090;
for i := 0 to 3 do
begin
FCATLastGlobal.CATSerialBaud[i] := 9600;
FCATLastGlobal.CATSerialDataBits[i] := 8;
FCATLastGlobal.CATSerialStopBits[i] := 1;
{$IFDEF MSWINDOWS}
FCATLastGlobal.CATSerialPort[i] := 'COM' + IntToStr(i + 1);
{$ELSE}
FCATLastGlobal.CATSerialPort[i] := '/dev/ttyS' + IntToStr(i);
{$ENDIF}
end;
FSettings.LoadCATSettings(FCATLastGlobal);
FLightTheme := FSettings.LoadTheme;
InitCATEngine;
CATApplySettings(FCATLastGlobal);
FSMeterPeak := -130;
FSMeterMin := -130;
FSMeterAvg := -130;
FRXPacketCount := 0;
FActiveDDC := 0;
FLastDDCSeq := 0;
FLastDDCIndex := 0;
FLastFwdW := 0;
FLastSWR := 1;
FLastSupplyV:= -1;
FLastPLLLock:= False;
FDeviceCount := 0;
FDeviceDialog := TDeviceDialog.Create(Self);
FDeviceDialog.OnDiscover := BtnDiscoverFromDialog;
FVfoOverlay := TVfoOverlay.Create(Self);
FVfoOverlay.OnSelect := OnModeFilterSelect;
FVfoOverlay.Visible := False;
FPanelHidden := False;
FShowSpectrum := True;
FShowWaterfall := True;
FDisplayFPS := 60;
FFreqMhzDigits := 3;
FSplitterRatio := 0.40;
FSplitterDrag := False;
FSpecView := TSpectrumView.Create;
FSpecView.VfoA := FVfoA;
FSpecView.VfoB := FVfoB;
FSpecView.ActiveVfo := FActiveVfo;
FSpecView.CenterFreq := FCenterFreq;
FSpecView.SpanHz := FSpanHz;
FSpecView.Mode := FMode;
FSpecView.FilterBW := FFilterBW;
FSpecView.AGCTop := FAGCTop;
FSpecView.WfAGCEnabled := FWfAGCEnabled;
FSpecView.WfNFEnabled := FWfNFEnabled;
FSpecView.WfManualHigh := FWfManualHigh;
FSpecView.WfManualLow := FWfManualLow;
FSpecView.WfAGCOffset := FWfAGCOffset;
FSpecView.SpecRefLevel := FSpecRefLevel;
FSpecView.SpecRange := FSpecRange;
FSpecView.SpecGridStep := FSpecGridStep;
// TX overlay: ширина окна = TX_SAMPLE_RATE из движка (фикс 192k)
FSpecView.TXSpanHz := WDSPEngine.TX_SAMPLE_RATE;
FSpecView.WfFrameInterval := FWaterfallFrameInterval;
FSpecView.PAMaxPower := FPAMaxPower;
FNetwork := THPSDRNetwork.Create;
FNetwork.OnDeviceFound := OnDeviceFound;
FNetwork.OnHPStatus := OnHPStatusCB;
FNetwork.OnDDCIQ := OnDDCIQCB;
FNetwork.OnMicPacket := OnMicPacketCB;
// Дефолты TX, Alex и XVTR (на случай если устройство ещё не выбрано).
TSettingsManager.DefaultTX(FTXSettings);
TSettingsManager.DefaultAlex(FAlexSettings);
TSettingsManager.DefaultXvtr(FXvtrSettings);
FCurrentXvtr := -1;
BuildUI;
StyleSpanButton(BtnSpan48k, FSampleRate = 48000);
StyleSpanButton(BtnSpan96k, FSampleRate = 96000);
StyleSpanButton(BtnSpan192k, FSampleRate = 192000);
StyleSpanButton(BtnSpan384k, FSampleRate = 384000);
StyleSpanButton(BtnSpan768k, FSampleRate = 768000);
StyleSpanButton(BtnSpan1536k, FSampleRate = 1536000);
ResizeSMeter;
ApplyDarkTheme;
UpdateVfoDisplay;
UpdateFilterButtons;
// DSP Engine — создаём объект, Open вызовется при нажатии START
// (FDSPEngine.Open загружает libwdsp и занимает ~1-2 сек)
FWDSPReady := False;
// BufSize=512: FBufSize=2048 @ 192kHz, FAudioBufSize=512 @ 48kHz.
// Уменьшение с 1024 вдвое сокращает время построения downsampler в OpenChannel RXA
// (~1300ms → ~650ms). Латентность: 512/48000 ≈ 10.7ms — допустимо для SDR.
FDSPEngine := TWDSPEngine.Create(FSampleRate, 48000, 512);
FDSPEngine.OnAudio := OnAudioReady;
FDSPEngine.OnSpectrum := OnSpectrumReady;
FDSPEngine.OnWaterfall := OnWaterfallReady;
FDSPEngine.OnTXIQ := OnTXIQReady;
// Sound-card mic путь: WDSP TX-thread дёргает этот колбэк перед каждым
// тиком, если выбран источник SoundCard. Для Radio колбэк не вызывается.
FDSPEngine.OnPullMicSamples := PullSoundCardMic;
// Audio output/input — создаём объекты сейчас, открываем после показа формы
// (Pa_Initialize на Linux пишет в stderr до перехвата сигналов FPC)
FAudioOut := TAudioOutput.Create(48000);
FAudioIn := TAudioInput.Create(48000);
FMeterTimer := TTimer.Create(Self);
FMeterTimer.Interval := 100;
FMeterTimer.OnTimer := MeterTimerTick;
FMeterTimer.Enabled := True;
FSpectrumTimer := TTimer.Create(Self);
FSpectrumTimer.Interval := 50;
FSpectrumTimer.OnTimer := SpectrumTimerTick;
FSpectrumTimer.Enabled := True;
// Однократный таймер — инициализация аудио после показа формы
FAfterShowTimer := TTimer.Create(Self);
FAfterShowTimer.Interval := 200;
FAfterShowTimer.OnTimer := AfterShowTick;
FAfterShowTimer.Enabled := True;
OnMouseWheel := FormMouseWheel;
// Проверяем и при необходимости добавляем исключение в Windows Firewall
// (UDP входящий трафик для HPSDR Protocol 2)
{$IFDEF WINDOWS}
FirewallEnsureAllowed(ParamStr(0), 'OpenHPSDR Transceiver');
{$ENDIF}
end;
procedure TMainForm.FormDestroy(Sender: TObject);
begin
FMeterTimer.Enabled := False;
FSpectrumTimer.Enabled := False;
if FNetwork.Running then
FNetwork.SetRunAndFreq(False, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), 0);
FNetwork.Disconnect;
FNetwork.Free;
FAudioOut.Close;
FAudioOut.Free;
FAudioIn.Close;
FAudioIn.Free;
FDSPEngine.Close;
FDSPEngine.Free;
FreeAndNil(FSpecView);
// Сохраняем настройки при закрытии
if FDevConnected then
begin
SaveCurrentBand;
FSettings.SaveGlobal(FDevMAC, MakeGlobalSettings);
if FDevConnected then
FSettings.SaveTX(FDevMAC, FTXSettings);
end;
// Размер окна сохраняем всегда (не зависит от подключения)
FSettings.SaveStartupPreview(FVfoA, FVfoB, FSampleRate);
FSettings.SaveWindowBounds(Left, Top, Width, Height);
FSettings.Save;
FSettings.Free;
FWebServer.Stop;
FWebServer.Free;
if Assigned(FCATTcp) then begin FCATTcp.Stop; FreeAndNil(FCATTcp); end;
if Assigned(FCATSerial) then begin FCATSerial.StopAll; FreeAndNil(FCATSerial); end;
FreeAndNil(FCATEngine);
end;
procedure TMainForm.FormClose(Sender: TObject; var CloseAction: TCloseAction);
begin
// Останавливаем трансивер как при нажатии STOP
if FNetwork.Connected then
begin
if FRunning then
begin
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), 0, False, True, True);
FNetwork.SetRunAndFreq(False, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), 0);
FRunning := False;
FTransmitting := False;
if FWDSPReady then FDSPEngine.SetTXRun(False);
end;
FNetwork.Disconnect;
end;
CloseAction := caFree;
end;
// ===========================================================================
// Build UI (без анонимных процедур и inline var)
// ===========================================================================
procedure TMainForm.BuildUI;
const
BTN_H = 26;
BTN_SM = 24;
LEFT_W = 232;
TOP_VFO_W = 244;
TOP_VFO_FREQ_H = 47;
TOP_VFO_BTN_H = 14;
var
i, X, Y, W: Integer;
B: TFlatButton;
function MakeBtn(Parent: TWinControl; const Cap: string;
ALeft, ATop, AW, AH: Integer; Handler: TNotifyEvent): TFlatButton;
begin
Result := TFlatButton.Create(Self);
Result.Parent := Parent;
Result.Caption := Cap;
Result.Left := ALeft;
Result.Top := ATop;
Result.Width := AW;
Result.Height := AH;
Result.OnClick := Handler;
StyleButton(Result, False);
end;
procedure MakeLbl(Parent: TWinControl; const Cap: string; ALeft, ATop: Integer);
var
L: TLabel;
begin
L := TLabel.Create(Self);
L.Parent := Parent;
L.Caption := Cap;
L.Left := ALeft;
L.Top := ATop;
L.Font.Color := CLR_TEXTDIM;
L.Font.Name := 'Courier New';
L.Font.Size := 7;
end;
begin
// ---- Toolbar ----
PanelToolbar := TPanel.Create(Self);
PanelToolbar.Parent := Self;
PanelToolbar.Align := alTop;
PanelToolbar.Height := 36;
PanelToolbar.BevelOuter := bvNone;
PanelToolbar.Color := CLR_PANEL;
X := 4;
BtnDiscover := MakeBtn(PanelToolbar, 'DISCOVER', X, 3, 80, BTN_H, BtnDiscoverClick);
BtnStartStop := MakeBtn(PanelToolbar, 'START', X+84, 3, 76, BTN_H, BtnStartStopClick);
BtnSettings := MakeBtn(PanelToolbar, 'SETUP', X+164, 3, 66, BTN_H, BtnSettingsClick);
// DISCOVER — синеватый акцент
BtnDiscover.ClrNorm := TColor($00101828);
BtnDiscover.ClrHot := TColor($00182038);
BtnDiscover.ClrActive := TColor($00182038);
BtnDiscover.ClrBorder := TColor($00304860);
BtnDiscover.ClrText := TColor($0090B8D0);
BtnDiscover.ClrTextAct := TColor($00B0D0F0);
// START/STOP — зелёный тон в обоих состояниях
BtnStartStop.ClrNorm := TColor($00101810);
BtnStartStop.ClrBorder := TColor($00304830);
BtnStartStop.ClrText := TColor($0090C090);
// ---- Separator line ----
// ---- Status bar ----
StatusBar1 := TStatusBar.Create(Self);
StatusBar1.Parent := Self;
StatusBar1.Align := alBottom;
StatusBar1.SimplePanel := False;
with StatusBar1.Panels do
begin
Add; Items[0].Width := 150; Items[0].Text := 'IP: --';
Add; Items[1].Width := 120; Items[1].Text := 'Board: --';
Add; Items[2].Width := 200; Items[2].Text := 'Supply: --';
Add; Items[3].Width := 250; Items[3].Text := 'RX: waiting...';
Add; Items[4].Width := 200; Items[4].Text := 'Not connected';
Add; Items[5].Width := 280; Items[5].Text := 'TX: --';
end;
// ---- Left panel ----
PanelLeft := TPanel.Create(Self);
PanelLeft.Parent := Self;
PanelLeft.Align := alLeft;
PanelLeft.Width := LEFT_W;
PanelLeft.BevelOuter := bvNone;
Y := 38; // gap below separator: separator at y=66, content starts at y=74
// Compact top VFO group
PanelTopVfoGroup := TPanel.Create(Self);
PanelTopVfoGroup.Parent := Self;
PanelTopVfoGroup.SetBounds(0, 0, TOP_VFO_W * 2 + 64, 60);
PanelTopVfoGroup.BevelOuter := bvNone;
PanelTopVfoGroup.Color := TColor($00181818);
// VFO A
PanelTopVfoA := TPanel.Create(Self);
PanelTopVfoA.Parent := PanelTopVfoGroup;
PanelTopVfoA.SetBounds(0, 0, TOP_VFO_W, 60);
PanelTopVfoA.BevelOuter := bvNone;
PanelTopVfoA.Color := CLR_PANEL;
PanelVfoA := TPanel.Create(Self);
PanelVfoA.Parent := PanelTopVfoA;
PanelVfoA.SetBounds(0, 0, TOP_VFO_W, TOP_VFO_FREQ_H);
PanelVfoA.BevelOuter := bvNone;
BtnTopVfoASelect := MakeBtn(PanelVfoA, 'A', 0, 5, 20, 18, BtnTopVfoASelectClick);
BtnTopVfoASelect.Font.Name := 'Courier New';
BtnTopVfoASelect.Font.Size := 7;
BtnTopVfoASelect.Font.Bold := True;
BtnTopVfoASelect.ClrNorm := TColor($00303030);
BtnTopVfoASelect.ClrHot := TColor($00404040);
BtnTopVfoASelect.ClrActive := TColor($00404040);
BtnTopVfoASelect.ClrBorder := TColor($00505050);
BtnTopVfoASelect.ClrText := CLR_TEXTDIM;
BtnTopVfoASelect.ClrTextAct := CLR_TEXT;
BtnTopVfoATX := MakeBtn(PanelVfoA, 'TX', 0, 25, 20, 18, BtnTopVfoATXClick);
BtnTopVfoATX.Font.Name := 'Courier New';
BtnTopVfoATX.Font.Size := 7;
BtnTopVfoATX.Font.Bold := True;
BtnTopVfoATX.ClrNorm := TColor($00303030);
BtnTopVfoATX.ClrHot := TColor($00404040);
BtnTopVfoATX.ClrActive := TColor($00404040);
BtnTopVfoATX.ClrBorder := TColor($00505050);
BtnTopVfoATX.ClrText := CLR_TEXTDIM;
BtnTopVfoATX.ClrTextAct := CLR_TEXT;
LblVfoALabel := TLabel.Create(Self);
LblVfoALabel.Parent := PanelVfoA;
LblVfoALabel.Caption := 'VFO-A';
LblVfoALabel.Left := 0; LblVfoALabel.Top := 0;
LblVfoALabel.Font.Name := 'Courier New'; LblVfoALabel.Font.Size := 7;
LblVfoALabel.Font.Color := CLR_TEXTDIM;
LblVfoALabel.Visible := False;
FreqDispA := TFreqDisplay.Create(Self);
FreqDispA.Parent := PanelVfoA;
FreqDispA.SetBounds(20, -2, TOP_VFO_W - 20, TOP_VFO_FREQ_H);
FreqDispA.FontSize := TOP_VFO_FONT;
FreqDispA.FontName := 'Courier New';
FreqDispA.Frequency := Round(FVfoA);
FreqDispA.ColorNormal := CLR_FREQ;
FreqDispA.ColorHover := TColor($0040DDFF);
FreqDispA.ColorDim := CLR_TEXTDIM;
FreqDispA.MinMhzDigits := 3;
FreqDispA.DimLeadingZeros := True;
FreqDispA.CenterText := False;
FreqDispA.OnChange := FreqDispAChanged;
FreqDispA.OnClick := FreqDispAClick;
// VFO B
PanelTopVfoB := TPanel.Create(Self);
PanelTopVfoB.Parent := PanelTopVfoGroup;
PanelTopVfoB.SetBounds(0, 0, TOP_VFO_W, 60);
PanelTopVfoB.BevelOuter := bvNone;
PanelTopVfoB.Color := CLR_PANEL;
PanelVfoB := TPanel.Create(Self);
PanelVfoB.Parent := PanelTopVfoB;
PanelVfoB.SetBounds(0, 0, TOP_VFO_W, TOP_VFO_FREQ_H);
PanelVfoB.BevelOuter := bvNone;
BtnTopVfoBSelect := MakeBtn(PanelVfoB, 'B', 0, 5, 20, 18, BtnTopVfoBSelectClick);
BtnTopVfoBSelect.Font.Name := 'Courier New';
BtnTopVfoBSelect.Font.Size := 7;
BtnTopVfoBSelect.Font.Bold := True;
BtnTopVfoBSelect.ClrNorm := TColor($00303030);
BtnTopVfoBSelect.ClrHot := TColor($00404040);
BtnTopVfoBSelect.ClrActive := TColor($00404040);
BtnTopVfoBSelect.ClrBorder := TColor($00505050);
BtnTopVfoBSelect.ClrText := CLR_TEXTDIM;
BtnTopVfoBSelect.ClrTextAct := CLR_TEXT;
BtnTopVfoBTX := MakeBtn(PanelVfoB, 'TX', 0, 25, 20, 18, BtnTopVfoBTXClick);
BtnTopVfoBTX.Font.Name := 'Courier New';
BtnTopVfoBTX.Font.Size := 7;
BtnTopVfoBTX.Font.Bold := True;
BtnTopVfoBTX.ClrNorm := TColor($00303030);
BtnTopVfoBTX.ClrHot := TColor($00404040);
BtnTopVfoBTX.ClrActive := TColor($00404040);
BtnTopVfoBTX.ClrBorder := TColor($00505050);
BtnTopVfoBTX.ClrText := CLR_TEXTDIM;
BtnTopVfoBTX.ClrTextAct := CLR_TEXT;
LblVfoBLabel := TLabel.Create(Self);
LblVfoBLabel.Parent := PanelVfoB;
LblVfoBLabel.Caption := 'VFO-B';
LblVfoBLabel.Left := 0; LblVfoBLabel.Top := 0;
LblVfoBLabel.Font.Name := 'Courier New'; LblVfoBLabel.Font.Size := 7;
LblVfoBLabel.Font.Color := CLR_TEXTDIM;
LblVfoBLabel.Visible := False;
FreqDispB := TFreqDisplay.Create(Self);
FreqDispB.Parent := PanelVfoB;
FreqDispB.SetBounds(20, -2, TOP_VFO_W - 20, TOP_VFO_FREQ_H);
FreqDispB.FontSize := TOP_VFO_FONT;
FreqDispB.FontName := 'Courier New';
FreqDispB.Frequency := Round(FVfoB);
FreqDispB.ColorNormal := CLR_FREQ_DIM;
FreqDispB.ColorHover := TColor($0040DDFF);
FreqDispB.ColorDim := CLR_TEXTDIM;
FreqDispB.MinMhzDigits := 3;
FreqDispB.DimLeadingZeros := True;
FreqDispB.CenterText := False;
FreqDispB.OnChange := FreqDispBChanged;
FreqDispB.OnClick := FreqDispBClick;
// VFO transfer buttons — горизонтальный ряд по центру
PanelVfoButtons := TPanel.Create(Self);
PanelVfoButtons.Parent := PanelTopVfoGroup;
PanelVfoButtons.SetBounds(0, 0, 140, 20);
PanelVfoButtons.BevelOuter := bvNone;
PanelVfoButtons.Color := TColor($00181818);
BtnVfoACopyB := MakeBtn(PanelVfoButtons, 'A>B', 0, 0, 44, 20, BtnVfoACopyBClick);
BtnVfoSwap := MakeBtn(PanelVfoButtons, 'A<>B', 0, 0, 44, 20, BtnVfoSwapClick);
BtnVfoBCopyA := MakeBtn(PanelVfoButtons, 'B>A', 0, 0, 44, 20, BtnVfoBCopyAClick);
// Янтарный стиль, шрифт под размер кнопок
StyleSpanButton(BtnVfoACopyB, False);
StyleSpanButton(BtnVfoSwap, False);
StyleSpanButton(BtnVfoBCopyA, False);
BtnVfoACopyB.Font.Size := 7;
BtnVfoSwap.Font.Size := 7;
BtnVfoBCopyA.Font.Size := 7;
LayoutTopVfoBlock;
// TX controls
PanelTX := TPanel.Create(Self);
PanelTX.Parent := PanelLeft;
PanelTX.SetBounds(0, Y, LEFT_W, 72);
PanelTX.BevelOuter := bvNone;
MakeLbl(PanelTX, 'TX', 4, 2);
W := (LEFT_W - 6) div 4;
BtnMOX := MakeBtn(PanelTX, 'MOX', 2, 16, W - 2, BTN_SM, BtnMOXClick);
BtnTUN := MakeBtn(PanelTX, 'TUN', W + 2, 16, W - 2, BTN_SM, BtnTUNClick);
LblDrv := TLabel.Create(Self);
LblDrv.Parent := PanelTX; LblDrv.Left := 4; LblDrv.Top := 53;
LblDrv.Caption := 'DRV'; LblDrv.Font.Color := CLR_TEXTDIM;
LblDrv.Font.Name := 'Courier New'; LblDrv.Font.Size := 7;
TrkDrive := TFlatSlider.Create(Self);
TrkDrive.Parent := PanelTX; TrkDrive.Left := 34; TrkDrive.Top := 50;
TrkDrive.Width := 150; TrkDrive.Height := 16;
TrkDrive.Min := 0; TrkDrive.Max := 100; TrkDrive.Position := 50;
TrkDrive.OnChange := TrkDriveChange;
Inc(Y, 76);
// Band selector. Высота резервирует 2 дополнительных ряда под XVTR-кнопки
// (создаются динамически в RebuildXvtrButtons). В каждом ряду до 6 кнопок,
// итого до 12 XVTR-кнопок (наш CFG_XVTR_COUNT = 8 умещается с запасом).
PanelBands := TPanel.Create(Self);
PanelBands.Parent := PanelLeft;
PanelBands.SetBounds(0, Y, LEFT_W, 74);
PanelBands.BevelOuter := bvNone;
MakeLbl(PanelBands, 'BAND', 4, 2);
W := (LEFT_W - 6) div 6;
for i := 0 to BAND_COUNT - 1 do
begin
B := MakeBtn(PanelBands, BAND_NAMES[i],
2 + (i mod 6) * W,
16 + (i div 6) * 27,
W - 2, BTN_SM, BtnBandClick);
B.Tag := i;
BtnBand[i] := B;
end;
// XVTR кнопки создаются позже (в RebuildXvtrButtons), их Tag = -(idx+1)
for i := 0 to CFG_XVTR_COUNT - 1 do
BtnXvtrBand[i] := nil;
Inc(Y, 76);
// Mode selector
PanelMode := TPanel.Create(Self);
PanelMode.Parent := PanelLeft;
PanelMode.SetBounds(0, Y, LEFT_W, 74);
PanelMode.BevelOuter := bvNone;
MakeLbl(PanelMode, 'MODE', 4, 2);
W := (LEFT_W - 6) div 4;
for i := 0 to MODE_COUNT - 1 do
begin
B := MakeBtn(PanelMode, MODE_NAMES[i],
2 + (i mod 4) * W,
16 + (i div 4) * 27,
W - 2, BTN_SM, BtnModeClick);
B.Tag := i;
BtnMode[i] := B;
StyleButton(B, i = FMode);
end;
Inc(Y, 76);
// Filter selector (10 кнопок в 2 ряда по 5)
PanelFilter := TPanel.Create(Self);
PanelFilter.Parent := PanelLeft;
PanelFilter.SetBounds(0, Y, LEFT_W, 74);
PanelFilter.BevelOuter := bvNone;
MakeLbl(PanelFilter, 'FILTER', 4, 2);
W := (LEFT_W - 6) div 5;
for i := 0 to FILT_COUNT - 1 do
begin
B := MakeBtn(PanelFilter, '---',
2 + (i mod 5) * W,
16 + (i div 5) * 27,
W - 2, BTN_SM, BtnFilterClick);
B.Tag := i;
BtnFilter[i] := B;
end;
Inc(Y, 76);
// FM Squelch panel (visible only in FM mode, above CTCSS)
PanelFMSQ := TPanel.Create(Self);
PanelFMSQ.Parent := PanelLeft;
PanelFMSQ.SetBounds(0, Y, LEFT_W, 50);
PanelFMSQ.BevelOuter := bvNone;
PanelFMSQ.Visible := False;
MakeLbl(PanelFMSQ, 'SQL', 4, 4);
W := LEFT_W div 4 - 2; // button width ~56px
BtnFMSQ := MakeBtn(PanelFMSQ, 'SQL', 2, 20, W, BTN_SM, BtnFMSQClick);
StyleButton(BtnFMSQ, False);
// Slider: vertically centred with button text (button top=20, h=24 → center=32)
// Reserve 28px on the right for the numeric label
TrkFMSQ := TFlatSlider.Create(Self);
TrkFMSQ.Parent := PanelFMSQ;
TrkFMSQ.Left := W + 6;
TrkFMSQ.Top := 25; // 32 - 14/2 = 25
TrkFMSQ.Width := LEFT_W - W - 6 - 30 - 4; // leave 30px for label + margins
TrkFMSQ.Height := 14;
TrkFMSQ.Min := 0; TrkFMSQ.Max := 100;
TrkFMSQ.Position := FFMSQLevel;
TrkFMSQ.OnChange := TrkFMSQChange;
// Numeric value label to the right of the slider
LblFMSQ := TLabel.Create(Self);
LblFMSQ.Parent := PanelFMSQ;
LblFMSQ.Left := TrkFMSQ.Left + TrkFMSQ.Width + 3;
LblFMSQ.Top := 25;
LblFMSQ.Width := 26;
LblFMSQ.Caption := IntToStr(FFMSQLevel);
LblFMSQ.Font.Color := CLR_TEXT;
LblFMSQ.Font.Name := 'Courier New';
LblFMSQ.Font.Size := 8;
// FM CTCSS panel (visible only when FM mode is active)
PanelFMCTCSS := TPanel.Create(Self);
PanelFMCTCSS.Parent := PanelLeft;
PanelFMCTCSS.SetBounds(0, Y, LEFT_W, 50);
PanelFMCTCSS.BevelOuter := bvNone;
PanelFMCTCSS.Visible := False;
MakeLbl(PanelFMCTCSS, 'CTCSS', 4, 4);
W := LEFT_W div 3 - 2;
BtnFMCTCSS := MakeBtn(PanelFMCTCSS, 'CTCSS', 2, 20, W, BTN_H, BtnFMCTCSSClick);
StyleButton(BtnFMCTCSS, False);
BtnFMCTCSSTone := MakeBtn(PanelFMCTCSS, CTCSS_NAMES[0], W + 4, 20, LEFT_W - W - 8, BTN_H, BtnFMCTCSSToneClick);
StyleButton(BtnFMCTCSSTone, False);
// Tone picker popup (child of PanelLeft, hidden until BtnFMCTCSSTone is clicked)
PanelFMCTCSSPop := TPanel.Create(Self);
PanelFMCTCSSPop.Parent := PanelLeft;
PanelFMCTCSSPop.BevelOuter := bvNone;
PanelFMCTCSSPop.Color := CLR_PANEL;
PanelFMCTCSSPop.Visible := False;
W := (LEFT_W - 4) div 4;
PanelFMCTCSSPop.SetBounds(0, 0, LEFT_W, ((CTCSS_COUNT + 3) div 4) * BTN_SM);
for i := 0 to CTCSS_COUNT - 1 do
begin
B := MakeBtn(PanelFMCTCSSPop, CTCSS_NAMES[i],
2 + (i mod 4) * W,
(i div 4) * BTN_SM,
W - 2, BTN_SM, BtnFMToneSelectClick);
B.Tag := i;
StyleButton(B, i = FFMCTCSSToneIdx);
FBtnCTCSSTones[i] := B;
end;
// FM Step panel (visible only in FM mode, between Filter and SQL)
PanelFMStep := TPanel.Create(Self);
PanelFMStep.Parent := PanelLeft;
PanelFMStep.SetBounds(0, Y, LEFT_W, 50);
PanelFMStep.BevelOuter := bvNone;
PanelFMStep.Visible := False;
MakeLbl(PanelFMStep, 'STEP', 4, 4);
W := LEFT_W div 3 - 2;
BtnFMStep := MakeBtn(PanelFMStep, 'STEP', 2, 20, W, BTN_H, BtnFMStepClick);
StyleButton(BtnFMStep, FFMStepOn);
BtnFMStepSel := MakeBtn(PanelFMStep, FM_STEP_NAMES[FFMStepIdx],
W + 4, 20, LEFT_W - W - 8, BTN_H, BtnFMStepSelClick);
StyleButton(BtnFMStepSel, False);
// Step picker popup (child of PanelLeft)
PanelFMStepPop := TPanel.Create(Self);
PanelFMStepPop.Parent := PanelLeft;
PanelFMStepPop.BevelOuter := bvNone;
PanelFMStepPop.Color := CLR_PANEL;
PanelFMStepPop.Visible := False;
W := (LEFT_W - 4) div FM_STEP_COUNT;
PanelFMStepPop.SetBounds(0, 0, LEFT_W, BTN_SM);
for i := 0 to FM_STEP_COUNT - 1 do
begin
B := MakeBtn(PanelFMStepPop, FM_STEP_NAMES[i],
2 + i * W, 0, W - 2, BTN_SM, BtnFMStepSelectClick);
B.Tag := i;
StyleButton(B, i = FFMStepIdx);
FBtnFMSteps[i] := B;
end;
// CTUN / DUP buttons (display-related)
BtnCTun := MakeBtn(PanelLeft, 'CTUN', 2, Y, LEFT_W div 3 - 2, BTN_H, BtnCTunClick);
BtnCTun.Tag := 0;
StyleButton(BtnCTun, FCTun);
BtnDUP := MakeBtn(PanelLeft, 'DUP',
2 + (LEFT_W div 3) + 0, Y, LEFT_W div 3 - 2, BTN_H, BtnDUPClick);
BtnDUP.Tag := 0;
StyleButton(BtnDUP, FDisplayDuplex);
Inc(Y, BTN_H + 4);
// RX controls
PanelRX := TPanel.Create(Self);
PanelRX.Parent := PanelLeft;
PanelRX.SetBounds(0, Y, LEFT_W, 122);
PanelRX.BevelOuter := bvNone;
// AGC mode — 5 кнопок в ряд
MakeLbl(PanelRX, 'AGC', 4, 4);
W := (LEFT_W - 10) div 5;
BtnAGCMode[0] := MakeBtn(PanelRX,'FAST', 2, 22, W, BTN_SM, BtnAGCModeClick); BtnAGCMode[0].Tag:=0;
BtnAGCMode[1] := MakeBtn(PanelRX,'MED', W+4, 22, W, BTN_SM, BtnAGCModeClick); BtnAGCMode[1].Tag:=1;
BtnAGCMode[2] := MakeBtn(PanelRX,'SLOW', 2*W+6, 22, W, BTN_SM, BtnAGCModeClick); BtnAGCMode[2].Tag:=2;
BtnAGCMode[3] := MakeBtn(PanelRX,'LONG', 3*W+8, 22, W, BTN_SM, BtnAGCModeClick); BtnAGCMode[3].Tag:=3;
BtnAGCMode[4] := MakeBtn(PanelRX,'OFF', 4*W+10, 22, W, BTN_SM, BtnAGCModeClick); BtnAGCMode[4].Tag:=4;
for i := 0 to 4 do StyleButton(BtnAGCMode[i], i = FAGCMode);
// AGC level slider
MakeLbl(PanelRX, 'THRESH', 4, 50);
TrkAGC := TFlatSlider.Create(Self);
TrkAGC.Parent := PanelRX; TrkAGC.Left := 56; TrkAGC.Top := 46;
TrkAGC.Width := LEFT_W - 96; TrkAGC.Height := 16;
TrkAGC.Min := 20; TrkAGC.Max := 120; // 20..120 → 20..-120 dBm
TrkAGC.Position := FAGCTop;
TrkAGC.Reversed := True;
TrkAGC.OnChange := TrkAGCChange;
LblAGCTop := TLabel.Create(Self);
LblAGCTop.Parent := PanelRX;
LblAGCTop.Left := LEFT_W - 38; LblAGCTop.Top := 50;
LblAGCTop.Caption := Format('-%ddB', [FAGCTop]);
LblAGCTop.Font.Color := CLR_TEXT; LblAGCTop.Font.Name := 'Courier New';
LblAGCTop.Font.Size := 7;
// VOL
MakeLbl(PanelRX, 'VOL', 4, 74);
TrkVolume := TFlatSlider.Create(Self);
TrkVolume.Parent := PanelRX; TrkVolume.Left := 34; TrkVolume.Top := 70;
TrkVolume.Width := LEFT_W - 38; TrkVolume.Height := 16;
TrkVolume.Min := 0; TrkVolume.Max := 100;
TrkVolume.Position := FVolume;
TrkVolume.OnChange := TrkVolumeChange;
// NR NB SNB ANF MUTE
W := (LEFT_W - 10) div 5;
BtnNR := MakeBtn(PanelRX, 'NR', 2, 96, W, BTN_SM, BtnNRClick);
BtnNB := MakeBtn(PanelRX, 'NB', W+4, 96, W, BTN_SM, BtnNBClick);
BtnSNB := MakeBtn(PanelRX, 'SNB', 2*W+6, 96, W, BTN_SM, BtnSNBClick);
BtnANF := MakeBtn(PanelRX, 'ANF', 3*W+8, 96, W, BTN_SM, BtnANFClick);
BtnMute:= MakeBtn(PanelRX, 'MUTE', 4*W+10, 96, W, BTN_SM, BtnMuteClick);
Inc(Y, 126);
// ---- Right panel ----
PanelRight := TPanel.Create(Self);
PanelRight.Parent := Self;
PanelRight.Align := alClient;
PanelRight.BevelOuter := bvNone;
PanelRight.OnResize := RightPanelResize;
PanelSpanButtons := TPanel.Create(Self);
PanelSpanButtons.Parent := PanelRight;
PanelSpanButtons.Align := alTop;
PanelSpanButtons.Height := 38;
PanelSpanButtons.BevelOuter := bvNone;
LblSpan := TLabel.Create(Self);
LblSpan.Parent := PanelSpanButtons; LblSpan.Left := 4; LblSpan.Top := 8;
LblSpan.Caption := 'SPAN:'; LblSpan.Font.Color := CLR_TEXTDIM;
LblSpan.Font.Name := 'Courier New'; LblSpan.Font.Size := 7;
BtnSpan48k := MakeBtn(PanelSpanButtons, '48k', 50, 3, 46, BTN_SM, BtnSpanClick);
BtnSpan96k := MakeBtn(PanelSpanButtons, '96k', 99, 3, 46, BTN_SM, BtnSpanClick);
BtnSpan192k := MakeBtn(PanelSpanButtons, '192k', 148, 3, 52, BTN_SM, BtnSpanClick);
BtnSpan384k := MakeBtn(PanelSpanButtons, '384k', 203, 3, 52, BTN_SM, BtnSpanClick);
BtnSpan768k := MakeBtn(PanelSpanButtons, '768k', 258, 3, 52, BTN_SM, BtnSpanClick);
BtnSpan1536k := MakeBtn(PanelSpanButtons, '1536k', 313, 3, 58, BTN_SM, BtnSpanClick);
BtnSpan48k.Tag := 48000;
BtnSpan96k.Tag := 96000;
BtnSpan192k.Tag := 192000;
BtnSpan384k.Tag := 384000;
BtnSpan768k.Tag := 768000;
BtnSpan1536k.Tag := 1536000;
StyleSpanButton(BtnSpan48k, False);
StyleSpanButton(BtnSpan96k, False);
StyleSpanButton(BtnSpan192k, True);
StyleSpanButton(BtnSpan384k, False);
StyleSpanButton(BtnSpan768k, False);
StyleSpanButton(BtnSpan1536k, False);
BtnHidePanel := MakeBtn(PanelRight, '◀', 0, 0, 28, 28, BtnHidePanelClick);
BtnHidePanel.Font.Size := 10;
BtnHidePanel.ClrNorm := TColor($00543F18);
BtnHidePanel.ClrHot := TColor($00624B1D);
BtnHidePanel.ClrActive := TColor($00624B1D);
BtnHidePanel.ClrBorder := TColor($00685624);
BtnHidePanel.ClrText := TColor($00D8E6EE);
BtnHidePanel.ClrTextAct := TColor($00F0FAFF);
BtnHidePanel.BringToFront;
// Разделитель: на уровне формы, под всеми верхними элементами
PanelSep := TPanel.Create(Self);
PanelSep.Parent := Self;
PanelSep.Align := alNone;
PanelSep.BevelOuter := bvNone;
PanelSep.ParentColor := False;
PanelSep.Color := TColor($00606060);
PanelSep.SetBounds(16, PanelToolbar.Height + 30, ClientWidth - 32, 2);
// Большой S-метр — на главной форме поверх двух панелей
// S-метр: Parent=Self, перекрывает Toolbar+SpanButtons вертикально
PanelSMeterRight := TPanel.Create(Self);
PanelSMeterRight.Parent := Self;
PanelSMeterRight.BevelOuter := bvNone;
PanelSMeterRight.Color := CLR_PANEL;
PbSMeterRight := TPaintBox.Create(Self);
PbSMeterRight.Parent := PanelSMeterRight;
PbSMeterRight.Align := alClient;
PbSMeterRight.OnPaint := FSpecView.PaintSMeterRight;
PbSMeterRight.Color := CLR_PANEL;
PbSpectrum := TPaintBox.Create(Self);
PbSpectrum.Parent := PanelRight;
PbSpectrum.OnPaint := FSpecView.PaintSpectrum;
PbSpectrum.OnMouseDown := PbSpectrumMouseDown;
PbSpectrum.OnDblClick := PbSpectrumDblClick;
PbSpectrum.OnMouseMove := PbSpectrumMouseMove;
PbSpectrum.OnMouseUp := PbSpectrumMouseUp;
PbRuler := TPaintBox.Create(Self);
PbRuler.Parent := PanelRight;
PbRuler.OnPaint := FSpecView.PaintRuler;
PbRuler.Cursor := crDefault;
FSpecView.PbRuler := PbRuler;
// ---- Splitter между спектром+линейкой и водопадом ----
PanelSplitter := TPanel.Create(Self);
PanelSplitter.Parent := PanelRight;
PanelSplitter.BevelOuter := bvNone;
PanelSplitter.Color := TColor($00303030);
PanelSplitter.Cursor := crVSplit;
PanelSplitter.Height := 5;
PanelSplitter.OnMouseDown := SplitterMouseDown;
PanelSplitter.OnMouseMove := SplitterMouseMove;
PanelSplitter.OnMouseUp := SplitterMouseUp;
PbWaterfall := TPaintBox.Create(Self);
PbWaterfall.Parent := PanelRight;
PbWaterfall.OnPaint := FSpecView.PaintWaterfall;
PbWaterfall.OnMouseDown := PbWaterfallMouseDown;
PbWaterfall.OnMouseMove := PbWaterfallMouseMove;
PbWaterfall.OnMouseUp := PbWaterfallMouseUp;
// SPAN кнопки как оверлей на спектре (в стиле web)
BtnSpan48k.Parent := PanelRight;
BtnSpan96k.Parent := PanelRight;
BtnSpan192k.Parent := PanelRight;
BtnSpan384k.Parent := PanelRight;
BtnSpan768k.Parent := PanelRight;
BtnSpan1536k.Parent := PanelRight;
LblSpan.Visible := False;
PositionSpanOverlayButtons;
// Initial layout
ResizeSpectrumPanels;
end;
// ===========================================================================
// Splitter — перетаскивание границы спектр/водопад
// Обновление размеров происходит только при отпускании мыши (MouseUp),
// чтобы исключить фризы во время перетаскивания.
// Во время перетаскивания рисуем только призрак-линию на PanelSplitter.
// ===========================================================================
procedure TMainForm.SplitterMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
var
P: TPoint;
begin
if Button <> mbLeft then Exit;
FSplitterDrag := True;
FSplitterSH0 := FSpectrumHeight;
// Y в координатах PanelRight
P := PanelRight.ScreenToClient(PanelSplitter.ClientToScreen(Point(X, Y)));
FSplitterDragY0 := P.Y;
PanelSplitter.Color := TColor($005050A0); // подсветка при захвате
{$IFDEF WINDOWS}
SetCapture(PanelSplitter.Handle);
{$ENDIF}
end;
procedure TMainForm.SplitterMouseMove(Sender: TObject; Shift: TShiftState;
X, Y: Integer);
var
P: TPoint;
DeltaY: Integer;
NewSH: Integer;
TopOff, AvailH: Integer;
RULER_H: Integer;
const
SPLITTER_H = 5;
MIN_SH = 60;
MIN_WH = 40;
begin
RULER_H := MulDiv(18, Screen.PixelsPerInch, 96);
if not FSplitterDrag then Exit;
P := PanelRight.ScreenToClient(PanelSplitter.ClientToScreen(Point(X, Y)));
DeltaY := P.Y - FSplitterDragY0;
TopOff := 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;
RULER_H: Integer;
const
SPLITTER_H = 5;
MIN_SH = 60;
MIN_WH = 40;
begin
RULER_H := MulDiv(18, Screen.PixelsPerInch, 96);
if not FSplitterDrag then Exit;
FSplitterDrag := False;
{$IFDEF WINDOWS}
ReleaseCapture;
{$ENDIF}
PanelSplitter.Color := TColor($00303030); // обычный цвет
P := PanelRight.ScreenToClient(PanelSplitter.ClientToScreen(Point(X, Y)));
DeltaY := P.Y - FSplitterDragY0;
TopOff := PanelSpanButtons.Height;
AvailH := PanelRight.ClientHeight - TopOff - RULER_H - SPLITTER_H;
if AvailH <= 0 then Exit;
NewSH := FSplitterSH0 + DeltaY;
if NewSH < MIN_SH then NewSH := MIN_SH;
if NewSH > AvailH - MIN_WH then NewSH := AvailH - MIN_WH;
// Сохраняем новое соотношение и делаем полный пересчёт с перерисовкой
FSplitterRatio := NewSH / AvailH;
ResizeSpectrumPanels;
end;
// ===========================================================================
// Resize handler (normal method instead of anonymous procedure)
// ===========================================================================
procedure TMainForm.RightPanelResize(Sender: TObject);
begin
ResizeSpectrumPanels;
end;
procedure TMainForm.LayoutTopVfoBlock;
const
MARGIN = 4;
TOP_OFFSET = 2;
TOP_VFO_FREQ_H = 47;
VFO_BTN_W = 20;
VFO_BTN_FREQ_GAP = 4;
VFO_FREQ_LEFT_PAD = 2;
VFO_FREQ_RIGHT_PAD = 8;
XFER_BTN_W = 44;
XFER_BTN_H = 20;
XFER_BTN_GAP = 4;
XFER_PANEL_W = XFER_BTN_W * 3 + XFER_BTN_GAP * 2; // = 140
XFER_SIDE_GAP = 14; // минимальный зазор между VFO и кнопками трансфера
SETTINGS_GAP = 8;
MAX_SPREAD = 100; // максимальный дополнительный отступ с каждой стороны при растяжении
// VFO-B получает левый отступ = правому паддингу VFO-A для симметрии зазоров
VFO_B_LEFT_PAD = VFO_FREQ_LEFT_PAD + VFO_FREQ_RIGHT_PAD; // = 10
var
BlockH, FreqTop, CenterY, SafeLeft, SafeRight, GroupLeft, GroupTop: Integer;
RightW, SMW, SMLeft, AvailW, MinGroupW, ExtraPerSide, DynGap: Integer;
TopVfoAW, TopVfoBW, NeedFreqAW, NeedFreqBW, FreqTextW, GroupW: Integer;
XferX, XferY: Integer;
begin
if (PanelToolbar = nil) or (PanelSpanButtons = nil) or (PanelTopVfoGroup = nil) or
(PanelTopVfoA = nil) or (PanelTopVfoB = nil) then Exit;
Canvas.Font.Name := FreqDispB.FontName;
Canvas.Font.Size := FreqDispB.FontSize;
Canvas.Font.Style := [fsBold];
FreqDispA.Left := BtnTopVfoASelect.Width + VFO_BTN_FREQ_GAP - VFO_FREQ_LEFT_PAD;
FreqDispB.Left := VFO_B_LEFT_PAD + BtnTopVfoBSelect.Width + VFO_BTN_FREQ_GAP - VFO_FREQ_LEFT_PAD;
FreqTextW := Canvas.TextWidth(StringOfChar('0', Max(3, FFreqMhzDigits)) + '.000.000');
NeedFreqAW := FreqDispA.Left + FreqTextW + VFO_FREQ_LEFT_PAD + VFO_FREQ_RIGHT_PAD;
NeedFreqBW := FreqDispB.Left + FreqTextW + VFO_FREQ_LEFT_PAD + VFO_FREQ_RIGHT_PAD;
TopVfoAW := NeedFreqAW;
TopVfoBW := NeedFreqBW;
// Минимальная ширина компактной группы
MinGroupW := TopVfoAW + XFER_SIDE_GAP + XFER_PANEL_W + XFER_SIDE_GAP + TopVfoBW;
SafeLeft := MARGIN;
if BtnSettings <> nil then
SafeLeft := BtnSettings.Left + BtnSettings.Width + SETTINGS_GAP;
SafeRight := ClientWidth - MARGIN;
if PanelLeft <> nil then
begin
RightW := ClientWidth - PanelLeft.Width;
if RightW > 200 then
begin
SMW := Round(RightW * SMETER_WIDTH_RATIO);
SMLeft := ClientWidth - SMW - SMETER_MARGIN;
SafeRight := Min(SafeRight, SMLeft - TOP_SMETER_GAP);
end;
end;
AvailW := SafeRight - SafeLeft;
if AvailW < MinGroupW then
begin
PanelTopVfoGroup.Visible := False;
Exit;
end;
// Динамический зазор: растёт с шириной окна, но не больше MAX_SPREAD
ExtraPerSide := Min((AvailW - MinGroupW) div 2, MAX_SPREAD);
DynGap := XFER_SIDE_GAP + ExtraPerSide;
GroupW := TopVfoAW + DynGap + XFER_PANEL_W + DynGap + TopVfoBW;
BlockH := PanelToolbar.Height + 30 - (MARGIN * 2); // 30 = span row content, not full PanelSpanButtons
if BlockH < 56 then BlockH := 56;
// Центрируем группу в доступном пространстве
GroupLeft := SafeLeft + (AvailW - GroupW) div 2;
GroupTop := TOP_OFFSET;
// Ось выравнивания: центр S-метра на форме (SMTop=3, SMBot=63) → y=33
// Относительно PanelTopVfoGroup (GroupTop=2): CenterY = 33 - 2 = 31
CenterY := (PanelToolbar.Height + 30) div 2 - GroupTop;
PanelTopVfoGroup.SetBounds(GroupLeft, GroupTop, GroupW, BlockH);
PanelTopVfoGroup.Visible := True;
// VFO-A: левый край группы
PanelTopVfoA.SetBounds(0, 0, TopVfoAW, BlockH);
PanelTopVfoA.Visible := True;
PanelVfoA.SetBounds(0, 0, TopVfoAW, BlockH);
// A выше оси, TX ниже; 1px зазор с каждой стороны
BtnTopVfoASelect.SetBounds(0, CenterY - BtnTopVfoASelect.Height - 1,
BtnTopVfoASelect.Width, BtnTopVfoASelect.Height);
BtnTopVfoATX.SetBounds(0, CenterY + 1,
BtnTopVfoATX.Width, BtnTopVfoATX.Height);
FreqTop := CenterY - (TOP_VFO_FREQ_H div 2);
FreqDispA.SetBounds(FreqDispA.Left, FreqTop,
FreqTextW + VFO_FREQ_LEFT_PAD + VFO_FREQ_RIGHT_PAD, TOP_VFO_FREQ_H);
// Кнопки трансфера: горизонтальный ряд, вертикально по оси
XferX := TopVfoAW + DynGap;
XferY := CenterY - (XFER_BTN_H div 2);
PanelVfoButtons.SetBounds(XferX, XferY, XFER_PANEL_W, XFER_BTN_H);
BtnVfoACopyB.SetBounds(0, 0, XFER_BTN_W, XFER_BTN_H);
BtnVfoSwap.SetBounds(XFER_BTN_W + XFER_BTN_GAP, 0, XFER_BTN_W, XFER_BTN_H);
BtnVfoBCopyA.SetBounds((XFER_BTN_W + XFER_BTN_GAP) * 2, 0, XFER_BTN_W, XFER_BTN_H);
// VFO-B: правый край группы
PanelTopVfoB.SetBounds(TopVfoAW + DynGap + XFER_PANEL_W + DynGap, 0, TopVfoBW, BlockH);
PanelTopVfoB.Visible := True;
PanelVfoB.SetBounds(0, 0, TopVfoBW, BlockH);
BtnTopVfoBSelect.SetBounds(VFO_B_LEFT_PAD, CenterY - BtnTopVfoBSelect.Height - 1,
BtnTopVfoBSelect.Width, BtnTopVfoBSelect.Height);
BtnTopVfoBTX.SetBounds(VFO_B_LEFT_PAD, CenterY + 1,
BtnTopVfoBTX.Width, BtnTopVfoBTX.Height);
FreqTop := CenterY - (TOP_VFO_FREQ_H div 2);
FreqDispB.SetBounds(FreqDispB.Left, FreqTop,
FreqTextW + VFO_FREQ_LEFT_PAD + VFO_FREQ_RIGHT_PAD, TOP_VFO_FREQ_H);
if PanelSep <> nil then
PanelSep.SetBounds(16, PanelToolbar.Height + 30, ClientWidth - 32, 2);
PanelTopVfoGroup.BringToFront;
end;
procedure TMainForm.ResizeSMeter;
// PanelSMeterRight на главной форме (Parent=Self).
// По Y: от самого верха формы до низа PanelSpanButtons (Toolbar+Span = 66px).
var
FW: Integer;
RightW: Integer; // ширина PanelRight
SMW: Integer; // ширина S-метра
SMLeft: Integer;
SMTop: Integer;
SMBot: Integer;
begin
if (PanelSMeterRight = nil) or (PanelToolbar = nil) or
(PanelSpanButtons = nil) or (PanelLeft = nil) then Exit;
LayoutTopVfoBlock;
FW := ClientWidth;
// Ширина зоны спектра (PanelRight)
RightW := FW - PanelLeft.Width;
if RightW < 200 then Exit;
// Ширина S-метра = 32% от PanelRight
SMW := Round(RightW * SMETER_WIDTH_RATIO);
// X: прижат к правому краю формы
SMLeft := FW - SMW - SMETER_MARGIN;
// Y: от верха формы+margin до низа PanelSpanButtons-margin
SMTop := SMETER_MARGIN;
SMBot := PanelToolbar.Height + 30 - SMETER_MARGIN; // 30 = span row content, ends before separator
PanelSMeterRight.SetBounds(SMLeft, SMTop, SMW, SMBot - SMTop);
PanelSMeterRight.BringToFront;
end;
procedure TMainForm.ResizeSpectrumPanels;
const
SPLITTER_H = 5;
MIN_SH = 60; // минимальная высота спектра
MIN_WH = 40; // минимальная высота водопада
var
RW, RH, SH, WH, TopOff: Integer;
AvailH: Integer;
RULER_H: Integer;
begin
RULER_H := MulDiv(18, Screen.PixelsPerInch, 96);
if PanelRight = nil then Exit;
if Assigned(FSpecView) then SyncSpecViewFreq;
RW := PanelRight.ClientWidth;
RH := PanelRight.ClientHeight;
TopOff := PanelSpanButtons.Height;
// S-метр позиционируется отдельно в ResizeSMeter
ResizeSMeter;
// ---- Оба скрыты ----
if (not FShowSpectrum) and (not FShowWaterfall) then
begin
PbSpectrum.Visible := False;
PbRuler.Visible := False;
PanelSplitter.Visible := False;
PbWaterfall.Visible := False;
FSpectrumWidth := RW;
FSpectrumHeight := 0;
FWaterfallHeight := 0;
PositionSpanOverlayButtons;
Exit;
end;
// ---- Только спектр (водопад скрыт) ----
if FShowSpectrum and (not FShowWaterfall) then
begin
AvailH := RH - TopOff - RULER_H;
if AvailH < MIN_SH then AvailH := MIN_SH;
SH := AvailH;
WH := 0;
PbSpectrum.SetBounds(0, TopOff, RW, SH);
PbSpectrum.Visible := True;
PbRuler.SetBounds(0, TopOff + SH, RW, RULER_H);
PbRuler.Visible := True;
PanelSplitter.Visible := False;
PbWaterfall.Visible := False;
PositionSpanOverlayButtons;
FSpectrumWidth := RW;
FSpectrumHeight := SH;
FWaterfallHeight := 0;
if RW > 0 then
begin
FSpecView.SetSpectrumBitmapSize(RW, SH);
FSpecView.SetWaterfallBitmapSize(1, 1);
FSpecView.SetRulerSize(RW, RULER_H);
FSpecView.DrawSpectrum;
PbSpectrum.Invalidate;
PbRuler.Invalidate;
end;
Exit;
end;
// ---- Только водопад (спектр скрыт): линейка под водопадом ----
if (not FShowSpectrum) and FShowWaterfall then
begin
AvailH := RH - TopOff - RULER_H;
if AvailH < MIN_WH then AvailH := MIN_WH;
SH := 0;
WH := AvailH;
PbSpectrum.Visible := False;
PanelSplitter.Visible := False;
PbWaterfall.SetBounds(0, TopOff, RW, WH);
PbWaterfall.Visible := True;
PbRuler.SetBounds(0, TopOff + WH, RW, RULER_H);
PbRuler.Visible := True;
PositionSpanOverlayButtons;
FSpectrumWidth := RW;
FSpectrumHeight := 0;
FWaterfallHeight := WH;
if RW > 0 then
begin
FSpecView.SetSpectrumBitmapSize(1, 1);
FSpecView.SetWaterfallBitmapSize(RW, WH);
FSpecView.SetRulerSize(RW, RULER_H);
FSpecView.DrawWaterfall;
PbWaterfall.Invalidate;
PbRuler.Invalidate;
end;
Exit;
end;
// ---- Оба видимы: стандартный режим со сплиттером ----
PbSpectrum.Visible := True;
PbRuler.Visible := True;
PanelSplitter.Visible := True;
PbWaterfall.Visible := True;
AvailH := RH - TopOff - RULER_H - SPLITTER_H;
if AvailH < (MIN_SH + MIN_WH) then Exit;
// Ограничиваем соотношение, чтобы каждая зона имела минимальный размер
if FSplitterRatio < MIN_SH / AvailH then
FSplitterRatio := MIN_SH / AvailH;
if FSplitterRatio > 1.0 - MIN_WH / AvailH then
FSplitterRatio := 1.0 - MIN_WH / AvailH;
SH := Round(AvailH * FSplitterRatio);
WH := AvailH - SH;
if WH < MIN_WH then WH := MIN_WH;
// Спектр
PbSpectrum.SetBounds(0, TopOff, RW, SH);
// Линейка частот — всегда прижата к низу спектра
PbRuler.SetBounds(0, TopOff + SH, RW, RULER_H);
// Сплиттер — между линейкой и водопадом
PanelSplitter.SetBounds(0, TopOff + SH + RULER_H, RW, SPLITTER_H);
// Водопад — под сплиттером
PbWaterfall.SetBounds(0, TopOff + SH + RULER_H + SPLITTER_H, RW, WH);
PositionSpanOverlayButtons;
// Обновляем переменные размеров
FSpectrumWidth := RW;
FSpectrumHeight := SH;
FWaterfallHeight := WH;
// Пересоздаём bitmap точно под новый размер
if RW > 0 then
begin
if SH > 0 then FSpecView.SetSpectrumBitmapSize(RW, SH);
if WH > 0 then FSpecView.SetWaterfallBitmapSize(RW, WH);
FSpecView.SetRulerSize(RW, RULER_H);
FSpecView.DrawSpectrum;
PbSpectrum.Invalidate;
if PbRuler <> nil then PbRuler.Invalidate;
PbWaterfall.Invalidate;
end;
end;
// ===========================================================================
// Dark Theme
// ===========================================================================
procedure TMainForm.ApplyDarkTheme;
var T: TAppTheme; i: Integer;
procedure DP(P: TPanel);
begin P.Color := T.Panel; P.Font.Color := T.Text; end;
procedure DL(L: TLabel; Dim: Boolean = True);
begin if Dim then L.Font.Color := T.TextDim else L.Font.Color := T.Text; end;
procedure DS(S: TFlatSlider);
begin
S.SetThemeColors(T.SliderBG, T.SliderTrackEmpty, T.SliderTrackFill,
T.SliderThumbNorm, T.SliderThumbHot, T.SliderThumbDrag, T.SliderThumbBdr);
end;
begin
if FLightTheme then T := LightTheme else T := DarkTheme;
Color := T.BG; Font.Color := T.Text;
DP(PanelToolbar); DP(PanelLeft);
DP(PanelTopVfoGroup); DP(PanelTopVfoA); DP(PanelTopVfoB);
DP(PanelVfoA); DP(PanelVfoB); DP(PanelVfoButtons);
DP(PanelBands); DP(PanelMode); DP(PanelFilter);
if PanelFMSQ <> nil then DP(PanelFMSQ);
if PanelFMCTCSS <> nil then DP(PanelFMCTCSS);
if PanelFMCTCSSPop <> nil then DP(PanelFMCTCSSPop);
if PanelFMStep <> nil then DP(PanelFMStep);
if PanelFMStepPop <> nil then DP(PanelFMStepPop);
DP(PanelRX); DP(PanelTX);
DP(PanelRight); DP(PanelSpanButtons);
PbSpectrum.Color := T.BG;
PbWaterfall.Color := T.BG;
if PanelSMeterRight <> nil then PanelSMeterRight.Color := T.Panel;
if PbSMeterRight <> nil then PbSMeterRight.Color := T.Panel;
// Метки
DL(LblAGCTop, False);
DL(LblDrv); DL(LblSpan);
if LblFMSQ <> nil then DL(LblFMSQ, False);
// Кнопки тулбара
BtnDiscover.ClrNorm := T.TbDiscoverNorm;
BtnDiscover.ClrHot := T.TbDiscoverHot;
BtnDiscover.ClrActive := T.TbDiscoverHot;
BtnDiscover.ClrBorder := T.TbDiscoverBorder;
BtnDiscover.ClrText := T.TbDiscoverText;
BtnDiscover.ClrTextAct := T.TbDiscoverText;
BtnDiscover.Invalidate;
BtnStartStop.ClrNorm := T.TbStartNorm;
BtnStartStop.ClrBorder := T.TbStartBorder;
BtnStartStop.ClrText := T.TbStartText;
BtnStartStop.Invalidate;
// Обычные кнопки — все массивы
for i := 0 to BAND_COUNT-1 do StyleButton(BtnBand[i], BtnBand[i].Active);
for i := 0 to MODE_COUNT-1 do StyleButton(BtnMode[i], BtnMode[i].Active);
for i := 0 to FILT_COUNT-1 do StyleButton(BtnFilter[i], BtnFilter[i].Active);
for i := 0 to 4 do StyleButton(BtnAGCMode[i],BtnAGCMode[i].Active);
StyleButton(BtnCTun, BtnCTun.Active);
StyleButton(BtnDUP, BtnDUP.Active);
if BtnFMSQ <> nil then StyleButton(BtnFMSQ, BtnFMSQ.Active);
if TrkFMSQ <> nil then DS(TrkFMSQ);
if BtnFMCTCSS <> nil then StyleButton(BtnFMCTCSS, BtnFMCTCSS.Active);
if BtnFMCTCSSTone <> nil then StyleButton(BtnFMCTCSSTone, False);
for i := 0 to CTCSS_COUNT-1 do
if FBtnCTCSSTones[i] <> nil then StyleButton(FBtnCTCSSTones[i], FBtnCTCSSTones[i].Active);
StyleButton(BtnNR, BtnNR.Active);
StyleButton(BtnNB, BtnNB.Active);
StyleButton(BtnSNB, BtnSNB.Active);
StyleButton(BtnANF, BtnANF.Active);
StyleButton(BtnMute, BtnMute.Active);
StyleButton(BtnMOX, BtnMOX.Active);
StyleButton(BtnTUN, BtnTUN.Active);
// Span / VFO-copy / HidePanel
StyleSpanButton(BtnSpan48k, BtnSpan48k.Active);
StyleSpanButton(BtnSpan96k, BtnSpan96k.Active);
StyleSpanButton(BtnSpan192k, BtnSpan192k.Active);
StyleSpanButton(BtnSpan384k, BtnSpan384k.Active);
StyleSpanButton(BtnSpan768k, BtnSpan768k.Active);
StyleSpanButton(BtnSpan1536k, BtnSpan1536k.Active);
StyleSpanButton(BtnVfoACopyB, False);
StyleSpanButton(BtnVfoSwap, False);
StyleSpanButton(BtnVfoBCopyA, False);
if BtnHidePanel <> nil then StyleSpanButton(BtnHidePanel, False);
// Ползунки
DS(TrkAGC); DS(TrkVolume); DS(TrkDrive);
// Спектр
FSpecView.SetTheme(T);
// VFO display — вызывает UpdateVfoDisplay, который использует тему
UpdateVfoDisplay;
end;
procedure TMainForm.SetLightTheme(V: Boolean);
begin
if FLightTheme = V then Exit;
FLightTheme := V;
ApplyDarkTheme;
FSettings.SaveTheme(V);
FSettings.Save;
if (FSettingsForm <> nil) and TSettingsForm(FSettingsForm).Visible then
TSettingsForm(FSettingsForm).LoadLightTheme(V);
end;
procedure TMainForm.StyleButton(B: TFlatButton; Active: Boolean);
var T: TAppTheme;
begin
if FLightTheme then T := LightTheme else T := DarkTheme;
B.Active := Active;
B.ClrNorm := T.BtnNorm;
B.ClrActive := T.BtnActive;
B.ClrHot := T.BtnHot;
if Active then B.ClrBorder := T.BtnBorderActive
else B.ClrBorder := T.BtnBorderNorm;
B.ClrText := T.BtnText;
B.ClrTextAct := T.BtnTextActive;
B.Font.Name := 'Courier New';
B.Font.Size := 8;
end;
procedure TMainForm.StyleSpanButton(B: TFlatButton; Active: Boolean);
var T: TAppTheme;
begin
if FLightTheme then T := LightTheme else T := DarkTheme;
B.Active := Active;
B.ClrNorm := T.SpanNorm;
B.ClrActive := T.SpanActive;
B.ClrHot := T.SpanActive;
B.ClrBorder := T.SpanBorder;
B.ClrText := T.SpanText;
B.ClrTextAct := T.SpanText;
B.Font.Name := 'Courier New';
B.Font.Size := 7;
end;
procedure TMainForm.PositionSpanOverlayButtons;
const
BTN_TOP = 6;
GAP = 3;
W48 = 40;
W96 = 40;
W192 = 44;
W384 = 44;
W768 = 44;
W1536 = 50;
HBTN = 18;
var
X, NeedW, BaseX, BaseY: Integer;
begin
if (PbSpectrum = nil) or (BtnSpan48k = nil) then Exit;
NeedW := W48 + W96 + W192 + W384 + W768 + W1536 + GAP * 5 + 8;
if PbSpectrum.Width < NeedW then
begin
BtnSpan48k.Visible := False;
BtnSpan96k.Visible := False;
BtnSpan192k.Visible := False;
BtnSpan384k.Visible := False;
BtnSpan768k.Visible := False;
BtnSpan1536k.Visible := False;
Exit;
end;
BaseX := PbSpectrum.Left;
BaseY := PbSpectrum.Top;
X := BaseX + 56;
BtnSpan48k.SetBounds(X, BaseY + BTN_TOP, W48, HBTN); Inc(X, W48 + GAP);
BtnSpan96k.SetBounds(X, BaseY + BTN_TOP, W96, HBTN); Inc(X, W96 + GAP);
BtnSpan192k.SetBounds(X, BaseY + BTN_TOP, W192, HBTN); Inc(X, W192 + GAP);
BtnSpan384k.SetBounds(X, BaseY + BTN_TOP, W384, HBTN); Inc(X, W384 + GAP);
BtnSpan768k.SetBounds(X, BaseY + BTN_TOP, W768, HBTN); Inc(X, W768 + GAP);
BtnSpan1536k.SetBounds(X, BaseY + BTN_TOP, W1536, HBTN);
if BtnHidePanel <> nil then
BtnHidePanel.SetBounds(BaseX, BaseY + BTN_TOP, 28, HBTN);
BtnSpan48k.Visible := True;
BtnSpan96k.Visible := True;
BtnSpan192k.Visible := True;
BtnSpan384k.Visible := True;
BtnSpan768k.Visible := True;
BtnSpan1536k.Visible := True;
BtnSpan48k.BringToFront;
BtnSpan96k.BringToFront;
BtnSpan192k.BringToFront;
BtnSpan384k.BringToFront;
BtnSpan768k.BringToFront;
BtnSpan1536k.BringToFront;
if BtnHidePanel <> nil then BtnHidePanel.BringToFront;
end;
// ===========================================================================
// VFO
// ===========================================================================
function TMainForm.FormatFreq(Hz: Double): string;
var
Mhz: Int64;
KHz, Rest: Integer;
begin
Mhz := Trunc(Hz / 1000000);
KHz := Trunc((Hz - Mhz * 1000000) / 1000);
Rest := Trunc(Hz) mod 1000;
Result := Format('%d.%3.3d.%3.3d', [Mhz, KHz, Rest]);
end;
procedure TMainForm.UpdateVfoDisplay;
const
TOP_VFO_FREQ_H = 47;
var T: TAppTheme;
begin
if FLightTheme then T := LightTheme else T := DarkTheme;
FreqDispA.Frequency := Round(FVfoA);
FreqDispB.Frequency := Round(FVfoB);
FreqDispA.FontSize := TOP_VFO_FONT;
FreqDispB.FontSize := TOP_VFO_FONT;
FreqDispA.Height := TOP_VFO_FREQ_H;
FreqDispB.Height := TOP_VFO_FREQ_H;
FreqDispA.ColorHover := T.FreqHover;
FreqDispA.ColorHoverBG := T.FreqHoverBG;
FreqDispA.ColorDim := T.FreqDimDot;
FreqDispB.ColorHover := T.FreqHover;
FreqDispB.ColorHoverBG := T.FreqHoverBG;
FreqDispB.ColorDim := T.FreqDimDot;
if FActiveVfo = 0 then
begin
FreqDispA.ColorNormal := T.FreqActive;
FreqDispB.ColorNormal := T.FreqDim;
LblVfoALabel.Font.Color := T.FreqActive;
LblVfoBLabel.Font.Color := T.TextDim;
if BtnTopVfoASelect <> nil then begin
BtnTopVfoASelect.ClrNorm := T.VfoSelActNorm;
BtnTopVfoASelect.ClrActive := T.VfoSelActNorm;
BtnTopVfoASelect.ClrHot := T.VfoSelActNorm;
BtnTopVfoASelect.ClrBorder := T.VfoSelActBorder;
BtnTopVfoASelect.ClrText := T.VfoSelActText;
BtnTopVfoASelect.ClrTextAct := T.VfoSelActTextAct;
BtnTopVfoASelect.Active := True;
BtnTopVfoASelect.Invalidate;
end;
if BtnTopVfoBSelect <> nil then begin
BtnTopVfoBSelect.ClrNorm := T.VfoSelInaNorm;
BtnTopVfoBSelect.ClrActive := T.VfoSelInaNorm;
BtnTopVfoBSelect.ClrHot := T.VfoSelInaNorm;
BtnTopVfoBSelect.ClrBorder := T.VfoSelInaBorder;
BtnTopVfoBSelect.ClrText := T.VfoSelInaText;
BtnTopVfoBSelect.ClrTextAct := T.VfoSelInaTextAct;
BtnTopVfoBSelect.Active := False;
BtnTopVfoBSelect.Invalidate;
end;
end else begin
FreqDispA.ColorNormal := T.FreqDim;
FreqDispB.ColorNormal := T.FreqActive;
LblVfoALabel.Font.Color := T.TextDim;
LblVfoBLabel.Font.Color := T.FreqActive;
if BtnTopVfoASelect <> nil then begin
BtnTopVfoASelect.ClrNorm := T.VfoSelInaNorm;
BtnTopVfoASelect.ClrActive := T.VfoSelInaNorm;
BtnTopVfoASelect.ClrHot := T.VfoSelInaNorm;
BtnTopVfoASelect.ClrBorder := T.VfoSelInaBorder;
BtnTopVfoASelect.ClrText := T.VfoSelInaText;
BtnTopVfoASelect.ClrTextAct := T.VfoSelInaTextAct;
BtnTopVfoASelect.Active := False;
BtnTopVfoASelect.Invalidate;
end;
if BtnTopVfoBSelect <> nil then begin
BtnTopVfoBSelect.ClrNorm := T.VfoSelActNorm;
BtnTopVfoBSelect.ClrActive := T.VfoSelActNorm;
BtnTopVfoBSelect.ClrHot := T.VfoSelActNorm;
BtnTopVfoBSelect.ClrBorder := T.VfoSelActBorder;
BtnTopVfoBSelect.ClrText := T.VfoSelActText;
BtnTopVfoBSelect.ClrTextAct := T.VfoSelActTextAct;
BtnTopVfoBSelect.Active := True;
BtnTopVfoBSelect.Invalidate;
end;
end;
if BtnTopVfoATX <> nil then
begin
if not FSplitTxB then
begin
BtnTopVfoATX.ClrNorm := T.VfoTxActNorm;
BtnTopVfoATX.ClrHot := T.VfoTxActNorm;
BtnTopVfoATX.ClrActive := T.VfoTxActNorm;
BtnTopVfoATX.ClrBorder := T.VfoTxActBorder;
BtnTopVfoATX.ClrText := T.VfoTxActText;
BtnTopVfoATX.ClrTextAct := T.VfoTxActText;
BtnTopVfoATX.Active := True;
end
else
begin
BtnTopVfoATX.ClrNorm := T.VfoTxInaNorm;
BtnTopVfoATX.ClrHot := T.VfoTxInaNorm;
BtnTopVfoATX.ClrActive := T.VfoTxInaNorm;
BtnTopVfoATX.ClrBorder := T.VfoTxInaBorder;
BtnTopVfoATX.ClrText := T.VfoTxInaText;
BtnTopVfoATX.ClrTextAct := T.VfoTxInaText;
BtnTopVfoATX.Active := False;
end;
BtnTopVfoATX.Invalidate;
end;
if BtnTopVfoBTX <> nil then
begin
if FSplitTxB then
begin
BtnTopVfoBTX.ClrNorm := T.VfoTxActNorm;
BtnTopVfoBTX.ClrHot := T.VfoTxActNorm;
BtnTopVfoBTX.ClrActive := T.VfoTxActNorm;
BtnTopVfoBTX.ClrBorder := T.VfoTxActBorder;
BtnTopVfoBTX.ClrText := T.VfoTxActText;
BtnTopVfoBTX.ClrTextAct := T.VfoTxActText;
BtnTopVfoBTX.Active := True;
end
else
begin
BtnTopVfoBTX.ClrNorm := T.VfoTxInaNorm;
BtnTopVfoBTX.ClrHot := T.VfoTxInaNorm;
BtnTopVfoBTX.ClrActive := T.VfoTxInaNorm;
BtnTopVfoBTX.ClrBorder := T.VfoTxInaBorder;
BtnTopVfoBTX.ClrText := T.VfoTxInaText;
BtnTopVfoBTX.ClrTextAct := T.VfoTxInaText;
BtnTopVfoBTX.Active := False;
end;
BtnTopVfoBTX.Invalidate;
end;
FreqDispA.Invalidate;
FreqDispB.Invalidate;
LblVfoALabel.Invalidate;
LblVfoBLabel.Invalidate;
end;
// ===========================================================================
// Rendering helpers — delegated to TSpectrumView
// ===========================================================================
procedure TMainForm.InvalidateGridCache;
begin
FSpecView.InvalidateGridCache;
end;
procedure TMainForm.SyncSpecViewFreq;
begin
FSpecView.VfoA := FVfoA;
FSpecView.VfoB := FVfoB;
FSpecView.ActiveVfo := FActiveVfo;
FSpecView.CenterFreq := FCenterFreq;
FSpecView.SpanHz := FSpanHz;
FSpecView.Mode := FMode;
FSpecView.FilterBW := FFilterBW;
if FMode = MODE_FM then
FSpecView.FMGridStepHz := FM_STEP_HZ[FFMStepIdx]
else
FSpecView.FMGridStepHz := 0;
// TX overlay следует за активной TX-частотой (учитывает split). При CTUN
// спектр TX будет отрисован на VFO, а не в центре дисплея.
FSpecView.TXFreq := ActiveTXFreqHz;
// Какой VFO привязан к TX (для split-полос фильтра): 1=B при FSplitTxB, иначе FActiveVfo.
if FSplitTxB then FSpecView.TXVfoIndex := 1
else FSpecView.TXVfoIndex := FActiveVfo;
end;
// ===========================================================================
// Timers
// ===========================================================================
procedure TMainForm.MeterTimerTick(Sender: TObject);
const
AVG_ALPHA = 0.25; // EMA для сглаженного среднего уровня
ZONE_DB = 6.0; // полуширина зоны вокруг среднего (дБ)
// Адаптивный alpha: базовый + ускорение при большой разнице
PEAK_BASE = 0.06; // базовая скорость пика
MIN_BASE = 0.06; // базовая скорость минимума
ACCEL_FACTOR = 0.018; // ускорение на каждый dB разницы
MAX_ALPHA = 0.85; // максимальная скорость (не мгновенно)
var
PeakTarget, MinTarget: Double;
PeakDiff, MinDiff: Double;
PeakAlpha, MinAlpha: Double;
begin
if not FRunning then
begin
if PbSMeterRight <> nil then PbSMeterRight.Invalidate;
Exit;
end;
// Сглаженное среднее (EMA)
FSMeterAvg := FSMeterAvg * (1.0 - AVG_ALPHA) + FLastSMeter * AVG_ALPHA;
// Цели для Peak и Min
PeakTarget := Max(FLastSMeter, FSMeterAvg + ZONE_DB);
MinTarget := Min(FLastSMeter, FSMeterAvg - ZONE_DB);
// Адаптивный alpha: чем дальше от цели — тем быстрее догоняем
PeakDiff := Abs(PeakTarget - FSMeterPeak);
MinDiff := Abs(MinTarget - FSMeterMin);
PeakAlpha := Min(MAX_ALPHA, PEAK_BASE + PeakDiff * ACCEL_FACTOR);
MinAlpha := Min(MAX_ALPHA, MIN_BASE + MinDiff * ACCEL_FACTOR);
FSMeterPeak := FSMeterPeak + PeakAlpha * (PeakTarget - FSMeterPeak);
FSMeterMin := FSMeterMin + MinAlpha * (MinTarget - FSMeterMin);
FSpecView.LastSMeter := FLastSMeter;
FSpecView.SMeterPeak := FSMeterPeak;
FSpecView.SMeterMin := FSMeterMin;
FSpecView.LastFwdW := FLastFwdW;
FSpecView.LastSWR := FLastSWR;
FSpecView.Transmitting := FTransmitting;
if PbSMeterRight <> nil then PbSMeterRight.Invalidate;
// PWR/SWR метки и бары обновляем здесь (10 Гц), а не в DoUpdateStatus —
// HP Status пакеты идут ~150 раз/с, при таком rate цифры дёргаются.
UpdateTXMeters;
// Диагностика TX mic-пути — в отдельной панели [5], чтобы её не перетирали
// другие источники (DDC seq, audio-логи и т.п.).
if FWDSPReady then
begin
if FTransmitting then
begin
if FTXSettings.MicSource = 1 then
StatusBar1.Panels[5].Text :=
Format('TX sc: avail=%d ring=%d',
[FAudioIn.Available, FDSPEngine.TXMicRingAvail])
else
StatusBar1.Panels[5].Text :=
Format('TX hw: ring=%d', [FDSPEngine.TXMicRingAvail]);
end
else
StatusBar1.Panels[5].Text := 'TX: idle';
end;
end;
procedure TMainForm.SpectrumTimerTick(Sender: TObject);
var
NowTick: QWord;
ElapsedTick: QWord;
begin
// Синхронизируем размеры если размер панели изменился (после ресайза).
// Важно: проверяем только видимые панели — когда спектр скрыт,
// PbSpectrum.Height может быть ненулевым при FSpectrumHeight=0,
// что вызвало бы сброс буфера водопада на каждом тике.
if (FSpectrumWidth = 0) or
(FShowSpectrum and ((PbSpectrum.Width <> FSpectrumWidth) or
(PbSpectrum.Height <> FSpectrumHeight))) or
(FShowWaterfall and not FShowSpectrum and
((PbWaterfall.Width <> FSpectrumWidth) or
(PbWaterfall.Height <> FWaterfallHeight))) then
ResizeSpectrumPanels;
ResizeSMeter;
if FSpectrumWidth <= 0 then Exit;
SyncSpecViewFreq;
// Проверка таймаута — трансивер не отвечает
if FRunning and FNetwork.Running then
begin
NowTick := GetTickCount64;
ElapsedTick := NowTick - FRXStartTime;
// Ждём первый пакет 5 секунд после старта
if (FRXLastPktTime = 0) and (ElapsedTick > 5000) then
begin
BtnStartStop.Caption := 'START';
StyleButton(BtnStartStop, False);
FRunning := False;
FNetwork.SetRunAndFreq(False, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), 0);
StatusBar1.Panels[4].Text := 'ТРАНСИВЕР НЕДОСТУПЕН';
BtnMOX.Enabled := False;
Exit;
end;
// После первого пакета — следим чтобы поток не прерывался более 3 сек
if (FRXLastPktTime > 0) and ((NowTick - FRXLastPktTime) > 3000) then
begin
BtnStartStop.Caption := 'START';
StyleButton(BtnStartStop, False);
FRunning := False;
FNetwork.SetRunAndFreq(False, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), 0);
StatusBar1.Panels[4].Text := 'ПОТЕРЯ СВЯЗИ С ТРАНСИВЕРОМ';
BtnMOX.Enabled := False;
Exit;
end;
end;
if FRunning then
begin
// Обновляем StatusBar из таймера — без Synchronize в сетевом потоке
StatusBar1.Panels[3].Text :=
Format('RX DDC%d | seq=%d | pkts=%d',
[FLastDDCIndex, FLastDDCSeq, FRXPacketCount]);
if FWDSPReady then
begin
FLastSMeter := FDSPEngine.GetSMeterDBm;
FDSPEngine.SetSpectrumWidth(FSpectrumWidth);
FDSPEngine.UpdateAGCLines(FSpectrumWidth);
FSpecView.AGCThresh := FDSPEngine.AGCThresh;
FSpecView.AGCHangLevel := FDSPEngine.AGCHangLevel;
end;
FSpecView.WDSPReady := FWDSPReady;
// Обновляем оверлей если видим
if Assigned(FVfoOverlay) and FVfoOverlay.Visible then
begin
FVfoOverlay.UpdateSMeter(FLastSMeter);
FVfoOverlay.UpdateVfo(FVfoA);
PositionVfoOverlay;
end;
// Панорама и водопад теперь рисуются независимо по факту новых кадров.
// Это лучше совпадает с Thetis: маленький FFT даёт новые строки чаще.
if FSpectrumDirty then
begin
FSpecView.DrawSpectrum;
PbSpectrum.Invalidate;
if FSpecView.NeedsRulerRedraw then
PbRuler.Invalidate;
FSpectrumDirty := False;
end;
if FWaterfallDirty then
begin
FSpecView.DrawWaterfall;
PbWaterfall.Invalidate;
FWaterfallDirty := False;
end;
end
else
begin
// Не подключены — показываем чистый экран (без анимации)
if not FNetwork.Connected then
begin
// При первом старте показываем демо-спектр
if FSpecView.SpectrumBitmapWidth <> FSpectrumWidth then
begin
FSpecView.SetSpectrumBitmapSize(FSpectrumWidth, FSpectrumHeight);
FSpecView.ResetSpectrumBuf;
FSpecView.DrawSpectrum;
PbSpectrum.Invalidate;
end;
end
else
begin
// Подключены, но не запущены — статичный спектр (шум)
FSpecView.DrawSpectrum;
PbSpectrum.Invalidate;
FSpectrumDirty := False;
end;
end;
// Если не в режиме RUN, но есть "грязный" кадр (drag/marker/UI), перерисуем.
if (FSpectrumDirty or FWaterfallDirty) and (not FRunning) then
begin
if FSpectrumDirty then
FSpecView.DrawSpectrum;
if FWaterfallDirty then
FSpecView.DrawWaterfall;
if FSpectrumDirty then
PbSpectrum.Invalidate;
if FSpecView.NeedsRulerRedraw then
PbRuler.Invalidate;
if FWaterfallDirty then
PbWaterfall.Invalidate;
FWaterfallDirty := False;
FSpectrumDirty := False;
end;
// Пушим текущее состояние в веб-клиенты (если есть)
if Assigned(FWebServer) then
FWebServer.PushSpectrum(
FSpectrumBuf, 1024,
FWaterfallBuf,
FLastSMeter,
FVfoA, FMode, FFilterBW, FAGCMode, FAGCTop,
FSpanHz, FVolume,
FWfAGCEnabled, FWfNFEnabled,
FCurrentBand,
FRunning and FNetwork.Connected,
FRunning, FMuted, FCTun,
BtnNR.Tag, BtnNB.Tag, BtnSNB.Tag <> 0, BtnANF.Tag <> 0,
FCenterFreq, FFilter,
FVfoB, FActiveVfo,
FTransmitting, TrkDrive.Position,
FAtten, FTuning, FDisplayDuplex,
FLastFwdW, FLastSWR, FPAMaxPower);
end;
// ===========================================================================
// Network callbacks → sync helpers
// ===========================================================================
procedure TMainForm.OnDeviceFound(const Dev: THPSDRDevice);
var
BoardName, Entry: string;
Sync: TDeviceFoundSync;
M: TThreadMethod;
begin
BoardName := BoardTypeName(Dev.BoardType);
Entry := Format('%s %s FW:%d DDC:%d',
[Dev.IPAddress, BoardName,
Dev.FirmwareVersion, Dev.NumDDCs]);
Sync := TDeviceFoundSync.Create(Self, Dev, Entry);
try
M := Sync.Execute;
TThread.Synchronize(nil, M);
finally
Sync.Free;
end;
end;
procedure TMainForm.DoAddDevice(const Dev: THPSDRDevice; const Entry: string);
var
Idx: Integer;
begin
Idx := Length(FDevices);
SetLength(FDevices, Idx + 1);
FDevices[Idx].Dev := Dev;
FDevices[Idx].Display := Entry;
Inc(FDeviceCount);
if Assigned(FDeviceDialog) then
FDeviceDialog.AddDiscovered(Dev.IPAddress, Entry, Dev.BoardType);
StatusBar1.Panels[4].Text := Format('Found: %s', [Dev.IPAddress]);
end;
procedure TMainForm.OnHPStatusCB(const Status: THighPriorityStatus);
const
// Порог fwd-мощности (W), ниже которого SWR не вычисляем — там всё ADC-шум.
SWR_MIN_FWD_W = 1.0;
// Верхняя граница SWR (выше — это всё равно «беда»).
SWR_CAP = 9.9;
var
ExcPwr, FwdPwr, RevPwr: Word;
SupplyV, FwdW, SWRV, Rho: Double;
IsTx: Boolean;
Sync: TStatusUISync;
M: TThreadMethod;
begin
ExcPwr := (Status.ExciterPwr0Hi shl 8) or Status.ExciterPwr0Lo;
FwdPwr := (Status.FwdPwrAlex0Hi shl 8) or Status.FwdPwrAlex0Lo;
RevPwr := (Status.RevPwrAlex0Hi shl 8) or Status.RevPwrAlex0Lo;
// Board 5 (Orion MkII) measures supply voltage via UserADC0 (bytes 57-58),
// not the supply_volts field (bytes 49-50) — matching Thetis behaviour.
if FNetwork.Device.BoardType = 5 then
SupplyV := ADCToSupplyVolts(
(Status.UserADC0Hi shl 8) or Status.UserADC0Lo,
FNetwork.Device.BoardType)
else
SupplyV := ADCToSupplyVolts(
(Status.SupplyVoltsHi shl 8) or Status.SupplyVoltsLo,
FNetwork.Device.BoardType);
// FLastSMeter обновляется только из WDSP (GetSMeterDBm) в SpectrumTimerTick —
// ExciterPwr это мощность TX, не уровень принятого сигнала.
FwdW := ADCToWatts100(FwdPwr);
// FTransmitting — наш программный флаг (MOX/TUN/HW-PTT через ApplyMOX).
// HPS_PTT-бит в StatusBits — это вход аппаратного PTT (footswitch/mic),
// а не «трансивер сейчас передаёт», поэтому только на него полагаться нельзя.
IsTx := FTransmitting or ((Status.StatusBits and HPS_PTT) <> 0);
// SWR: правильная формула (1+ρ)/(1-ρ), считаем только когда есть значимая
// прямая мощность — иначе ADC-шум RevPwr даёт случайные «прыжки» SWR.
// Не на передаче — SWR не показываем (фиксируем 1.0), Fwd принудительно 0.
if IsTx and (FwdW >= SWR_MIN_FWD_W) and (FwdPwr > 0) then
begin
// ρ = sqrt(Pr/Pf). Поскольку P=V²/R и V пропорционально ADC, то
// sqrt(Pr/Pf) = adc_r/adc_f напрямую — лишнего sqrt не нужно.
Rho := RevPwr / FwdPwr;
if Rho >= 0.99 then
SWRV := SWR_CAP
else
begin
SWRV := (1.0 + Rho) / (1.0 - Rho);
if SWRV > SWR_CAP then SWRV := SWR_CAP;
if SWRV < 1.0 then SWRV := 1.0;
end;
end else
SWRV := 1.0;
if not IsTx then FwdW := 0;
Sync := TStatusUISync.Create(Self, FwdW, SWRV, SupplyV,
(Status.StatusBits and HPS_PLL_LOCKED) <> 0,
(Status.StatusBits and HPS_PTT) <> 0);
try
M := Sync.Execute;
TThread.Synchronize(nil, M);
finally
Sync.Free;
end;
end;
procedure TMainForm.DoUpdateStatus(FwdW, SWRV, SupplyV: Double; PLLLock, HWPTT: Boolean);
const
// Thetis/piHPSDR PEP-style баллистика для FwdW и SWR: атака мгновенная
// (peak-hold), спад медленный — стрелка «висит» на пике и плавно опускается.
// Это маскирует огибающую SSB-голоса, которая на ADC прыгает на 10-20 дБ
// внутри слога. На rate ~150 пакетов/с эти α дают τ_decay ≈ 350 мс / 500 мс.
DECAY_FWD = 0.02;
DECAY_SWR = 0.015;
// Напряжение питания меняется медленно — обычное симметричное EMA.
ALPHA_SUPPLY = 0.05;
begin
// Запоминаем значения; вывод в UI делает MeterTimerTick на 10 Гц.
// Сама обработка здесь идёт на UI-потоке через Synchronize.
// FwdW=0 / SWRV=1 — это «не на передаче»: снапим без decay,
// чтобы метры моментально падали при отпускании PTT.
if FwdW <= 0 then
FLastFwdW := 0
else if FwdW > FLastFwdW then
FLastFwdW := FwdW // attack: instant
else
FLastFwdW := DECAY_FWD * FwdW + (1.0 - DECAY_FWD) * FLastFwdW;
if SWRV <= 1.0 then
FLastSWR := 1.0
else if SWRV > FLastSWR then
FLastSWR := SWRV // attack: instant
else
FLastSWR := DECAY_SWR * SWRV + (1.0 - DECAY_SWR) * FLastSWR;
if SupplyV >= 0 then
begin
if FLastSupplyV < 0 then
FLastSupplyV := SupplyV // первое значение — без сглаживания
else
FLastSupplyV := ALPHA_SUPPLY * SupplyV + (1.0 - ALPHA_SUPPLY) * FLastSupplyV;
end;
FLastPLLLock := PLLLock;
// Hardware PTT (foot switch / mic PTT) — обнаружение фронта (нужно делать
// на rate HP Status, а не таймера, чтобы не пропустить короткое нажатие).
if HWPTT <> FHWPTTActive then
begin
FHWPTTActive := HWPTT;
// Hardware PTT игнорируется во время TUN; иначе включает/выключает передачу
// только если кнопка MOX не нажата (приоритет за UI).
if (not FTuning) and (not BtnMOX.Active) then
ApplyMOX(HWPTT);
end;
end;
procedure TMainForm.UpdateTXMeters;
// Вызывается из MeterTimerTick (10 Гц). Использует FLastFwdW/FLastSWR/
// FLastSupplyV/FLastPLLLock — они обновляются на rate HP Status (~150/с)
// в DoUpdateStatus.
var
PLLStr: string;
begin
if FLastPLLLock then PLLStr := 'PLL OK' else PLLStr := 'PLL?';
if FLastSupplyV >= 0 then
StatusBar1.Panels[2].Text := Format('Supply: %.1fV | %s', [FLastSupplyV, PLLStr])
else
StatusBar1.Panels[2].Text := PLLStr;
end;
procedure TMainForm.OnDDCIQCB(DDCIndex: Integer; const Data: TDDCIQPacket);
var
SamplesPerFrame: Integer;
begin
// Вызывается из сетевого потока — UI не трогаем напрямую
// Счётчик принятых пакетов
Inc(FRXPacketCount);
FRXLastPktTime := GetTickCount64; // фиксируем время последнего пакета
// 1. Подаём IQ данные в DSP — только с активного DDC
if (DDCIndex = FActiveDDC) and FWDSPReady then
begin
SamplesPerFrame := (Integer(Data.SamplesPerFrame[0]) shl 8) or
Integer(Data.SamplesPerFrame[1]);
if SamplesPerFrame <= 0 then SamplesPerFrame := 238; // fallback
FDSPEngine.PushDDCPacket(Data.IQData, 0, SamplesPerFrame);
end;
// 2. Запоминаем последний seq — StatusBar обновит таймер (без Synchronize в горячем пути)
FLastDDCSeq := (LongWord(Data.Seq[0]) shl 24) or (LongWord(Data.Seq[1]) shl 16)
or (LongWord(Data.Seq[2]) shl 8) or LongWord(Data.Seq[3]);
FLastDDCIndex := DDCIndex;
end;
procedure TMainForm.DoUpdateDDCSeq(DDCIdx: Integer; Seq: LongWord);
begin
StatusBar1.Panels[3].Text :=
Format('RX DDC%d | seq=%d | pkts=%d', [DDCIdx, Seq, FRXPacketCount]);
end;
procedure TMainForm.OnMicPacketCB(const Data: TMicDataPacket);
begin
// HW mic-стрим уходит в WDSP только если выбран источник Radio.
// Иначе сэмплы игнорируются (mic берётся со звуковой карты).
if FTransmitting and FWDSPReady and
(FTXSettings.MicSource = Ord(txmsRadio)) then
FDSPEngine.PushTXMicSamples16(Data.Samples, 64);
end;
// ---------------------------------------------------------------------------
// TX settings — применение к WDSP, сборка DUC Specific, sound-card pull
// ---------------------------------------------------------------------------
function TMainForm.BuildMicLineSelectByte: Byte;
// openHPSDR Ethernet Protocol v4.3, DUC Specific byte 50:
// bit 0 — Line in (1 = line-in вместо mic-in)
// bit 1 — Mic Boost (+20 dB)
// bit 2 — 0 = Orion mic PTT enabled, 1 = disabled (инверсия!)
// bit 3 — PTT/bias polarity (не используем)
// bit 4 — Orion mic bias (1 = enable +5V bias)
// bit 5 — Saturn 3.5mm/XLR (не используем)
begin
Result := 0;
if FTXSettings.MicLineIn then Result := Result or $01;
if FTXSettings.MicBoost then Result := Result or $02;
if not FTXSettings.MicPTTEnabled then Result := Result or $04;
if FTXSettings.MicBias then Result := Result or $10;
end;
procedure TMainForm.SendDUCSpecificFromSettings;
var
DUCPkt: TDUCSpecificPacket;
RateKsps: Word;
begin
if not (FNetwork.Connected and FNetwork.Running) then Exit;
FillChar(DUCPkt, SizeOf(DUCPkt), 0);
// DUC0 rate в ksps берём из WDSP-движка (фиксированный TX rate, не RX).
RateKsps := FDSPEngine.TXSampleRate div 1000;
DUCPkt.NumDACs := 1;
DUCPkt.SidetoneLevel := 50;
DUCPkt.SidetoneFreqHi := Hi(600);
DUCPkt.SidetoneFreqLo := Lo(600);
DUCPkt.KeyerSpeed := 20;
DUCPkt.KeyerWeight := 50;
DUCPkt.DUC0RateHi := Hi(RateKsps);
DUCPkt.DUC0RateLo := Lo(RateKsps);
DUCPkt.DUC0Bits := 24;
DUCPkt.MicLineSelect := BuildMicLineSelectByte;
// ATT on TX — RX-ADC step attenuator во время передачи (Thetis-style protection).
// Дублируем значение во все три ADC-байта: на однопотоковых платах активен только
// ADC0, на Saturn/Hermes Lite 2 — могут использоваться ADC1/ADC2.
DUCPkt.StepAtten0 := Byte(EnsureRange(FTXSettings.AttOnTX, 0, 31));
DUCPkt.StepAtten1 := DUCPkt.StepAtten0;
DUCPkt.StepAtten2 := DUCPkt.StepAtten0;
FNetwork.SendDUCSpecific(DUCPkt);
end;
procedure TMainForm.ApplyTXSettingsToDSP;
begin
if not FWDSPReady then Exit;
// Mic source — сначала, чтобы PullSoundCardMic уже видел правильное значение
FDSPEngine.SetTXMicSource(TTXMicSource(FTXSettings.MicSource));
FDSPEngine.SetMicGain(FTXSettings.MicGainDB);
FDSPEngine.SetTXFilterFull(FTXSettings.FilterLow, FTXSettings.FilterHigh,
FTXSettings.FilterNC, FTXSettings.FilterMP,
FTXSettings.FilterWindow);
FDSPEngine.SetTXCompressor(FTXSettings.CompressorOn, FTXSettings.CompressorGain);
FDSPEngine.SetTXLeveler(FTXSettings.LevelerOn, FTXSettings.LevelerTop,
FTXSettings.LevelerDecay);
FDSPEngine.SetTXALC(FTXSettings.ALCOn, FTXSettings.ALCMaxGain,
FTXSettings.ALCDecay);
FDSPEngine.SetTXPhaseRot(FTXSettings.PhaseRotOn, FTXSettings.PhaseRotStages,
FTXSettings.PhaseRotFreq);
FDSPEngine.SetTXEQ(FTXSettings.EQOn, FTXSettings.EQNumBands,
FTXSettings.EQGains, FTXSettings.EQFreqs);
FDSPEngine.SetTXAMCarrierLevel(FTXSettings.AMCarrierLevel);
FDSPEngine.SetTXFMParams(FTXSettings.FMDeviation, FTXSettings.FMLowCut,
FTXSettings.FMHighCut, FTXSettings.FMEmphPosition);
FDSPEngine.SetTXCTCSS(FTXSettings.CTCSSOn, FTXSettings.CTCSSFreq);
// TX display
FDSPEngine.SetTXFFTParams(FTXSettings.TXFFTSize, FTXSettings.TXWindowType);
FDSPEngine.SetTXSpectrumDisplay(FTXSettings.TXSpecDetector,
FTXSettings.TXSpecAvgMode,
FTXSettings.TXSpecAvgTimeMS);
FDSPEngine.SetTXWaterfallDisplay(FTXSettings.TXWfDetector,
FTXSettings.TXWfAvgMode,
FTXSettings.TXWfAvgTimeMS);
end;
function TMainForm.PullSoundCardMic(MaxN: Integer): Integer;
// Колбэк, который TTXDSPThread зовёт при MicSource=SoundCard.
// Тянем только реально доступные сэмплы из FAudioIn — иначе мы пушили бы
// нули и mic-цепь в WDSP получала бы тишину вместо реального сигнала.
// Читаем пачкой в локальный буфер и пушим одним вызовом — экономим
// будилки семафора и предотвращаем подмену TX-блока тишиной.
var
i, n: Integer;
Buf: array[0..2047] of Double;
begin
Result := 0;
if (FAudioIn = nil) or (not FAudioIn.IsOpen) then Exit;
n := FAudioIn.Available;
if n > MaxN then n := MaxN;
if n > Length(Buf) then n := Length(Buf);
for i := 0 to n - 1 do
Buf[i] := FAudioIn.ReadSample;
if n > 0 then
FDSPEngine.PushTXMicSamplesD(Buf, n);
Result := n;
end;
// ===========================================================================
// Button handlers
// ===========================================================================
{ Поток открытия — отдельный класс, без анонимных процедур }
// Открываем WDSP в фоновом потоке чтобы не блокировать UI при нажатии START
type
TWDSPOpenThread = class(TThread)
private
FForm: TMainForm;
FResult: Boolean;
procedure SyncDone;
protected
procedure Execute; override;
public
constructor Create(AForm: TMainForm);
end;
constructor TWDSPOpenThread.Create(AForm: TMainForm);
begin
inherited Create(False);
FForm := AForm;
FResult := False;
FreeOnTerminate := True;
end;
procedure TWDSPOpenThread.Execute;
var
M: TThreadMethod;
begin
try
FResult := FForm.FDSPEngine.Open;
except
FResult := False;
end;
M := SyncDone;
TThread.Synchronize(nil, M);
end;
procedure TWDSPOpenThread.SyncDone;
begin
FForm.OnWDSPOpenDone(FResult);
end;
type
TDiscoverThread = class(TThread)
private
FNet: THPSDRNetwork;
FForm: TMainForm;
protected
procedure Execute; override;
public
constructor Create(ANet: THPSDRNetwork; AForm: TMainForm);
end;
constructor TDiscoverThread.Create(ANet: THPSDRNetwork; AForm: TMainForm);
begin
FNet := ANet;
FForm := AForm;
FreeOnTerminate := True;
inherited Create(False);
end;
procedure TDiscoverThread.Execute;
var
Devs: THPSDRDeviceArray;
Sync: TDDCSeqSync;
M: TThreadMethod;
begin
Devs := FNet.Discover(2000);
if Length(Devs) = 0 then
begin
// DDCIdx = -1 is the sentinel for "no device found"
Sync := TDDCSeqSync.Create(FForm, -1, 0);
try
M := Sync.Execute;
TThread.Synchronize(nil, M);
finally
Sync.Free;
end;
end;
// Devices found are signalled via OnDeviceFound callback during Discover()
end;
{ Перегруженный DoUpdateDDCSeq принимает -1 как «нет устройств» }
procedure TMainForm.DoUpdateDDCSeqOrNoDevice(DDCIdx: Integer; Seq: LongWord);
begin
if DDCIdx = -1 then
begin
if Assigned(FDeviceDialog) then
begin
FDeviceDialog.ClearDiscovered;
FDeviceDialog.AddDiscovered('', '-- no device found --');
end;
StatusBar1.Panels[4].Text := 'No hardware found';
end
else
StatusBar1.Panels[3].Text := Format('DDC%d seq=%d', [DDCIdx, Seq]);
end;
procedure TMainForm.RecreateDSPEngine(ASampleRate: Integer);
begin
if ASampleRate <= 0 then Exit;
if Assigned(FDSPEngine) then
begin
FDSPEngine.Close;
FreeAndNil(FDSPEngine);
end;
FWDSPReady := False;
FDSPEngine := TWDSPEngine.Create(ASampleRate, 48000, 512);
FDSPEngine.OnAudio := OnAudioReady;
FDSPEngine.OnSpectrum := OnSpectrumReady;
FDSPEngine.OnWaterfall := OnWaterfallReady;
FDSPEngine.OnTXIQ := OnTXIQReady;
end;
function TMainForm.EnsureWDSPWisdom: Boolean;
var
WisdomDir: string;
WisdomPath: string;
DirA: AnsiString;
BuildThread: TWisdomBuildThread;
Dlg: TWisdomProgressDialog;
begin
Result := True;
if not Assigned(@WDSPwisdom) then Exit;
WisdomDir := IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0)));
WisdomPath := WisdomDir + WDSP_WISDOM_FILE;
if FileExists(WisdomPath) then
begin
// По рекомендации WDSP вызываем функцию на каждом старте:
// если файл уже есть, библиотека просто импортирует wisdom.
DirA := AnsiString(WisdomDir);
WDSPwisdom(PAnsiChar(DirA));
Exit;
end;
BtnStartStop.Enabled := False;
StatusBar1.Panels[3].Text := 'Creating WDSP wisdom...';
BuildThread := TWisdomBuildThread.Create(WisdomDir);
try
Dlg := TWisdomProgressDialog.Create(Self, BuildThread);
try
Dlg.ShowModal;
finally
Dlg.Free;
end;
BuildThread.WaitFor;
if BuildThread.ErrorText <> '' then
begin
ShowMessage('Failed to create WDSP wisdom.' + LineEnding +
BuildThread.ErrorText);
Result := False;
end
else if not FileExists(WisdomPath) then
begin
ShowMessage('WDSP wisdom was not created.' + LineEnding +
'Expected file: ' + WisdomPath);
Result := False;
end;
finally
BuildThread.Free;
BtnStartStop.Enabled := True;
end;
if Result then
StatusBar1.Panels[3].Text := 'WDSP wisdom ready'
else
StatusBar1.Panels[3].Text := 'WDSP wisdom missing';
end;
procedure TMainForm.BtnDiscoverClick(Sender: TObject);
begin
// Открываем диалог выбора устройства
if not Assigned(FDeviceDialog) then
FDeviceDialog := TDeviceDialog.Create(Self);
FDeviceDialog.OnDiscover := BtnDiscoverFromDialog;
if FLightTheme then FDeviceDialog.SetTheme(LightTheme)
else FDeviceDialog.SetTheme(DarkTheme);
FDeviceDialog.ClearResult; // сбрасываем старый выбор перед открытием
FDeviceDialog.ShowModal;
// Если пользователь нажал CONNECT — результат хранится в DialogResult,
// START его заберёт и использует с приоритетом над autostart
end;
procedure TMainForm.BtnDiscoverFromDialog(Sender: TObject);
// Запускается когда пользователь нажимает DISCOVER внутри диалога
begin
SetLength(FDevices, 0);
FDeviceCount := 0;
StatusBar1.Panels[4].Text := 'Discovering...';
FNetwork.DirectIP := '';
TDiscoverThread.Create(FNetwork, Self);
end;
procedure TMainForm.BtnStartStopClick(Sender: TObject);
var
Idx, i: Integer;
Dev: THPSDRDevice;
GenPkt: TGeneralPacket;
G_Settings: TGlobalSettings;
PreG: TGlobalSettings;
PreBands: array[0..CFG_BAND_COUNT-1] of TBandSettings;
PreloadRate: Integer;
FoundByIP: Boolean;
AutoIP: string;
begin
if FNetwork.Connected then
begin
// --- STOP ---
if FRunning then
begin
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), 0, False, True, True);
FNetwork.SetRunAndFreq(False, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), 0);
FRunning := False;
FTransmitting := False;
if FWDSPReady then FDSPEngine.SetTXRun(False);
StyleButton(BtnMOX, False);
end;
FRXPacketCount := 0;
FActiveDDC := 0;
StatusBar1.Panels[3].Text := 'RX: waiting...';
// Сохраняем текущую частоту/настройки чтобы следующий START восстановил их
if FDevConnected then
begin
SaveCurrentBand;
FSettings.SaveGlobal(FDevMAC, MakeGlobalSettings);
FSettings.SaveAlex(FDevMAC, FAlexSettings);
// XVTR LastFreq → актуальная видимая частота в момент остановки
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) then
FXvtrSettings.Entries[FCurrentXvtr].LastFreq := FVfoA;
FSettings.SaveXvtr(FDevMAC, FXvtrSettings);
end;
FSettings.SaveStartupPreview(FVfoA, FVfoB, FSampleRate);
FSettings.Save;
FSpecView.ResetSpectrumBuf;
FSpecView.DrawSpectrum;
PbSpectrum.Invalidate;
FNetwork.Disconnect;
BtnStartStop.Caption := 'START';
StyleButton(BtnStartStop, False);
StatusBar1.Panels[4].Text := 'Not connected';
StatusBar1.Panels[0].Text := 'IP: --';
StatusBar1.Panels[1].Text := 'Board: --';
BtnMOX.Enabled := False;
Exit;
end;
// --- START ---
AutoIP := '';
FPendingBoardType := 0;
// Явный выбор через CONNECT в диалоге имеет приоритет над autostart
if Assigned(FDeviceDialog) and FDeviceDialog.DialogResult.Accepted then
begin
FPendingIP := FDeviceDialog.DialogResult.IPAddress;
FPendingBoardType := FDeviceDialog.GetSavedBoardType(
FDeviceDialog.DialogResult.SavedIdx);
FDeviceDialog.ClearResult; // consumed — следующий START снова спросит
end
else
begin
if Assigned(FDeviceDialog) then
AutoIP := FDeviceDialog.GetAutoStartIP;
if AutoIP <> '' then
begin
FPendingIP := AutoIP;
FPendingBoardType := FDeviceDialog.GetAutoStartBoardType;
end
else
begin
if not Assigned(FDeviceDialog) then
FDeviceDialog := TDeviceDialog.Create(Self);
FDeviceDialog.OnDiscover := BtnDiscoverFromDialog;
if FLightTheme then FDeviceDialog.SetTheme(LightTheme)
else FDeviceDialog.SetTheme(DarkTheme);
if FDeviceDialog.ShowModal <> mrOk then Exit;
if not FDeviceDialog.DialogResult.Accepted then Exit;
FPendingIP := FDeviceDialog.DialogResult.IPAddress;
FPendingBoardType := FDeviceDialog.GetSavedBoardType(
FDeviceDialog.DialogResult.SavedIdx);
FDeviceDialog.ClearResult;
end;
end;
// Создаём запись устройства из IP + BoardType из диалога если есть
FillChar(Dev, SizeOf(Dev), 0);
Dev.IPAddress := FPendingIP;
Dev.BoardType := FPendingBoardType;
// Подбираем sample rate до открытия WDSP, чтобы не делать двойной Open:
// 1) Open на 192k, 2) сразу ChangeSampleRate+Open ещё раз.
// Если устройство уже было обнаружено (есть MAC) и есть сохранённый профиль,
// создаём WDSPEngine сразу с нужной частотой.
PreloadRate := FSampleRate;
FoundByIP := False;
for i := 0 to High(FDevices) do
if SameText(FDevices[i].Dev.IPAddress, Dev.IPAddress) then
begin
FoundByIP := True;
if FSettings.LoadDevice(FDevices[i].Dev.MAC, PreG, PreBands) and (PreG.SampleRate > 0) then
PreloadRate := PreG.SampleRate;
Break;
end;
if FoundByIP and (PreloadRate > 0) then
begin
FSampleRate := PreloadRate;
FSpanHz := PreloadRate;
if (not FWDSPReady) and (Assigned(FDSPEngine)) and (FDSPEngine.SampleRate <> PreloadRate) then
RecreateDSPEngine(PreloadRate);
end;
// --- Открываем WDSP асинхронно чтобы не блокировать UI ---
FPendingDev := Dev;
BtnStartStop.Enabled := False;
StatusBar1.Panels[3].Text := 'Opening WDSP...';
if FWDSPReady then
// WDSP уже открыт — сразу подключаемся
DoConnectDevice(Dev)
else
// Открываем в фоновом потоке
TWDSPOpenThread.Create(Self);
end;
procedure TMainForm.OnWDSPOpenDone(Success: Boolean);
begin
// Вызывается из TWDSPOpenThread.SyncDone — уже в главном потоке
FWDSPReady := Success;
if not Success then
begin
StatusBar1.Panels[3].Text := 'WDSP open failed';
BtnStartStop.Enabled := True;
BtnStartStop.Caption := 'START';
StyleButton(BtnStartStop, False);
ShowMessage('WDSP not loaded.' + LineEnding +
IfThen(FDSPEngine.LastError <> '', FDSPEngine.LastError, 'Unknown WDSP init error.') + LineEnding +
'Put wdsp.dll рядом с ewsdr.exe (той же разрядности, что и приложение).');
Exit;
end;
DoConnectDevice(FPendingDev);
end;
procedure TMainForm.DoConnectDevice(const Dev: THPSDRDevice);
var
GenPkt: TGeneralPacket;
G_Settings: TGlobalSettings;
i: Integer;
begin
BtnStartStop.Enabled := True;
if not FNetwork.Connect(Dev) then
begin
ShowMessage('Failed to connect to ' + Dev.IPAddress +
IfThen(FNetwork.LastError <> '', ': ' + FNetwork.LastError, ''));
Exit;
end;
// --- Загружаем настройки устройства по MAC ---
Move(FNetwork.Device.MAC[0], FDevMAC[0], 6);
FDevConnected := True;
FSettings.LoadDevice(FDevMAC, G_Settings, FBandCache);
FCATLastGlobal := G_Settings;
CATApplySettings(G_Settings);
// TX-настройки per-device — отдельная JSON-секция "tx"
FSettings.LoadTX(FDevMAC, FTXSettings);
// Alex-настройки per-device (антенны/маршрутизация)
FSettings.LoadAlex(FDevMAC, FAlexSettings);
FNetwork.SetAlexConfig(FAlexSettings);
// XVTR-настройки per-device (трансвертеры)
FSettings.LoadXvtr(FDevMAC, FXvtrSettings);
FCurrentXvtr := -1;
RebuildXvtrButtons;
ApplyXvtrToNetwork;
PushXvtrToWeb;
FTXSpecRefLevel := FTXSettings.TXSpecRefLevel;
FTXSpecRange := FTXSettings.TXSpecRange;
if FTXSettings.TXSpecGridStep > 0 then
FTXSpecGridStep := FTXSettings.TXSpecGridStep;
FVolume := G_Settings.Volume;
FActiveVfo := G_Settings.ActiveVfo;
// PA settings
FPAMaxPower := G_Settings.PAMaxPower;
for i := 0 to BAND_COUNT - 1 do
FPABandCal[i] := G_Settings.PABandCal[i];
// Slider position (0..100) and calibrated drive byte
TrkDrive.Position := EnsureRange(G_Settings.DriveLevel, 0, 100);
FDriveLevel := CalcDriveByte;
FCurrentBand := G_Settings.LastBand;
// SampleRate — глобальный, загружаем до RestoreBand
if G_Settings.SampleRate > 0 then
begin
FSampleRate := G_Settings.SampleRate;
FSpanHz := FSampleRate;
end;
BtnNR.Tag := EnsureRange(G_Settings.NRMode, 0, 4); UpdateNRButton;
BtnNB.Tag := EnsureRange(G_Settings.NBMode, 0, 2); UpdateNBButton;
BtnSNB.Tag := Ord(G_Settings.SNBEnabled); UpdateSNBButton;
BtnANF.Tag := Ord(G_Settings.ANFEnabled); UpdateANFButton;
FWfAGCEnabled := G_Settings.WfAGCEnabled;
FWfNFEnabled := G_Settings.WfNFEnabled;
FDitherEnabled := G_Settings.DitherEnabled;
FRandomEnabled := G_Settings.RandomEnabled;
FWfManualHigh := G_Settings.WfManualHigh;
FWfManualLow := G_Settings.WfManualLow;
FWfAGCOffset := G_Settings.WfAGCOffset;
FSpecView.WfAGCEnabled := FWfAGCEnabled;
FSpecView.WfNFEnabled := FWfNFEnabled;
FSpecView.WfManualHigh := FWfManualHigh;
FSpecView.WfManualLow := FWfManualLow;
FSpecView.WfAGCOffset := FWfAGCOffset;
FSpecView.ResetWfAvgBuf;
FShowSpectrum := G_Settings.ShowSpectrum;
FShowWaterfall := G_Settings.ShowWaterfall;
FDisplayDuplex := G_Settings.DisplayDuplex;
if BtnDUP <> nil then StyleButton(BtnDUP, FDisplayDuplex);
if G_Settings.DisplayFPS > 0 then
ApplyFPS(G_Settings.DisplayFPS);
ApplyFreqMhzDigits(EnsureRange(G_Settings.FreqMhzDigits, 3, 5));
// Настройки дисплея и сетки
if G_Settings.FFTSize > 0 then
begin
FSpecRefLevel := G_Settings.SpecRefLevel;
FSpecRange := G_Settings.SpecRange;
if G_Settings.SpecGridStep > 0 then FSpecGridStep := G_Settings.SpecGridStep;
FSpecView.SpecRefLevel := FSpecRefLevel;
FSpecView.SpecRange := FSpecRange;
FSpecView.SpecGridStep := FSpecGridStep;
InvalidateGridCache;
if FWDSPReady then
begin
FDSPEngine.SetFFTParams(G_Settings.FFTSize, G_Settings.WindowType);
FDSPEngine.SetSpectrumDisplay(G_Settings.SpecDetector, G_Settings.SpecAvgMode,
G_Settings.SpecAvgTimeMS);
FDSPEngine.SetWaterfallDisplay(G_Settings.WfDetector, G_Settings.WfAvgMode,
G_Settings.WfAvgTimeMS);
end;
// Аудио устройство вывода
FAudioOut.Close;
if G_Settings.AudioSampleRate > 0 then
FAudioOut.SampleRate := G_Settings.AudioSampleRate;
if G_Settings.AudioOutDevice <> '' then
begin
FAudioOut.DeviceIndex := FAudioOut.FindDeviceByName(G_Settings.AudioOutDevice);
FAudioOutDevName := G_Settings.AudioOutDevice;
end
else
begin
FAudioOut.DeviceIndex := -1;
FAudioOutDevName := '';
end;
FAudioOut.Open;
// Аудио устройство ввода (TX mic). Если в настройках выбрана конкретная
// soundcard — открываем её и принудительно переключаем MicSource на SoundCard,
// иначе пользователь нажмёт MOX и не поймёт почему его микрофон молчит.
if G_Settings.AudioInDevice <> '' then
begin
FAudioIn.DeviceIndex := FAudioIn.FindDeviceByName(G_Settings.AudioInDevice);
FAudioInDevName := G_Settings.AudioInDevice;
FAudioIn.Open;
FTXSettings.MicSource := 1; // SoundCard
end
else
begin
FAudioIn.DeviceIndex := -1;
FAudioInDevName := '';
end;
end;
RestoreBand(FCurrentBand);
// Восстанавливаем XVTR-режим, если он был активен при выходе.
// FCurrentBand уже загружен (HF-диапазон, на который пользователь зашёл
// до XVTR), его FBandCache не пострадает — ActivateXvtrBand только
// выставит FVfoA на XVTR-частоту и FCurrentXvtr.
if (G_Settings.LastXvtr >= 0) and (G_Settings.LastXvtr < CFG_XVTR_COUNT)
and FXvtrSettings.Entries[G_Settings.LastXvtr].Enabled then
ActivateXvtrBand(G_Settings.LastXvtr);
StatusBar1.Panels[0].Text := 'IP: ' + FNetwork.Device.IPAddress;
StatusBar1.Panels[1].Text := 'Board: ' + BoardTypeName(FNetwork.Device.BoardType);
// General packet
FillChar(GenPkt, SizeOf(GenPkt), 0);
GenPkt.Command := CMD_GENERAL;
GenPkt.Flags37 := $08;
GenPkt.Flags38 := $01;
GenPkt.PAConfig := $01;
if FNetwork.Device.BoardType = 5 then
GenPkt.AlexEnable := $03
else
GenPkt.AlexEnable := $01;
FNetwork.SendGeneralPacket(GenPkt);
if FNetwork.Device.BoardType in [3, 4, 5] then
FActiveDDC := 2
else
FActiveDDC := 0;
// Синхронизируем samplerate WDSP если нужно
if FWDSPReady and (FDSPEngine.SampleRate <> FSampleRate) then
begin
FWDSPReady := False;
FDSPEngine.ChangeSampleRate(FSampleRate);
FWDSPReady := FDSPEngine.Initialized;
end;
if FWDSPReady then
begin
FDSPEngine.SetMode(FMode);
FDSPEngine.SetVolume(FVolume / 100.0);
ApplyModeFilter;
ApplyNoiseFilterButtonsToDSP;
// ChangeSampleRate пересоздаёт WDSP-канал — восстанавливаем все настройки
FDSPEngine.SetAGCTop(FAGCTop);
FDSPEngine.SetAGC(TWDSPAGCMode(FAGCMode), 50.0);
end;
FSpecView.AGCTop := FAGCTop;
// Сначала отправляем Run=1 — эмулятор/железо создают ddc_specific_thread
// (порт 1025) и rx_thread (порт 1035+) только после получения HP Run=1.
// DDC Specific и DUC Specific отправляем ПОСЛЕ, иначе порты ещё не открыты.
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, False, True, True);
FNetwork.SetRunAndFreq(True, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), FDriveLevel);
FRunning := True;
FRXStartTime := GetTickCount64;
FRXLastPktTime := 0;
// DDC и DUC Specific — теперь потоки эмулятора слушают на своих портах.
// DUC Specific содержит mic-конфигурацию (boost/bias/line/PTT) из FTXSettings.
FNetwork.ConfigureDDCs(1, FSampleRate div 1000, 0, FDitherEnabled, FRandomEnabled);
SendDUCSpecificFromSettings;
// Применяем TX-цепь (фильтр, mic gain, EQ, leveler, ALC, comp, ...).
ApplyTXSettingsToDSP;
ApplyModeFilter;
// Принудительно обновляем AGC линии — гарантированно после всех SetAGC вызовов
if FWDSPReady then
FDSPEngine.UpdateAGCLines(FSpectrumWidth); // ещё не получили ни одного пакета
BtnStartStop.Caption := 'STOP';
StyleButton(BtnStartStop, True);
StatusBar1.Panels[4].Text := 'RUNNING';
BtnMOX.Enabled := True;
end;
// ---------------------------------------------------------------------------
// ActivateVfo — переключаем активный VFO и перестраиваем приёмник.
// Idx=0 → VFO-A активен, Idx=1 → VFO-B активен.
// Перестройка всегда происходит на частоту активного VFO (не на оффсет).
// CTUN при этом сбрасывается чтобы не было путаницы с оффсетом.
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// FreqToBandIdx — возвращает индекс диапазона для частоты Hz, или -1
// ---------------------------------------------------------------------------
function FreqToBandIdx(Hz: Double): Integer;
const
// Границы диапазонов [Low, High] в Гц (приблизительные)
BAND_LO: array[0..10] of Double = (
1800000, 3500000, 5330000, 7000000, 10100000,
14000000, 18068000, 21000000, 24890000, 28000000, 50000000);
BAND_HI: array[0..10] of Double = (
2000000, 4000000, 5410000, 7300000, 10150000,
14350000, 18168000, 21450000, 24990000, 29700000, 54000000);
var
i: Integer;
begin
Result := -1;
for i := 0 to 10 do
if (Hz >= BAND_LO[i]) and (Hz <= BAND_HI[i]) then
begin
Result := i;
Exit;
end;
end;
// ---------------------------------------------------------------------------
// ActiveVfoFreq — частота активного VFO
// ---------------------------------------------------------------------------
function TMainForm.ActiveVfoFreq: Int64;
begin
if FActiveVfo = 0 then Result := Round(FVfoA)
else Result := Round(FVfoB);
end;
// ---------------------------------------------------------------------------
// ActivateVfo — переключаем активный VFO и перестраиваем приёмник.
// Idx=0 → VFO-A активен, Idx=1 → VFO-B активен.
// Перестройка на частоту выбранного VFO. CTUN не трогаем.
// ---------------------------------------------------------------------------
procedure TMainForm.ActivateVfo(Idx: Integer);
var
ActiveFreq: Int64;
NewBand: Integer;
Offset: Double;
HalfSpan: Double;
FreqVisible: Boolean;
begin
FActiveVfo := Idx;
// Частота нового активного VFO
if FActiveVfo = 0 then
ActiveFreq := Round(FVfoA)
else
ActiveFreq := Round(FVfoB);
// Обновляем визуальный дисплей
UpdateVfoDisplay;
if FCTun then
begin
// CTUN включён: проверяем, видна ли новая частота на текущем спектре
HalfSpan := FSpanHz / 2;
Offset := ActiveFreq - FCenterFreq;
FreqVisible := (Offset > -HalfSpan) and (Offset < HalfSpan);
if FreqVisible then
begin
// Частота видна в окне — просто меняем shift, DDC не трогаем
if FWDSPReady then FDSPEngine.SetShift(Offset);
// Сеть не трогаем — DDC (FCenterFreq) остаётся прежним
end
else
begin
// Частота за пределами спектра — центрируем окно и сбрасываем CTUN
FCTun := False;
StyleButton(BtnCTun, False);
FBandCache[FCurrentBand].CTun := False;
FCenterFreq := ActiveFreq;
if FWDSPReady then FDSPEngine.SetShift(0.0);
if FNetwork.Connected and FNetwork.Running then
begin
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True);
FNetwork.SendFullHP;
end;
end;
end
else
begin
// CTUN выключен: перестраиваем DDC на новую частоту
FCenterFreq := ActiveFreq;
if FWDSPReady then FDSPEngine.SetShift(0.0);
if FNetwork.Connected and FNetwork.Running then
begin
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True);
FNetwork.SendFullHP;
end;
end;
// Переключаем кнопку диапазона по новой частоте
NewBand := FreqToBandIdx(ActiveFreq);
if (NewBand >= 0) and (NewBand <> FCurrentBand) then
begin
StyleButton(BtnBand[FCurrentBand], False);
FCurrentBand := NewBand;
StyleButton(BtnBand[FCurrentBand], True);
end;
FSpecView.InvalidateRulerCache;
SyncSpecViewFreq;
FSpecView.DrawSpectrum;
PbSpectrum.Invalidate;
FSpecView.DrawWaterfall;
PbWaterfall.Invalidate;
if PbRuler <> nil then PbRuler.Invalidate;
end;
procedure TMainForm.FreqDispAClick(Sender: TObject);
begin
// Клик на VFO-A → активируем A (перестройка на FVfoA)
if FActiveVfo <> 0 then
ActivateVfo(0);
end;
procedure TMainForm.FreqDispBClick(Sender: TObject);
begin
// Клик на VFO-B → активируем B (перестройка на FVfoB)
if FActiveVfo <> 1 then
ActivateVfo(1);
end;
procedure TMainForm.BtnTopVfoASelectClick(Sender: TObject);
begin
if FActiveVfo <> 0 then
ActivateVfo(0);
end;
procedure TMainForm.BtnTopVfoATXClick(Sender: TObject);
begin
FSplitTxB := False;
UpdateVfoDisplay;
if FNetwork.Connected and FNetwork.Running then
begin
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel,
FTransmitting, True, True);
FNetwork.SendFullHP;
end;
end;
procedure TMainForm.BtnTopVfoBSelectClick(Sender: TObject);
begin
if FActiveVfo <> 1 then
ActivateVfo(1);
end;
procedure TMainForm.BtnTopVfoBTXClick(Sender: TObject);
begin
FSplitTxB := True;
if FSplitTxB and (FActiveVfo <> 0) then
ActivateVfo(0)
else
UpdateVfoDisplay;
if FNetwork.Connected and FNetwork.Running then
begin
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel,
FTransmitting, True, True);
FNetwork.SendFullHP;
end;
end;
procedure TMainForm.FreqDispAChanged(Sender: TObject; NewFreq: Int64);
begin
if FActiveVfo = 0 then
ApplyVfoA(NewFreq)
else
FVfoA := NewFreq; // просто сохраняем, приёмник не перестраиваем
end;
procedure TMainForm.FreqDispBChanged(Sender: TObject; NewFreq: Int64);
var
BandIdx: Integer;
begin
FVfoB := NewFreq;
if FActiveVfo = 1 then
begin
// VFO-B активен: перестраиваем приёмник
FCenterFreq := FVfoB;
if FWDSPReady then FDSPEngine.SetShift(0.0);
if FNetwork.Connected and FNetwork.Running then
begin
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True);
FNetwork.SendFullHP;
end;
// Обновляем диапазон
BandIdx := FreqToBandIdx(FVfoB);
if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then
begin
StyleButton(BtnBand[FCurrentBand], False);
FCurrentBand := BandIdx;
StyleButton(BtnBand[FCurrentBand], True);
end;
FSpecView.InvalidateRulerCache;
SyncSpecViewFreq;
FSpecView.DrawSpectrum;
PbSpectrum.Invalidate;
if PbRuler <> nil then PbRuler.Invalidate;
end;
end;
// ---------------------------------------------------------------------------
// UpdateFilterButtons — обновляет подписи кнопок фильтра под текущий режим
// ---------------------------------------------------------------------------
procedure TMainForm.UpdateFilterButtons;
var
i: Integer;
Names: array[0..FILT_COUNT-1] of string;
DefIdx: Integer;
W: Integer;
begin
if FMode = MODE_FM then
begin
// FM: show only NFM and WFM buttons, resize them to fill the row
CloseCTCSSPopup;
if FFilter > 1 then FFilter := FILT_FM_DEF; // sanitize, keep NFM/WFM as-is
FFilterBW := FILT_FM_BW[FFilter];
FFMDeviation := FILT_FM_DEV[FFilter];
W := (PanelFilter.Width - 6) div 2;
BtnFilter[0].Caption := FILT_FM_NAMES[0];
BtnFilter[0].Left := 2;
BtnFilter[0].Width := W - 2;
BtnFilter[0].Top := 16;
BtnFilter[0].Visible := True;
StyleButton(BtnFilter[0], FFilter = 0);
BtnFilter[1].Caption := FILT_FM_NAMES[1];
BtnFilter[1].Left := W + 2;
BtnFilter[1].Width := W - 2;
BtnFilter[1].Top := 16;
BtnFilter[1].Visible := True;
StyleButton(BtnFilter[1], FFilter = 1);
for i := 2 to FILT_COUNT - 1 do BtnFilter[i].Visible := False;
// Shrink panel to fit label + 1 button row so SQL/CTCSS sit right below
PanelFilter.Height := BtnFilter[0].Top + BtnFilter[0].Height + 4;
if PanelFMSQ <> nil then
begin
PanelFMSQ.Visible := True;
StyleButton(BtnFMSQ, FFMSQOn);
TrkFMSQ.Position := FFMSQLevel;
LblFMSQ.Caption := IntToStr(FFMSQLevel);
end;
if PanelFMCTCSS <> nil then PanelFMCTCSS.Visible := True;
if PanelFMStep <> nil then
begin
StyleButton(BtnFMStep, FFMStepOn);
BtnFMStepSel.Caption := FM_STEP_NAMES[FFMStepIdx];
PanelFMStep.Visible := True;
end;
RelayoutBelowBands;
Exit;
end;
// Non-FM: restore full 2-row height and 5-column layout, hide FM panels
PanelFilter.Height := 74;
if PanelFMSQ <> nil then PanelFMSQ.Visible := False;
if PanelFMCTCSS <> nil then PanelFMCTCSS.Visible := False;
if PanelFMStep <> nil then begin CloseFMStepPopup; PanelFMStep.Visible := False; end;
W := (PanelFilter.Width - 6) div 5;
for i := 0 to FILT_COUNT - 1 do
begin
BtnFilter[i].Left := 2 + (i mod 5) * W;
BtnFilter[i].Top := 16 + (i div 5) * 27;
BtnFilter[i].Width := W - 2;
BtnFilter[i].Visible := True;
end;
case FMode of
0, 1: begin
for i := 0 to FILT_COUNT-1 do Names[i] := FILT_SSB_NAMES[i];
DefIdx := FILT_SSB_DEF;
end;
2: begin
for i := 0 to FILT_COUNT-1 do Names[i] := FILT_DSB_NAMES[i];
DefIdx := FILT_DSB_DEF;
end;
3, 4: begin
for i := 0 to FILT_COUNT-1 do Names[i] := FILT_CW_NAMES[i];
DefIdx := FILT_CW_DEF;
end;
else begin
for i := 0 to FILT_COUNT-1 do Names[i] := FILT_AM_NAMES[i];
DefIdx := FILT_AM_DEF;
end;
end;
FFilter := DefIdx;
case FMode of
0, 1: FFilterBW := FILT_SSB_BW[DefIdx];
2: FFilterBW := FILT_DSB_BW[DefIdx];
3, 4: FFilterBW := FILT_CW_BW[DefIdx];
else FFilterBW := FILT_AM_BW[DefIdx];
end;
for i := 0 to FILT_COUNT-1 do
begin
BtnFilter[i].Caption := Names[i];
StyleButton(BtnFilter[i], i = FFilter);
end;
RelayoutBelowBands;
end;
// ---------------------------------------------------------------------------
// ApplyVfoA — единая точка смены частоты VFO A
//
// CTUN ВЫКЛ (классический режим):
// FCenterFreq = FVfoA — спектр центрирован на VFO, маркер всегда в центре
//
// CTUN ВКЛ (piHPSDR/Thetis режим):
// FCenterFreq статичен — спектр/водопад не двигаются
// FVfoA движется по дисплею (маркер гуляет)
// Перецентрирование — только когда КРАЙ ПОЛОСЫ ФИЛЬТРА выходит за край дисплея
// (по образцу OpenHPSDR/PowerSDR: "re-centering occurs as the edge of the passband
// hits the edge of the display")
// ---------------------------------------------------------------------------
procedure TMainForm.ApplyVfoA(NewFreq: Int64);
var
Offset: Double;
FiltLo: Double;
FiltHi: Double;
HalfSpan: Double;
Scrolled: Boolean;
BandIdx: Integer;
begin
// Клэмп в пределах XVTR-band — не даём VFO уйти за FreqBegin/FreqEnd
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and
FXvtrSettings.Entries[FCurrentXvtr].Enabled then
begin
if NewFreq < FXvtrSettings.Entries[FCurrentXvtr].FreqBegin then
NewFreq := Round(FXvtrSettings.Entries[FCurrentXvtr].FreqBegin);
if NewFreq > FXvtrSettings.Entries[FCurrentXvtr].FreqEnd then
NewFreq := Round(FXvtrSettings.Entries[FCurrentXvtr].FreqEnd);
end;
FVfoA := NewFreq;
Scrolled := False;
if not FCTun then
begin
// ---- CTUN OFF ----
// DDC = VFO, сдвига нет
FCenterFreq := FVfoA;
if FWDSPReady then
FDSPEngine.SetShift(0.0);
end
else
begin
// ---- CTUN ON ----
// DDC (FCenterFreq) стоит на месте.
// SetRXAShiftFreq сдвигает спектр внутри WDSP так чтобы
// демодулятор принимал сигнал на FVfoA, а не на FCenterFreq.
// Shift = FVfoA - FCenterFreq (в Гц)
Offset := FVfoA - FCenterFreq;
if FWDSPReady then
FDSPEngine.SetShift(Offset);
// Проверяем не вышла ли полоса фильтра за край дисплея
HalfSpan := FSpanHz / 2;
case FMode of
0: begin FiltLo := Offset - FFilterBW; FiltHi := Offset - 100; end;
1: begin FiltLo := Offset + 100; FiltHi := Offset + FFilterBW; end;
else begin FiltLo := Offset - FFilterBW/2; FiltHi := Offset + FFilterBW/2; end;
end;
if (FiltHi > HalfSpan) or (FiltLo < -HalfSpan) then
begin
// Полоса вышла за край → прокручиваем центр
if FiltHi > HalfSpan then
FCenterFreq := FVfoA - HalfSpan * 0.5
else
FCenterFreq := FVfoA + HalfSpan * 0.5;
// Пересчитываем сдвиг после прокрутки
if FWDSPReady then
FDSPEngine.SetShift(FVfoA - FCenterFreq);
// DDC перестраивается на новый FCenterFreq
Scrolled := True;
end;
end;
// Обновляем VFO дисплей
FreqDispA.Frequency := Round(FVfoA);
// Обновляем кнопку диапазона если частота перешла в другой диапазон.
// В XVTR-режиме HF band-кнопки не подсвечиваем (мы клэмпим VFO внутри
// XVTR-диапазона — FreqToBandIdx всегда вернёт -1 для VHF/UHF).
if FCurrentXvtr < 0 then
begin
BandIdx := FreqToBandIdx(FVfoA);
if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then
begin
StyleButton(BtnBand[FCurrentBand], False);
FCurrentBand := BandIdx;
StyleButton(BtnBand[FCurrentBand], True);
end;
end;
// DDC: передаём FCenterFreq (при CTUN OFF = FVfoA, при CTUN ON = фиксирован)
if FNetwork.Connected and FNetwork.Running then
begin
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True);
FNetwork.SendFullHP;
end;
// Перерисовываем спектр всегда (маркер и полоса двигаются)
// Если идёт drag — не вызываем Draw напрямую, таймер подхватит
SyncSpecViewFreq;
if FSpecDrag then
FSpectrumDirty := True
else
begin
FSpecView.DrawSpectrum;
PbSpectrum.Invalidate;
if Scrolled then
begin
FSpecView.DrawWaterfall;
PbWaterfall.Invalidate;
end;
end;
end;
// ---------------------------------------------------------------------------
// CTUN toggle
// ---------------------------------------------------------------------------
procedure TMainForm.BtnCTunClick(Sender: TObject);
begin
FCTun := not FCTun;
StyleButton(BtnCTun, FCTun);
FBandCache[FCurrentBand].CTun := FCTun;
if not FCTun then
begin
// Выключили CTUN: центрируемся на VFO, shift=0
FCenterFreq := FVfoA;
if FWDSPReady then
FDSPEngine.SetShift(0.0);
if FNetwork.Connected and FNetwork.Running then
begin
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True);
FNetwork.SendFullHP;
end;
end
else
begin
// Включили CTUN: фиксируем текущее положение дисплея
// FCenterFreq остаётся как есть — спектр не прыгает
end;
SyncSpecViewFreq;
FSpecView.DrawSpectrum;
FSpecView.DrawWaterfall;
PbSpectrum.Invalidate;
PbWaterfall.Invalidate;
end;
// ---------------------------------------------------------------------------
// DUP toggle (Thetis-style local display mode — без аппаратной команды)
// ---------------------------------------------------------------------------
procedure TMainForm.ApplyDUP(Active: Boolean);
begin
FDisplayDuplex := Active;
StyleButton(BtnDUP, FDisplayDuplex);
// Если сейчас идёт передача — переключаем источник пикселей и сбрасываем
// буферы, чтобы старые TX/RX bin'ы не оставались на следующем кадре.
if FTransmitting then
begin
if FWDSPReady then
FDSPEngine.SetDisplaySourceTX(not FDisplayDuplex);
FSpecView.TXMode := FTransmitting and not FDisplayDuplex;
FSpecView.ResetSpectrumBuf;
FSpecView.ResetWfAvgBuf;
ApplySpecViewGridFromState;
FSpecView.DrawSpectrum;
FSpecView.DrawWaterfall;
PbSpectrum.Invalidate;
PbWaterfall.Invalidate;
end;
end;
procedure TMainForm.BtnDUPClick(Sender: TObject);
begin
ApplyDUP(not FDisplayDuplex);
end;
// ---------------------------------------------------------------------------
// TUN — Thetis-style тон: WDSP TXA PostGen + MOX с отдельным уровнем Drive
// ---------------------------------------------------------------------------
procedure TMainForm.ApplyTUN(Active: Boolean);
begin
if Active = FTuning then Exit;
// Safety: те же блокировки что в ApplyMOX. TUN — это TX через WDSP PostGen,
// он тоже должен молчать на бэндах с DoNotTx или на RX-only XVTR.
if Active and FDevConnected and (FCurrentXvtr < 0)
and FAlexSettings.DoNotTx[EnsureRange(FCurrentBand, 0, 10)] then
Exit;
if Active and (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT)
and FXvtrSettings.Entries[FCurrentXvtr].RXOnly then
Exit;
if Active then
begin
// Включаем тон ДО запуска TX-аудио, чтобы первые сэмплы уже шли с тоном
if FWDSPReady then
FDSPEngine.SetTXTone(True, FTXSettings.TUNFreq, 1.0);
FTuning := True;
StyleButton(BtnTUN, True);
// Drive байт пересчитываем (CalcDriveByte увидит FTuning=True)
FDriveLevel := CalcDriveByte;
if FWDSPReady then
FDSPEngine.SetDriveLevel(FTXSettings.TUNLevel / 100.0);
// Активируем TX (то же что MOX — PTT, run TXA-канал)
if not FTransmitting then ApplyMOX(True);
end else
begin
// Выключаем TX, потом тон, потом восстанавливаем drive
if FTransmitting then ApplyMOX(False);
if FWDSPReady then
FDSPEngine.SetTXTone(False, 0, 0);
FTuning := False;
StyleButton(BtnTUN, False);
FDriveLevel := CalcDriveByte;
if FWDSPReady then
FDSPEngine.SetDriveLevel(TrkDrive.Position / 100.0);
if FNetwork.Connected and FNetwork.Running then
begin
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel,
FTransmitting, True, True);
FNetwork.SendFullHP;
end;
end;
end;
procedure TMainForm.BtnTUNClick(Sender: TObject);
begin
ApplyTUN(not FTuning);
end;
// ---------------------------------------------------------------------------
// Клик/драг по спектру/водопаду
// ---------------------------------------------------------------------------
// Перевод X пикселя в частоту
function PixelToFreq(PixelX, PanelWidth: Integer;
CenterFreq, SpanHz: Double): Double;
begin
Result := CenterFreq + (PixelX / PanelWidth - 0.5) * SpanHz;
end;
procedure TMainForm.DoSpectrumClick(PixelX: Integer; PanelWidth: Integer);
var
ClickFreq: Int64;
StepHz: Int64;
BandIdx: Integer;
begin
if PanelWidth <= 0 then Exit;
StepHz := 100;
ClickFreq := Round(FCenterFreq + (PixelX / PanelWidth - 0.5) * FSpanHz);
ClickFreq := (ClickFreq div StepHz) * StepHz;
if FActiveVfo = 0 then
ApplyVfoA(ClickFreq)
else
begin
FVfoB := ClickFreq;
FreqDispB.Frequency := Round(FVfoB);
FCenterFreq := ClickFreq;
if FWDSPReady then FDSPEngine.SetShift(0.0);
if FNetwork.Connected and FNetwork.Running then
begin
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True);
FNetwork.SendFullHP;
end;
BandIdx := FreqToBandIdx(FVfoB);
if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then
begin
StyleButton(BtnBand[FCurrentBand], False);
FCurrentBand := BandIdx;
StyleButton(BtnBand[FCurrentBand], True);
end;
FSpecView.InvalidateRulerCache;
SyncSpecViewFreq;
FSpecView.DrawSpectrum;
PbSpectrum.Invalidate;
if PbRuler <> nil then PbRuler.Invalidate;
end;
end;
procedure TMainForm.DoSpectrumDrag(PixelX: Integer; PanelWidth: Integer);
var
dPix: Integer;
dFreq: Double;
begin
if PanelWidth <= 0 then Exit;
dPix := PixelX - FSpecDragX0;
dFreq := dPix / PanelWidth * FSpanHz;
if FCTun then
begin
// CTUN ON: drag двигает окно просмотра (FCenterFreq/DDC)
// VFO остаётся, shift обновляется = VFO - новый FCenterFreq
FCenterFreq := FSpecDragFreq - dFreq;
if FWDSPReady then
begin
if FActiveVfo = 0 then
FDSPEngine.SetShift(FVfoA - FCenterFreq)
else
FDSPEngine.SetShift(FVfoB - FCenterFreq);
end;
if FNetwork.Connected and FNetwork.Running then
begin
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True);
FNetwork.SendFullHP;
end;
FSpectrumDirty := True;
end
else
begin
// CTUN OFF: drag двигает VFO, FCenterFreq следует через Apply
if FActiveVfo = 0 then
ApplyVfoA(Round(FSpecDragFreq - dFreq))
else
begin
FVfoB := FSpecDragFreq - dFreq;
FreqDispB.Frequency := Round(FVfoB);
FCenterFreq := FVfoB;
if FWDSPReady then FDSPEngine.SetShift(0.0);
if FNetwork.Connected and FNetwork.Running then
begin
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True);
FNetwork.SendFullHP;
end;
FSpectrumDirty := True;
end;
end;
end;
// Спектр
procedure TMainForm.PbSpectrumMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
if Button = mbLeft then
begin
// Левая кнопка: сбрасываем маркер если активен
if FSpecView.MarkerActive then
begin
FSpecView.MarkerActive := False;
PbSpectrum.Invalidate;
PbWaterfall.Invalidate;
end;
FSpecDrag := True;
FSpecDragX0 := X;
if FCTun then FSpecDragFreq := FCenterFreq
else begin
if FActiveVfo = 0 then FSpecDragFreq := FVfoA
else FSpecDragFreq := FVfoB;
end;
PbSpectrum.Cursor := crSizeWE;
end
else if Button = mbRight then
begin
// Правая кнопка: toggle маркера
if FSpecView.MarkerActive and (Abs(Round(X / PbSpectrum.Width * 1000) - FSpecView.MarkerX) < 8) then
begin
// Клик рядом с текущим маркером — убираем
FSpecView.MarkerActive := False;
end
else
begin
// Ставим маркер в новое место
FSpecView.MarkerActive := True;
FSpecView.MarkerX := Round(X / PbSpectrum.Width * 1000);
end;
PbSpectrum.Invalidate;
PbWaterfall.Invalidate;
end;
end;
procedure TMainForm.PbSpectrumMouseMove(Sender: TObject; Shift: TShiftState;
X, Y: Integer);
begin
if FSpecDrag and (ssLeft in Shift) then
DoSpectrumDrag(X, PbSpectrum.Width);
// Маркер следует за курсором — обновление произойдёт в ближайшем тике таймера (50ms)
if FSpecView.MarkerActive then
begin
FSpecView.MarkerX := Round(X / PbSpectrum.Width * 1000);
FSpectrumDirty := True;
end;
end;
procedure TMainForm.PbSpectrumMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
if Button = mbLeft then
begin
if (Abs(X - FSpecDragX0) < 4) then
DoSpectrumClick(X, PbSpectrum.Width); // это был клик, не драг
FSpecDrag := False;
PbSpectrum.Cursor := crDefault;
end;
end;
// Водопад (аналогично)
procedure TMainForm.PbWaterfallMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
if Button = mbLeft then
begin
if FSpecView.MarkerActive then
begin
FSpecView.MarkerActive := False;
PbSpectrum.Invalidate;
PbWaterfall.Invalidate;
end;
FSpecDrag := True;
FSpecDragX0 := X;
if FCTun then FSpecDragFreq := FCenterFreq
else begin
if FActiveVfo = 0 then FSpecDragFreq := FVfoA
else FSpecDragFreq := FVfoB;
end;
PbWaterfall.Cursor := crSizeWE;
end
else if Button = mbRight then
begin
if FSpecView.MarkerActive and (Abs(Round(X / PbWaterfall.Width * 1000) - FSpecView.MarkerX) < 8) then
FSpecView.MarkerActive := False
else
begin
FSpecView.MarkerActive := True;
FSpecView.MarkerX := Round(X / PbWaterfall.Width * 1000);
end;
PbSpectrum.Invalidate;
PbWaterfall.Invalidate;
end;
end;
procedure TMainForm.PbWaterfallMouseMove(Sender: TObject; Shift: TShiftState;
X, Y: Integer);
begin
if FSpecDrag and (ssLeft in Shift) then
DoSpectrumDrag(X, PbWaterfall.Width);
if FSpecView.MarkerActive then
begin
FSpecView.MarkerX := Round(X / PbWaterfall.Width * 1000);
FSpectrumDirty := True;
end;
end;
procedure TMainForm.PbWaterfallMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
if Button = mbLeft then
begin
if (Abs(X - FSpecDragX0) < 4) then
DoSpectrumClick(X, PbWaterfall.Width);
FSpecDrag := False;
PbWaterfall.Cursor := crDefault;
end;
end;
procedure TMainForm.BtnBandClick(Sender: TObject);
var
NewBand: Integer;
begin
NewBand := (Sender as TFlatButton).Tag;
if (NewBand < 0) or (NewBand >= BAND_COUNT) then Exit;
// Если в XVTR-режиме — DeactivateXvtr вернёт HF-состояние (RestoreBand
// на сохранённом FCurrentBand). Если NewBand совпадает с восстановленным
// FCurrentBand, дальше делать ничего не нужно.
if FCurrentXvtr >= 0 then
begin
DeactivateXvtr;
if NewBand = FCurrentBand then Exit;
end
else if NewBand = FCurrentBand then
Exit;
if FDevConnected then
begin
SaveCurrentBand;
FSettings.Save;
end;
FCurrentBand := NewBand;
RestoreBand(FCurrentBand);
end;
procedure TMainForm.ApplyModeFilter;
var
Lo, Hi, Half: Integer;
begin
Half := FFilterBW div 2;
case FMode of
0: begin Lo := -FFilterBW; Hi := -100; end; // LSB
1: begin Lo := 100; Hi := FFilterBW; end; // USB
2: begin Lo := -Half; Hi := Half; end; // DSB
3: begin Lo := -Half; Hi := Half; end; // CWL
4: begin Lo := -Half; Hi := Half; end; // CWU
5: begin Lo := -Half; Hi := Half; end; // FM
6: begin Lo := -Half; Hi := Half; end; // AM
7: begin Lo := -Half; Hi := Half; end; // SAM
else Lo := -Half; Hi := Half;
end;
if FWDSPReady then
begin
FDSPEngine.SetFilter(Lo, Hi);
FDSPEngine.SetTXFilter(Lo, Hi);
if FMode = MODE_FM then
begin
FDSPEngine.SetRXFMDeviation(FFMDeviation);
FDSPEngine.SetTXFMParams(FFMDeviation, FTXSettings.FMLowCut,
FTXSettings.FMHighCut, FTXSettings.FMEmphPosition);
FDSPEngine.SetFMSquelch(FFMSQOn, FFMSQLevel);
end
else
FDSPEngine.SetFMSquelch(False, 0);
end;
end;
procedure TMainForm.BtnModeClick(Sender: TObject);
var
i, N: Integer;
begin
N := (Sender as TFlatButton).Tag;
FMode := N;
for i := 0 to MODE_COUNT - 1 do StyleButton(BtnMode[i], i = FMode);
if FWDSPReady then
begin
FDSPEngine.SetMode(N);
// Если TUN активен — пересчитываем знак частоты под новый режим
if FTuning then
FDSPEngine.SetTXTone(True, FTXSettings.TUNFreq, 1.0);
end;
UpdateFilterButtons; // обновляем подписи и дефолт фильтра под новый режим
ApplyModeFilter;
SyncSpecViewFreq; // FSpecView.Mode/FilterBW — иначе полоса рисуется под старый режим
end;
procedure TMainForm.BtnFilterClick(Sender: TObject);
var
i, N: Integer;
begin
N := (Sender as TFlatButton).Tag;
FFilter := N;
case FMode of
0, 1: FFilterBW := FILT_SSB_BW[N];
2: FFilterBW := FILT_DSB_BW[N];
3, 4: FFilterBW := FILT_CW_BW[N];
5: begin
FFilterBW := FILT_FM_BW[N];
FFMDeviation := FILT_FM_DEV[N];
end;
else FFilterBW := FILT_AM_BW[N];
end;
for i := 0 to FILT_COUNT - 1 do StyleButton(BtnFilter[i], i = FFilter);
ApplyModeFilter;
SyncSpecViewFreq;
end;
// ---------------------------------------------------------------------------
// FM CTCSS helpers
// ---------------------------------------------------------------------------
procedure TMainForm.ApplyFMSquelch;
begin
if FWDSPReady then
FDSPEngine.SetFMSquelch(FFMSQOn and (FMode = MODE_FM), FFMSQLevel);
end;
procedure TMainForm.BtnFMSQClick(Sender: TObject);
begin
FFMSQOn := not FFMSQOn;
StyleButton(BtnFMSQ, FFMSQOn);
BtnFMSQ.Repaint;
ApplyFMSquelch;
SaveCurrentBand;
end;
procedure TMainForm.TrkFMSQChange(Sender: TObject);
begin
FFMSQLevel := TrkFMSQ.Position;
if LblFMSQ <> nil then LblFMSQ.Caption := IntToStr(FFMSQLevel);
ApplyFMSquelch;
SaveCurrentBand;
end;
procedure TMainForm.CloseCTCSSPopup;
begin
if (PanelFMCTCSSPop <> nil) and PanelFMCTCSSPop.Visible then
PanelFMCTCSSPop.Visible := False;
CloseFMStepPopup;
end;
procedure TMainForm.CloseFMStepPopup;
begin
if (PanelFMStepPop <> nil) and PanelFMStepPop.Visible then
PanelFMStepPop.Visible := False;
end;
procedure TMainForm.SetFMStep(Idx: Integer);
var OldIdx: Integer;
begin
OldIdx := FFMStepIdx;
FFMStepIdx := Idx;
if BtnFMStepSel <> nil then
begin
BtnFMStepSel.Caption := FM_STEP_NAMES[Idx];
BtnFMStepSel.Invalidate;
end;
if (OldIdx <> Idx) and (FBtnFMSteps[OldIdx] <> nil) then
FBtnFMSteps[OldIdx].Active := False;
if FBtnFMSteps[Idx] <> nil then
FBtnFMSteps[Idx].Active := True;
if FWebServer <> nil then FWebServer.FMStepIdx := Idx;
if (FMode = MODE_FM) and (FSpecView <> nil) then
FSpecView.FMGridStepHz := FM_STEP_HZ[Idx];
end;
procedure TMainForm.BtnFMStepClick(Sender: TObject);
begin
FFMStepOn := not FFMStepOn;
StyleButton(BtnFMStep, FFMStepOn);
BtnFMStep.Repaint;
SaveCurrentBand;
end;
procedure TMainForm.BtnFMStepSelClick(Sender: TObject);
var Y0: Integer;
begin
if PanelFMStepPop.Visible then
begin
CloseFMStepPopup;
Exit;
end;
CloseCTCSSPopup;
Y0 := PanelFMStep.Top + PanelFMStep.Height + 2;
if Y0 + PanelFMStepPop.Height > PanelLeft.ClientHeight then
Y0 := PanelFMStep.Top - PanelFMStepPop.Height - 2;
if Y0 < 0 then Y0 := 0;
PanelFMStepPop.Top := Y0;
PanelFMStepPop.Left := 0;
PanelFMStepPop.BringToFront;
PanelFMStepPop.Visible := True;
end;
procedure TMainForm.BtnFMStepSelectClick(Sender: TObject);
begin
CloseFMStepPopup;
SetFMStep((Sender as TFlatButton).Tag);
SaveCurrentBand;
end;
procedure TMainForm.SetFMCTCSSTone(Idx: Integer);
var
OldIdx: Integer;
begin
OldIdx := FFMCTCSSToneIdx;
FFMCTCSSToneIdx := Idx;
if BtnFMCTCSSTone <> nil then
begin
BtnFMCTCSSTone.Caption := CTCSS_NAMES[Idx];
BtnFMCTCSSTone.Invalidate;
end;
// Only flip Active on the two affected buttons — avoids 38× StyleButton/font overhead
if (OldIdx <> Idx) and (FBtnCTCSSTones[OldIdx] <> nil) then
FBtnCTCSSTones[OldIdx].Active := False;
if FBtnCTCSSTones[Idx] <> nil then
FBtnCTCSSTones[Idx].Active := True;
if FWDSPReady then
FDSPEngine.SetTXCTCSS(FFMCTCSSOn, CTCSS_TONES[Idx]);
end;
procedure TMainForm.BtnFMCTCSSClick(Sender: TObject);
begin
FFMCTCSSOn := not FFMCTCSSOn;
StyleButton(BtnFMCTCSS, FFMCTCSSOn);
BtnFMCTCSS.Repaint; // Flush visual state before WDSP call
if FWDSPReady then
FDSPEngine.SetTXCTCSS(FFMCTCSSOn, CTCSS_TONES[FFMCTCSSToneIdx]);
end;
procedure TMainForm.BtnFMCTCSSToneClick(Sender: TObject);
var
Y0: Integer;
begin
// Toggle the dropdown
if PanelFMCTCSSPop.Visible then
begin
CloseCTCSSPopup;
Exit;
end;
// Position below the CTCSS panel, or above if no room
Y0 := PanelFMCTCSS.Top + PanelFMCTCSS.Height + 2;
if Y0 + PanelFMCTCSSPop.Height > PanelLeft.ClientHeight then
Y0 := PanelFMCTCSS.Top - PanelFMCTCSSPop.Height - 2;
if Y0 < 0 then Y0 := 0;
PanelFMCTCSSPop.Top := Y0;
PanelFMCTCSSPop.Left := 0;
PanelFMCTCSSPop.BringToFront;
PanelFMCTCSSPop.Visible := True;
end;
procedure TMainForm.BtnFMToneSelectClick(Sender: TObject);
begin
CloseCTCSSPopup;
SetFMCTCSSTone((Sender as TFlatButton).Tag);
end;
procedure TMainForm.BtnVfoSwapClick(Sender: TObject);
var
Tmp: Double;
BandIdx: Integer;
ActiveFreq: Int64;
begin
// Меняем частоты местами
Tmp := FVfoA; FVfoA := FVfoB; FVfoB := Tmp;
// Определяем новую частоту активного VFO
if FActiveVfo = 0 then ActiveFreq := Round(FVfoA)
else ActiveFreq := Round(FVfoB);
// Перестраиваем приёмник
FCenterFreq := ActiveFreq;
if FWDSPReady then FDSPEngine.SetShift(0.0);
if FNetwork.Connected and FNetwork.Running then
begin
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True);
FNetwork.SendFullHP;
end;
// Обновляем кнопку диапазона
BandIdx := FreqToBandIdx(ActiveFreq);
if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then
begin
StyleButton(BtnBand[FCurrentBand], False);
FCurrentBand := BandIdx;
StyleButton(BtnBand[FCurrentBand], True);
end;
FSpecView.InvalidateRulerCache;
UpdateVfoDisplay;
SyncSpecViewFreq;
FSpecView.DrawSpectrum;
PbSpectrum.Invalidate;
if PbRuler <> nil then PbRuler.Invalidate;
end;
procedure TMainForm.BtnVfoACopyBClick(Sender: TObject);
begin
// A>B: копируем частоту A в B (VFO-B получает частоту VFO-A)
FVfoB := FVfoA;
if FActiveVfo = 1 then
begin
// B активен — перестраиваем приёмник на новую частоту B
FCenterFreq := Round(FVfoB);
if FWDSPReady then FDSPEngine.SetShift(0.0);
if FNetwork.Connected and FNetwork.Running then
begin
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True);
FNetwork.SendFullHP;
end;
FSpecView.InvalidateRulerCache;
SyncSpecViewFreq;
FSpecView.DrawSpectrum; PbSpectrum.Invalidate;
if PbRuler <> nil then PbRuler.Invalidate;
end;
FreqDispB.Frequency := Round(FVfoB);
UpdateVfoDisplay;
end;
procedure TMainForm.BtnVfoBCopyAClick(Sender: TObject);
begin
// B>A: копируем частоту B в A (VFO-A получает частоту VFO-B)
FVfoA := FVfoB;
if FActiveVfo = 0 then
ApplyVfoA(Round(FVfoA)) // A активен — перестраиваем приёмник
else
begin
FreqDispA.Frequency := Round(FVfoA);
UpdateVfoDisplay;
end;
end;
function TMainForm.ActiveTXFreqHz: Double;
begin
if FSplitTxB then Result := FVfoB
else if FActiveVfo = 0 then Result := FVfoA
else Result := FVfoB;
end;
procedure TMainForm.ApplyMOX(Active: Boolean);
var
WasTransmitting: Boolean;
begin
// Safety: block TX on bands marked DoNotTx in Alex config
if Active and FDevConnected and (FCurrentXvtr < 0)
and FAlexSettings.DoNotTx[EnsureRange(FCurrentBand, 0, 10)] then
Exit;
// Safety: block TX on RX-only transverter
if Active and (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT)
and FXvtrSettings.Entries[FCurrentXvtr].RXOnly then
Exit;
WasTransmitting := FTransmitting;
FTransmitting := Active;
FDUCPendingCount := 0;
if FTransmitting then
begin
BtnMOX.ClrNorm := TColor($00000044);
BtnMOX.ClrText := TColor($000000FF);
BtnMOX.ClrTextAct := TColor($000000FF);
BtnMOX.Active := True;
end else
StyleButton(BtnMOX, False);
// TX-overlay в SpectrumView: фиксируем актуальную TX-частоту/VFO-индекс и режим
FSpecView.TXFreq := ActiveTXFreqHz;
if FSplitTxB then FSpecView.TXVfoIndex := 1
else FSpecView.TXVfoIndex := FActiveVfo;
// DUP: данные остаются от RX-анализатора (TXMode=False), но TX-полоса
// фильтра рисуется поверх через TXOverlay. Без DUP при TX оба флага True.
FSpecView.TXMode := FTransmitting and not FDisplayDuplex;
FSpecView.TXOverlay := FTransmitting;
// Перерисовываем сетку спектра под активный тракт (Thetis-style).
ApplySpecViewGridFromState;
if FTransmitting and FNetwork.Connected and FNetwork.Running then
begin
FNetwork.ClearDUCIQQueue;
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel,
True, True, True);
FNetwork.SendFullHP; // 1й пакет: Alex bit27=1 (T/R relay), drive — без PTT
FNetwork.SendPTT(True); // 2й пакет: PTT=1 после переключения реле
end;
if FWDSPReady then
begin
// Переключаем источник микрофона: при TX из браузера — txmsWeb,
// при TX с кнопки без веб-клиента — штатный источник из настроек.
if FTransmitting and Assigned(FWebServer) and FWebServer.WebClientActive then
begin
FSavedMicSource := TTXMicSource(FTXSettings.MicSource);
FWebMicActive := True;
FDSPEngine.SetTXMicSource(txmsWeb);
end
else if not FTransmitting and FWebMicActive then
begin
FWebMicActive := False;
FDSPEngine.SetTXMicSource(FSavedMicSource);
end;
// В non-DUP RX-IQ пакеты дропаются на входе DSP-потока во время TX,
// чтобы TX leakage не накапливался в RXA pipeline и FFT-истории
// RX-анализатора. В DUP RX-тракт работает как обычно.
FDSPEngine.KeepRXDuringTX := FDisplayDuplex;
FDSPEngine.SetTXRun(FTransmitting);
if WasTransmitting and (not FTransmitting) and (not FDisplayDuplex) then
begin
FDSPEngine.FlushRX;
if Assigned(FAudioOut) then
FAudioOut.Clear;
{$IFDEF WINDOWS}
// Windows: на TX→RX дропаем И RX-IQ пакеты, И аудио-выход на это окно.
// Источник «хвоста» — radio FPGA TX-buffer + PA slew-down: после MOX-off
// радио дотравливает буфер передатчика, RX1 ловит это как leak, и WDSP
// отдаёт это и в водопад, и в звук. На Linux наблюдается окно <30мс,
// на Windows может тянуться 200–500мс. Подобрать значение можно здесь
// без перекомпиляции engine. На Linux код-путь не активируется.
FDSPEngine.BeginPostTXMute(250);
{$ENDIF}
end;
// SetTXRun переключает FActiveDisplayID на TX_DISP_ID; при DUP возвращаем
// источник на RX, чтобы видеть приём во время передачи.
if FTransmitting and FDisplayDuplex then
FDSPEngine.SetDisplaySourceTX(False);
end;
if (not FTransmitting) and FNetwork.Connected and FNetwork.Running then
begin
FNetwork.SendPTT(False); // 1й пакет: PTT=0 — сначала снимаем PTT
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel,
False, True, True);
FNetwork.ClearDUCIQQueue;
FNetwork.SendFullHP; // 2й пакет: Alex bit27=0 (T/R relay RX), drive=0
end;
end;
procedure TMainForm.BtnMOXClick(Sender: TObject);
begin
// Если идёт tune — клик MOX просто выходит из tune (без перехода на голос).
if FTuning then
begin
ApplyTUN(False);
Exit;
end;
ApplyMOX(not FTransmitting);
end;
procedure TMainForm.BtnMuteClick(Sender: TObject);
begin
FMuted := not FMuted;
StyleButton(BtnMute, FMuted);
if FMuted then BtnMute.Caption := 'UNMUTE'
else BtnMute.Caption := 'MUTE';
if FWDSPReady then FDSPEngine.SetMute(FMuted);
end;
procedure TMainForm.UpdateNRButton;
begin
case BtnNR.Tag of
1: BtnNR.Caption := 'NR';
2: BtnNR.Caption := 'NR2';
3: BtnNR.Caption := 'NR3';
4: BtnNR.Caption := 'NR4';
else
BtnNR.Caption := 'NR';
end;
StyleButton(BtnNR, BtnNR.Tag <> 0);
BtnNR.Invalidate;
end;
procedure TMainForm.UpdateNBButton;
begin
case BtnNB.Tag of
1: BtnNB.Caption := 'NB';
2: BtnNB.Caption := 'NB2';
else
BtnNB.Caption := 'NB';
end;
StyleButton(BtnNB, BtnNB.Tag <> 0);
BtnNB.Invalidate;
end;
procedure TMainForm.UpdateSNBButton;
begin
BtnSNB.Caption := 'SNB';
StyleButton(BtnSNB, BtnSNB.Tag <> 0);
BtnSNB.Invalidate;
end;
procedure TMainForm.UpdateANFButton;
begin
BtnANF.Caption := 'ANF';
StyleButton(BtnANF, BtnANF.Tag <> 0);
BtnANF.Invalidate;
end;
procedure TMainForm.ApplyNoiseFilterButtonsToDSP;
begin
if not FWDSPReady then Exit;
FDSPEngine.SetNRMode(BtnNR.Tag);
FDSPEngine.SetNBMode(BtnNB.Tag);
FDSPEngine.SetSNB(BtnSNB.Tag <> 0);
FDSPEngine.SetANF(BtnANF.Tag <> 0);
end;
procedure TMainForm.BtnNRClick(Sender: TObject);
begin
BtnNR.Tag := (BtnNR.Tag + 1) mod 5;
UpdateNRButton;
if FWDSPReady then FDSPEngine.SetNRMode(BtnNR.Tag);
end;
procedure TMainForm.BtnNBClick(Sender: TObject);
begin
BtnNB.Tag := (BtnNB.Tag + 1) mod 3;
UpdateNBButton;
if FWDSPReady then FDSPEngine.SetNBMode(BtnNB.Tag);
end;
procedure TMainForm.BtnSNBClick(Sender: TObject);
begin
BtnSNB.Tag := 1 - BtnSNB.Tag;
UpdateSNBButton;
if FWDSPReady then FDSPEngine.SetSNB(BtnSNB.Tag <> 0);
end;
procedure TMainForm.BtnANFClick(Sender: TObject);
begin
BtnANF.Tag := 1 - BtnANF.Tag;
UpdateANFButton;
if FWDSPReady then FDSPEngine.SetANF(BtnANF.Tag <> 0);
end;
procedure TMainForm.ApplyWfAGCNF(WfAGC, WfNF: Boolean);
begin
FWfAGCEnabled := WfAGC;
FWfNFEnabled := WfNF;
FSpecView.WfAGCEnabled := FWfAGCEnabled;
FSpecView.WfNFEnabled := FWfNFEnabled;
end;
procedure TMainForm.ApplyADCSettings(Dither, Random: Boolean);
begin
FDitherEnabled := Dither;
FRandomEnabled := Random;
if FNetwork.Connected and FRunning then
FNetwork.ConfigureDDCs(1, FSampleRate div 1000, 0, FDitherEnabled, FRandomEnabled);
if FDevConnected then
FSettings.SaveGlobal(FDevMAC, MakeGlobalSettings);
end;
procedure TMainForm.ApplyWebSettings(Enabled: Boolean; Port: Integer;
const BindAddr, User, Pass: string);
var W: TWebSettings;
begin
FWebEnabled := Enabled;
FWebPort := Port;
FWebBindAddr := BindAddr;
FWebUser := User;
FWebPass := Pass;
FWebServer.Reconfigure(User, Pass, BindAddr, Word(Port));
if Enabled then FWebServer.Start;
W.Enabled := Enabled;
W.Port := Port;
W.BindAddr := BindAddr;
W.User := User;
W.Pass := Pass;
FSettings.SaveWebSettings(W);
end;
procedure TMainForm.BtnHidePanelClick(Sender: TObject);
begin
FPanelHidden := not FPanelHidden;
if FPanelHidden then
begin
PanelLeft.Width := 0;
BtnHidePanel.Caption := '▶';
FVfoOverlay.Parent := PanelRight;
FVfoOverlay.Width := 260;
FVfoOverlay.Height := 130;
FVfoOverlay.SetState(FMode, FFilterBW, FVfoA, FLastSMeter);
FVfoOverlay.Visible := True;
FVfoOverlay.BringToFront;
PositionVfoOverlay;
end
else
begin
PanelLeft.Width := 232;
BtnHidePanel.Caption := '◀';
FVfoOverlay.Visible := False;
end;
ResizeSpectrumPanels;
ResizeSMeter;
end;
procedure TMainForm.PositionVfoOverlay;
var
VfoX, OW, SpTop: Integer;
begin
if not Assigned(FVfoOverlay) or not FVfoOverlay.Visible then Exit;
OW := FVfoOverlay.Width;
SpTop := PbSpectrum.Top; // смещение PbSpectrum внутри PanelRight
if PbSpectrum.Width > 0 then
VfoX := Round((FVfoA - FCenterFreq + FSpanHz / 2) / FSpanHz * PbSpectrum.Width)
else
VfoX := PbSpectrum.Width div 2;
// Всегда справа от VFO-линии
FVfoOverlay.Left := Min(PanelRight.Width - OW - 4, VfoX + 6);
FVfoOverlay.Top := SpTop + 6;
end;
procedure TMainForm.PbSpectrumDblClick(Sender: TObject);
begin
// Двойной клик — ничего не делаем (оверлей всегда виден при скрытой панели)
end;
procedure TMainForm.OnModeFilterSelect(Mode: Integer; FilterBW: Integer);
begin
FMode := Mode;
FFilterBW := FilterBW;
if FWDSPReady then
begin
FDSPEngine.SetMode(FMode);
ApplyModeFilter;
end;
SyncSpecViewFreq;
if Assigned(FVfoOverlay) and FVfoOverlay.Visible then
begin
FVfoOverlay.SetState(FMode, FFilterBW, FVfoA, FLastSMeter);
PositionVfoOverlay; // перепозиционируем при смене LSB↔USB
end;
end;
procedure TMainForm.BtnSpanClick(Sender: TObject);
var
NewRate: Integer;
DDCRate: Word;
begin
NewRate := (Sender as TFlatButton).Tag;
if NewRate = FSampleRate then Exit;
FSampleRate := NewRate;
FSpanHz := NewRate;
DDCRate := NewRate div 1000;
// Обновляем кнопки сразу — UI отзывчив
StyleSpanButton(BtnSpan48k, FSampleRate = 48000);
StyleSpanButton(BtnSpan96k, FSampleRate = 96000);
StyleSpanButton(BtnSpan192k, FSampleRate = 192000);
StyleSpanButton(BtnSpan384k, FSampleRate = 384000);
StyleSpanButton(BtnSpan768k, FSampleRate = 768000);
StyleSpanButton(BtnSpan1536k, FSampleRate = 1536000);
Application.ProcessMessages;
FBandCache[FCurrentBand].SpanHz := FSpanHz; // для обратной совместимости
// SampleRate — глобальный: сохраняем в global settings
if FDevConnected then
FSettings.SaveGlobal(FDevMAC, MakeGlobalSettings);
// 1. Останавливаем WDSP и сбрасываем очередь старых пакетов
if FWDSPReady then
begin
FWDSPReady := False;
FDSPEngine.ChangeSampleRate(NewRate); // Close + FlushQueue + Open
end;
// 2. Отправляем новый rate трансиверу
// (после остановки DSP — новые пакеты сразу идут с правильным rate)
if FNetwork.Connected then
FNetwork.ConfigureDDCs(1, DDCRate, 0, FDitherEnabled, FRandomEnabled);
// 3. Восстанавливаем настройки DSP
FWDSPReady := FDSPEngine.Initialized;
if FWDSPReady then
begin
FDSPEngine.SetMode(FMode);
FDSPEngine.SetVolume(FVolume / 100.0);
ApplyModeFilter;
ApplyNoiseFilterButtonsToDSP;
// ChangeSampleRate пересоздаёт WDSP-канал — восстанавливаем AGC
FDSPEngine.SetAGCTop(FAGCTop);
FDSPEngine.SetAGC(TWDSPAGCMode(FAGCMode), 50.0);
FDSPEngine.UpdateAGCLines(FSpectrumWidth);
if FNetwork.Connected then
FDSPEngine.SetShift(FVfoA - FCenterFreq);
end;
end;
// ===========================================================================
// Веб-интерфейс: callbacks от TWebServer (вызываются из WS-потока)
// Все изменения состояния выполняем через TThread.Synchronize чтобы
// не трогать UI и WDSP из чужого потока.
// ===========================================================================
procedure TMainForm.WebOnFreq(Hz: Double);
begin
FWebSyncFreq := Hz;
FWebSyncM := SyncWebFreq;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebFreq;
var BandIdx: Integer;
begin
// Веб шлёт "freq" для активного VFO
if FActiveVfo = 0 then
ApplyVfoA(Round(FWebSyncFreq))
else
begin
FVfoB := FWebSyncFreq;
FreqDispB.Frequency := Round(FVfoB);
FCenterFreq := FVfoB;
if FWDSPReady then FDSPEngine.SetShift(0.0);
if FRunning then
FNetwork.SetRunAndFreq(True, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), FDriveLevel);
BandIdx := FreqToBandIdx(FVfoB);
if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then
begin
StyleButton(BtnBand[FCurrentBand], False);
FCurrentBand := BandIdx;
StyleButton(BtnBand[FCurrentBand], True);
end;
FSpecView.InvalidateRulerCache;
SyncSpecViewFreq;
FSpecView.DrawSpectrum; PbSpectrum.Invalidate;
if PbRuler <> nil then PbRuler.Invalidate;
UpdateVfoDisplay;
end;
end;
procedure TMainForm.WebOnMode(Mode: Integer);
begin
FWebSyncInt := Mode;
FWebSyncM := SyncWebMode;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebMode;
var i: Integer;
begin
if (FWebSyncInt < 0) or (FWebSyncInt >= MODE_COUNT) then Exit;
FMode := FWebSyncInt;
for i := 0 to MODE_COUNT - 1 do StyleButton(BtnMode[i], i = FMode);
if FWDSPReady then FDSPEngine.SetMode(FMode);
UpdateFilterButtons;
ApplyModeFilter;
SyncSpecViewFreq;
FBandCache[FCurrentBand].Mode := FMode;
end;
procedure TMainForm.WebOnFilter(BW: Integer);
begin
FWebSyncInt := BW;
FWebSyncM := SyncWebFilter;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebFilter;
var
best, dist, i, j: Integer;
BW_arr: array[0..FILT_COUNT-1] of Integer;
Idx: Integer;
begin
// Negative value = -(idx+1) means filter index was passed directly
if FWebSyncInt < 0 then
begin
Idx := (-FWebSyncInt) - 1;
if (Idx >= 0) and (Idx < FILT_COUNT) then
begin
FFilter := Idx;
case FMode of
0,1: FFilterBW := FILT_SSB_BW[Idx];
2: FFilterBW := FILT_DSB_BW[Idx];
3,4: FFilterBW := FILT_CW_BW[Idx];
5: if Idx < FILT_FM_COUNT then
begin
FFilterBW := FILT_FM_BW[Idx];
FFMDeviation := FILT_FM_DEV[Idx];
end;
else FFilterBW := FILT_AM_BW[Idx];
end;
for j := 0 to FILT_COUNT-1 do StyleButton(BtnFilter[j], j = FFilter);
ApplyModeFilter;
SyncSpecViewFreq;
FBandCache[FCurrentBand].FilterBW := FFilterBW;
end;
Exit;
end;
// Positive value = bandwidth in Hz, find closest filter
FFilterBW := FWebSyncInt;
for i := 0 to FILT_COUNT-1 do BW_arr[i] := MaxInt;
case FMode of
0,1: for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_SSB_BW[i];
2: for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_DSB_BW[i];
3,4: for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_CW_BW[i];
5: for i := 0 to FILT_FM_COUNT-1 do BW_arr[i] := FILT_FM_BW[i];
else for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_AM_BW[i];
end;
best := 0; dist := MaxInt;
for i := 0 to FILT_COUNT-1 do
if Abs(BW_arr[i] - FWebSyncInt) < dist then
begin
dist := Abs(BW_arr[i] - FWebSyncInt);
best := i;
end;
FFilter := best;
if FMode = 5 then FFMDeviation := FILT_FM_DEV[best];
for j := 0 to FILT_COUNT-1 do StyleButton(BtnFilter[j], j = FFilter);
ApplyModeFilter;
SyncSpecViewFreq;
FBandCache[FCurrentBand].FilterBW := FFilterBW;
end;
procedure TMainForm.WebOnAGC(Mode: Integer);
begin
FWebSyncInt := Mode;
FWebSyncM := SyncWebAGC;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebAGC;
var i: Integer;
begin
if (FWebSyncInt < 0) or (FWebSyncInt > 4) then Exit;
FAGCMode := FWebSyncInt;
for i := 0 to 4 do StyleButton(BtnAGCMode[i], i = FAGCMode);
if FWDSPReady then
FDSPEngine.SetAGC(TWDSPAGCMode(FAGCMode), 50.0);
FBandCache[FCurrentBand].AGCMode := FAGCMode;
end;
procedure TMainForm.WebOnAGCTop(DB: Integer);
begin
FWebSyncInt := DB;
FWebSyncM := SyncWebAGCTop;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebAGCTop;
begin
FAGCTop := Max(20, Min(120, FWebSyncInt));
TrkAGC.Position := FAGCTop;
LblAGCTop.Caption := Format('%ddB', [FAGCTop]);
if FWDSPReady then
begin
FDSPEngine.SetAGCTop(FAGCTop);
FDSPEngine.SetAGC(TWDSPAGCMode(FAGCMode), 50.0);
end;
FBandCache[FCurrentBand].AGCTop := FAGCTop;
end;
procedure TMainForm.WebOnBand(Idx: Integer);
begin
FWebSyncInt := Idx;
FWebSyncM := SyncWebBand;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebBand;
begin
if (FWebSyncInt < 0) or (FWebSyncInt >= BAND_COUNT) then Exit;
// Выйти из XVTR при переходе на HF из web. DeactivateXvtr вернёт HF-состояние.
if FCurrentXvtr >= 0 then
begin
DeactivateXvtr;
if FWebSyncInt = FCurrentBand then Exit;
end
else if FWebSyncInt = FCurrentBand then
Exit;
if FDevConnected then begin SaveCurrentBand; FSettings.Save; end;
FCurrentBand := FWebSyncInt;
RestoreBand(FCurrentBand);
end;
procedure TMainForm.WebOnXvtrBand(Idx: Integer);
begin
FWebSyncInt := Idx;
FWebSyncM := SyncWebXvtrBand;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebXvtrBand;
begin
if FWebSyncInt < 0 then
begin
DeactivateXvtr;
Exit;
end;
if (FWebSyncInt >= CFG_XVTR_COUNT) then Exit;
if not FXvtrSettings.Entries[FWebSyncInt].Enabled then Exit;
if FWebSyncInt = FCurrentXvtr then Exit;
if FDevConnected then begin SaveCurrentBand; FSettings.Save; end;
ActivateXvtrBand(FWebSyncInt);
end;
procedure TMainForm.WebOnSpan(Hz: Integer);
begin
FWebSyncInt := Hz;
FWebSyncM := SyncWebSpan;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebSpan;
var
ValidSpans: array[0..5] of Integer;
Found: Boolean;
i: Integer;
Btn: TFlatButton;
begin
ValidSpans[0]:=48000; ValidSpans[1]:=96000; ValidSpans[2]:=192000;
ValidSpans[3]:=384000; ValidSpans[4]:=768000; ValidSpans[5]:=1536000;
Found := False;
for i := 0 to 5 do
if ValidSpans[i] = FWebSyncInt then begin Found := True; Break; end;
if not Found then Exit;
if FWebSyncInt = FSampleRate then Exit;
Btn := nil;
if FWebSyncInt = 48000 then Btn := BtnSpan48k
else if FWebSyncInt = 96000 then Btn := BtnSpan96k
else if FWebSyncInt = 192000 then Btn := BtnSpan192k
else if FWebSyncInt = 384000 then Btn := BtnSpan384k
else if FWebSyncInt = 768000 then Btn := BtnSpan768k
else if FWebSyncInt = 1536000 then Btn := BtnSpan1536k;
if Assigned(Btn) then BtnSpanClick(Btn);
end;
procedure TMainForm.WebOnVolume(V: Integer);
begin
FWebSyncInt := V;
FWebSyncM := SyncWebVolume;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebVolume;
begin
FVolume := Max(0, Min(100, FWebSyncInt));
TrkVolume.Position := FVolume;
if FWDSPReady then FDSPEngine.SetVolume(FVolume / 100.0);
end;
procedure TMainForm.WebOnWfAGC(On_: Boolean);
begin
FWebSyncBool := On_;
FWebSyncM := SyncWebWfAGC;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebWfAGC;
begin
FWfAGCEnabled := FWebSyncBool;
FSpecView.WfAGCEnabled := FWfAGCEnabled;
end;
procedure TMainForm.WebOnWfNF(On_: Boolean);
begin
FWebSyncBool := On_;
FWebSyncM := SyncWebWfNF;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebWfNF;
begin
FWfNFEnabled := FWebSyncBool;
FSpecView.WfNFEnabled := FWfNFEnabled;
end;
// ── Run ────────────────────────────────────────────────────────────────────
procedure TMainForm.WebOnRun(On_: Boolean);
begin
FWebSyncBool := On_;
FWebSyncM := SyncWebRun;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebRun;
begin
// Toggle only if state differs
if FWebSyncBool <> FRunning then
BtnStartStopClick(nil);
end;
// ── Mute ───────────────────────────────────────────────────────────────────
procedure TMainForm.WebOnMute(On_: Boolean);
begin
FWebSyncBool := On_;
FWebSyncM := SyncWebMute;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebMute;
begin
if FWebSyncBool <> FMuted then
BtnMuteClick(nil);
end;
// ── CTUN ──────────────────────────────────────────────────────────────────
procedure TMainForm.WebOnCtun(On_: Boolean);
begin
FWebSyncBool := On_;
FWebSyncM := SyncWebCtun;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebCtun;
begin
if FWebSyncBool <> FCTun then
BtnCTunClick(nil);
end;
// ── NR ────────────────────────────────────────────────────────────────────
procedure TMainForm.WebOnNR(Mode: Integer);
begin
FWebSyncInt := Mode;
FWebSyncM := SyncWebNR;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebNR;
begin
if FWebSyncInt < 0 then FWebSyncInt := 0;
if FWebSyncInt > 4 then FWebSyncInt := 4;
if BtnNR.Tag <> FWebSyncInt then
begin
BtnNR.Tag := FWebSyncInt;
UpdateNRButton;
if FWDSPReady then FDSPEngine.SetNRMode(BtnNR.Tag);
end;
end;
// ── NB ────────────────────────────────────────────────────────────────────
procedure TMainForm.WebOnNB(Mode: Integer);
begin
FWebSyncInt := Mode;
FWebSyncM := SyncWebNB;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebNB;
begin
if FWebSyncInt < 0 then FWebSyncInt := 0;
if FWebSyncInt > 2 then FWebSyncInt := 2;
if BtnNB.Tag <> FWebSyncInt then
begin
BtnNB.Tag := FWebSyncInt;
UpdateNBButton;
if FWDSPReady then FDSPEngine.SetNBMode(BtnNB.Tag);
end;
end;
procedure TMainForm.WebOnSNB(On_: Boolean);
begin
FWebSyncBool := On_;
FWebSyncM := SyncWebSNB;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebSNB;
begin
if FWebSyncBool <> (BtnSNB.Tag <> 0) then
BtnSNBClick(BtnSNB);
end;
// ── ANF ───────────────────────────────────────────────────────────────────
procedure TMainForm.WebOnANF(On_: Boolean);
begin
FWebSyncBool := On_;
FWebSyncM := SyncWebANF;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebANF;
begin
if FWebSyncBool <> (BtnANF.Tag <> 0) then
BtnANFClick(BtnANF);
end;
// ── VFO-B freq (из веба) ─────────────────────────────────────────────────
procedure TMainForm.WebOnFreqB(Hz: Double);
begin
FWebSyncFreq := Hz;
FWebSyncM := SyncWebFreqB;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebFreqB;
var BandIdx: Integer;
begin
FVfoB := FWebSyncFreq;
FreqDispB.Frequency := Round(FVfoB);
if FActiveVfo = 1 then
begin
FCenterFreq := FVfoB;
if not FCTun then
if FWDSPReady then FDSPEngine.SetShift(0.0);
if FRunning then
FNetwork.SetRunAndFreq(True, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), FDriveLevel);
BandIdx := FreqToBandIdx(FVfoB);
if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then
begin
StyleButton(BtnBand[FCurrentBand], False);
FCurrentBand := BandIdx;
StyleButton(BtnBand[FCurrentBand], True);
end;
FSpecView.InvalidateRulerCache;
SyncSpecViewFreq;
FSpecView.DrawSpectrum; PbSpectrum.Invalidate;
if PbRuler <> nil then PbRuler.Invalidate;
end;
UpdateVfoDisplay;
end;
// ── Активный VFO (из веба) ───────────────────────────────────────────────
procedure TMainForm.WebOnActiveVfo(Idx: Integer);
begin
FWebSyncInt := Idx;
FWebSyncM := SyncWebActiveVfo;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebActiveVfo;
begin
if (FWebSyncInt < 0) or (FWebSyncInt > 1) then Exit;
if FWebSyncInt = FActiveVfo then Exit;
ActivateVfo(FWebSyncInt);
end;
// ── CTUN drag center scroll (из веба) ────────────────────────────────────
procedure TMainForm.WebOnCenter(Hz: Double);
begin
FWebSyncFreq := Hz;
FWebSyncM := SyncWebCenter;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebCenter;
begin
// Сдвигаем DDC-центр без смены VFO (аналог CTUN-drag в десктопе)
FCenterFreq := FWebSyncFreq;
if FWDSPReady then
begin
if FActiveVfo = 0 then FDSPEngine.SetShift(FVfoA - FCenterFreq)
else FDSPEngine.SetShift(FVfoB - FCenterFreq);
end;
if FNetwork.Connected and FNetwork.Running then
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True);
FSpecView.InvalidateRulerCache;
FSpectrumDirty := True;
end;
procedure TMainForm.WebOnMOX(On_: Boolean);
begin
FWebSyncBool := On_;
FWebSyncM := SyncWebMOX;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebMOX;
begin
ApplyMOX(FWebSyncBool);
end;
procedure TMainForm.WebOnDrive(V: Integer);
begin
FWebSyncInt := V;
FWebSyncM := SyncWebDrive;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebDrive;
begin
// Установка Position вызывает TrkDriveChange через OnChange автоматически
TrkDrive.Position := FWebSyncInt;
end;
procedure TMainForm.WebOnFreqA(Hz: Double);
begin
FWebSyncFreq := Hz;
FWebSyncM := SyncWebFreqA;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebFreqA;
begin
ApplyVfoA(Round(FWebSyncFreq));
end;
procedure TMainForm.WebOnAttn(Idx: Integer);
begin
FWebSyncInt := Idx;
FWebSyncM := SyncWebAttn;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebAttn;
begin
FAtten := Max(0, Min(2, FWebSyncInt));
FNetwork.SetStepAtten(FAtten * 10);
end;
procedure TMainForm.WebOnFMStep(Idx: Integer);
begin
FWebSyncInt := Idx;
FWebSyncM := SyncWebFMStep;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebFMStep;
begin
SetFMStep(Max(0, Min(FM_STEP_COUNT - 1, FWebSyncInt)));
SaveCurrentBand;
end;
procedure TMainForm.WebOnTun(On_: Boolean);
begin
FWebSyncBool := On_;
FWebSyncM := SyncWebTun;
TThread.Synchronize(nil, FWebSyncM);
end;
procedure TMainForm.SyncWebTun;
begin
ApplyTUN(FWebSyncBool);
end;
procedure TMainForm.WebOnMic(Samples: PSingle; Count: Integer);
var
Singles: array[0..5759] of Single;
Buf: array[0..5759] of Double;
i, n: Integer;
begin
if not FWDSPReady then Exit;
n := Count;
if n > Length(Buf) then n := Length(Buf);
Move(Samples^, Singles[0], n * SizeOf(Single));
for i := 0 to n - 1 do
Buf[i] := Singles[i];
FDSPEngine.PushTXMicSamplesD(Buf, n);
end;
procedure TMainForm.TrkDriveChange(Sender: TObject);
begin
FDriveLevel := CalcDriveByte;
if FWDSPReady then
FDSPEngine.SetDriveLevel(TrkDrive.Position / 100.0);
if FNetwork.Connected and FNetwork.Running then
begin
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True);
FNetwork.SendFullHP;
end;
end;
function TMainForm.CalcDriveByte: Byte;
var
Cal: Double;
Pos: Integer;
XvtrActive: Boolean;
begin
if (FCurrentBand >= 0) and (FCurrentBand < BAND_COUNT) and (FPABandCal[FCurrentBand] > 0.0) then
Cal := FPABandCal[FCurrentBand]
else
Cal := 100.0;
XvtrActive := (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and
FXvtrSettings.Entries[FCurrentXvtr].Enabled;
// Pos — итоговый процент драйва 0..100 ДО per-band калибровки.
//
// Логика (как в Thetis):
// • Регулярный TX:
// HF → Pos = TrkDrive.Position
// XVTR → Pos = TrkDrive.Position × (XVTR.TXPower / 100)
// (TXPower работает как верхний предел драйва на трансверторе)
// • TUN (кнопка):
// HF → Pos = TUNLevel (TTXSettings.TUNLevel)
// XVTR, UseXVTRTunePower=False → Pos = TUNLevel (поведение как HF)
// XVTR, UseXVTRTunePower=True → Pos = XVTR.TXPower
// (галка ЗАМЕНЯЕТ TUNLevel на XVTR power — Thetis-семантика)
if FTuning then
begin
if XvtrActive and FXvtrSettings.UseXVTRTunePower then
Pos := EnsureRange(FXvtrSettings.Entries[FCurrentXvtr].TXPower, 0, 100)
else
Pos := EnsureRange(FTXSettings.TUNLevel, 0, 100);
end
else
begin
if XvtrActive then
Pos := Round(TrkDrive.Position
* FXvtrSettings.Entries[FCurrentXvtr].TXPower / 100.0)
else
Pos := TrkDrive.Position;
end;
Result := Round(Pos / 100.0 * Cal / 100.0 * 255.0);
end;
procedure TMainForm.OnTXIQReady(const Buf: array of Double; Count: Integer);
// FTXOutBufSize не обязан быть кратен 240, поэтому копим хвост между
// вызовами и шлём только полные DUC-пакеты. Иначе зануление хвоста
// последнего пакета давало периодический провал на каждом TX-блоке
// (≈47 Гц гул при 192k IQ rate). Также клампим в 24-bit чтобы пики
// от компрессора/EQ (значение чуть >1.0) не оборачивались через wrap.
function ScaleIQ24(V: Double): Integer;
var S: Double;
begin
S := V * 8388607.0;
if S > 8388607.0 then Result := 8388607
else if S < -8388608.0 then Result := -8388608
else Result := Round(S);
end;
var
i: Integer;
begin
if not FNetwork.Connected then Exit;
for i := 0 to Count - 1 do
begin
FDUCPendingI[FDUCPendingCount] := ScaleIQ24(Buf[i * 2]);
FDUCPendingQ[FDUCPendingCount] := ScaleIQ24(Buf[i * 2 + 1]);
Inc(FDUCPendingCount);
if FDUCPendingCount >= 240 then
begin
FNetwork.SendDUCIQ(FDUCPendingI, FDUCPendingQ);
FDUCPendingCount := 0;
end;
end;
end;
procedure TMainForm.OnPASettingsChange(MaxPower: Double;
const BandCal: array of Double);
var i: Integer;
begin
FPAMaxPower := MaxPower;
if FSpecView <> nil then FSpecView.PAMaxPower := MaxPower;
for i := 0 to BAND_COUNT - 1 do
begin
if i <= High(BandCal) then
FPABandCal[i] := BandCal[i]
else
FPABandCal[i] := 100.0;
end;
FDriveLevel := CalcDriveByte;
if FNetwork.Connected and FNetwork.Running then
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True);
end;
procedure TMainForm.OnTXSettingsChange(const T: TTXSettings);
// Любая правка в Settings→Transmit летит сюда: обновляем FTXSettings,
// перешиваем WDSP TX-цепь и (если поменялись mic-биты) перепосылаем DUC Specific.
var
OldMicByte, NewMicByte: Byte;
OldAtt, OldTUNLevel, OldTUNFreq: Integer;
GridChanged: Boolean;
begin
OldMicByte := BuildMicLineSelectByte;
OldAtt := FTXSettings.AttOnTX;
OldTUNLevel := FTXSettings.TUNLevel;
OldTUNFreq := FTXSettings.TUNFreq;
GridChanged := (FTXSpecRefLevel <> T.TXSpecRefLevel)
or (FTXSpecRange <> T.TXSpecRange)
or (FTXSpecGridStep <> T.TXSpecGridStep);
FTXSettings := T;
NewMicByte := BuildMicLineSelectByte;
// TX-grid в момент правки — обновляем рабочие поля и (если на передаче) пушим в FSpecView
FTXSpecRefLevel := FTXSettings.TXSpecRefLevel;
FTXSpecRange := FTXSettings.TXSpecRange;
if FTXSettings.TXSpecGridStep > 0 then
FTXSpecGridStep := FTXSettings.TXSpecGridStep;
if GridChanged and FTransmitting then
ApplySpecViewGridFromState;
ApplyTXSettingsToDSP;
if (OldMicByte <> NewMicByte) or (OldAtt <> FTXSettings.AttOnTX) then
SendDUCSpecificFromSettings;
// TUN-параметры live: если идёт tune — обновить тон/drive, иначе просто запомнить.
if FTuning then
begin
if (OldTUNFreq <> FTXSettings.TUNFreq) and FWDSPReady then
FDSPEngine.SetTXTone(True, FTXSettings.TUNFreq, 1.0);
if OldTUNLevel <> FTXSettings.TUNLevel then
begin
FDriveLevel := CalcDriveByte;
if FWDSPReady then
FDSPEngine.SetDriveLevel(FTXSettings.TUNLevel / 100.0);
if FNetwork.Connected and FNetwork.Running then
begin
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel,
FTransmitting, True, True);
FNetwork.SendFullHP;
end;
end;
end;
// Сохраняем сразу — настройки per-device, попадают в hpsdr_settings.json
if FDevConnected then
begin
FSettings.SaveTX(FDevMAC, FTXSettings);
FSettings.Save;
end;
end;
procedure TMainForm.OnAlexSettingsChange(const A: TAlexSettings);
begin
FAlexSettings := A;
if FNetwork.Connected then
FNetwork.SetAlexConfig(A);
if FDevConnected then
begin
FSettings.SaveAlex(FDevMAC, A);
FSettings.Save;
end;
end;
// ===========================================================================
// XVTR (Transverter) integration
// ===========================================================================
// Ищет XVTR слот, в чьё [FreqBegin..FreqEnd] попадает данная видимая частота.
// Возвращает индекс 0..CFG_XVTR_COUNT-1 или -1.
function TMainForm.FindXvtrIdxForFreq(FreqHz: Double): Integer;
var i: Integer;
begin
Result := -1;
for i := 0 to CFG_XVTR_COUNT - 1 do
with FXvtrSettings.Entries[i] do
if Enabled and (FreqHz >= FreqBegin) and (FreqHz <= FreqEnd) then
Exit(i);
end;
// Транслирует видимую частоту в IF (то, что слышит/передаёт трансивер).
// Если активный XVTR не задан — возвращает входную частоту без изменений.
function TMainForm.XvtrTranslate(VisibleHz: Double): Double;
begin
Result := VisibleHz;
if (FCurrentXvtr < 0) or (FCurrentXvtr >= CFG_XVTR_COUNT) then Exit;
if not FXvtrSettings.Entries[FCurrentXvtr].Enabled then Exit;
// f_IF = f_visible - LOOffset + LOError
Result := VisibleHz
- FXvtrSettings.Entries[FCurrentXvtr].LOOffset
+ FXvtrSettings.Entries[FCurrentXvtr].LOError;
end;
// Передаёт XVTR-режим в сетевой слой. Вызывается при смене XVTR или его
// настроек. Управляет XVTR Enable bit, DDC IN routing, T/R relay suppression.
procedure TMainForm.ApplyXvtrToNetwork;
var
En: Boolean;
UseDDCIn: Boolean;
DisablePA: Boolean;
RxAnt: Byte;
begin
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and
FXvtrSettings.Entries[FCurrentXvtr].Enabled then
begin
En := True;
UseDDCIn := True;
DisablePA := FXvtrSettings.Entries[FCurrentXvtr].DisablePA;
RxAnt := FXvtrSettings.Entries[FCurrentXvtr].RXAntenna;
end
else
begin
En := False; UseDDCIn := False; DisablePA := False; RxAnt := 0;
end;
if FNetwork.Connected then
FNetwork.SetXvtrMode(En, UseDDCIn, DisablePA, RxAnt);
end;
// Перестраивает динамические XVTR-кнопки в PanelBands. Удаляет старые,
// создаёт новые для enabled слотов в 3-м и 4-м ряду PanelBands.
procedure TMainForm.RelayoutBelowBands;
var
Y: Integer;
begin
if PanelBands = nil then Exit;
Y := PanelBands.Top + PanelBands.Height + 2;
PanelMode.Top := Y; Y := Y + PanelMode.Height + 2;
PanelFilter.Top := Y; Y := Y + PanelFilter.Height + 2;
if (PanelFMStep <> nil) and PanelFMStep.Visible then
begin
PanelFMStep.Top := Y; Y := Y + PanelFMStep.Height + 2;
end;
if (PanelFMSQ <> nil) and PanelFMSQ.Visible then
begin
PanelFMSQ.Top := Y; Y := Y + PanelFMSQ.Height + 2;
end;
if (PanelFMCTCSS <> nil) and PanelFMCTCSS.Visible then
begin
PanelFMCTCSS.Top := Y; Y := Y + PanelFMCTCSS.Height + 2;
end;
BtnCTun.Top := Y; BtnDUP.Top := Y; Y := Y + BtnCTun.Height + 4;
PanelRX.Top := Y;
end;
procedure TMainForm.RebuildXvtrButtons;
var
i, Pos, XvtrRows: Integer;
W, BtnH, RowH, MaxBottom: Integer;
B: TFlatButton;
Cap: string;
begin
// Уничтожаем все старые
for i := 0 to CFG_XVTR_COUNT - 1 do
if BtnXvtrBand[i] <> nil then
begin
BtnXvtrBand[i].Free;
BtnXvtrBand[i] := nil;
end;
if PanelBands = nil then Exit;
// Измеряем фактический bottom последнего ряда обычных кнопок BAND
// (уже отмасштабированы LCL при показе формы)
MaxBottom := 0;
BtnH := 0;
for i := 0 to BAND_COUNT - 1 do
if BtnBand[i] <> nil then
begin
if BtnBand[i].Top + BtnBand[i].Height > MaxBottom then
MaxBottom := BtnBand[i].Top + BtnBand[i].Height;
BtnH := BtnBand[i].Height;
end;
if BtnH = 0 then BtnH := MulDiv(24, Screen.PixelsPerInch, 96);
RowH := BtnH + 3; // шаг ряда: высота кнопки + 3px зазор (как 27=24+3 при 1x)
W := (PanelBands.Width - 6) div 6;
Pos := 0; // позиция среди enabled слотов
for i := 0 to CFG_XVTR_COUNT - 1 do
if FXvtrSettings.Entries[i].Enabled then
begin
if Pos >= 12 then Break; // 2 ряда по 6 кнопок
Cap := FXvtrSettings.Entries[i].ButtonText;
if Cap = '' then Cap := 'X' + IntToStr(i + 1);
B := TFlatButton.Create(Self);
B.Parent := PanelBands;
B.Caption := Cap;
B.Left := 2 + (Pos mod 6) * W;
B.Top := MaxBottom + 4 + (Pos div 6) * RowH;
B.Width := W - 2;
B.Height := BtnH;
B.OnClick := BtnXvtrBandClick;
B.Tag := -(i + 1); // отрицательное значение = XVTR индекс
BtnXvtrBand[i] := B;
StyleButton(B, i = FCurrentXvtr);
Inc(Pos);
end;
// Динамически подгоняем высоту PanelBands под реальное содержимое
if Pos = 0 then XvtrRows := 0
else if Pos <= 6 then XvtrRows := 1
else XvtrRows := 2;
if XvtrRows = 0 then
PanelBands.Height := MaxBottom + 6
else
PanelBands.Height := MaxBottom + 4 + XvtrRows * RowH + 6;
RelayoutBelowBands;
end;
// Обработчик клика по XVTR-кнопке. Tag = -(idx+1).
procedure TMainForm.BtnXvtrBandClick(Sender: TObject);
var
TagVal, Idx: Integer;
begin
TagVal := (Sender as TFlatButton).Tag;
Idx := -TagVal - 1;
if (Idx < 0) or (Idx >= CFG_XVTR_COUNT) then Exit;
if not FXvtrSettings.Entries[Idx].Enabled then Exit;
if FCurrentXvtr = Idx then Exit; // уже на этом XVTR
// Сохраняем текущий band перед сменой (как BtnBandClick)
if FDevConnected then
begin
SaveCurrentBand;
FSettings.Save;
end;
ActivateXvtrBand(Idx);
end;
// Активирует XVTR-band: устанавливает видимую частоту по LastFreq или
// середине [FreqBegin..FreqEnd], применяет network XVTR-режим.
procedure TMainForm.ActivateXvtrBand(Idx: Integer);
var
Vis: Double;
i: Integer;
E: TXvtrEntry;
begin
if (Idx < 0) or (Idx >= CFG_XVTR_COUNT) then Exit;
if not FXvtrSettings.Entries[Idx].Enabled then Exit;
FCurrentXvtr := Idx;
E := FXvtrSettings.Entries[Idx];
// Выбор начальной частоты: LastFreq если в диапазоне, иначе середина
Vis := E.LastFreq;
if (Vis < E.FreqBegin) or (Vis > E.FreqEnd) then
Vis := (E.FreqBegin + E.FreqEnd) / 2.0;
// Отключаем кнопки HF band
for i := 0 to BAND_COUNT - 1 do
StyleButton(BtnBand[i], False);
// Подсвечиваем активную XVTR-кнопку
for i := 0 to CFG_XVTR_COUNT - 1 do
if BtnXvtrBand[i] <> nil then
StyleButton(BtnXvtrBand[i], i = Idx);
// Применяем XVTR режим к сети (XVTR enable bit + DDC IN + DisablePA + RX ant)
ApplyXvtrToNetwork;
// Восстанавливаем Mode + FM Squelch + CTCSS для этого XVTR-слота
FFMSQOn := E.LastFMSQOn;
FFMSQLevel := E.LastFMSQLevel;
FMode := E.LastMode;
FFilter := E.LastFilterIdx;
for i := 0 to MODE_COUNT - 1 do StyleButton(BtnMode[i], i = FMode);
if FWDSPReady then FDSPEngine.SetMode(FMode);
UpdateFilterButtons; // показывает/скрывает FM-панели, обновляет SQL UI
ApplyModeFilter;
FFMCTCSSOn := E.LastCTCSSOn;
if BtnFMCTCSS <> nil then StyleButton(BtnFMCTCSS, FFMCTCSSOn);
SetFMCTCSSTone(E.LastCTCSSToneIdx);
ApplyFMSquelch;
FFMStepOn := E.LastFMStepOn;
SetFMStep(E.LastFMStepIdx);
if BtnFMStep <> nil then StyleButton(BtnFMStep, FFMStepOn);
// AGC
FAGCMode := E.LastAGCMode;
FAGCTop := E.LastAGCTop;
for i := 0 to 4 do StyleButton(BtnAGCMode[i], i = FAGCMode);
TrkAGC.Position := FAGCTop;
LblAGCTop.Caption := Format('%ddB', [FAGCTop]);
if FWDSPReady then
begin
FDSPEngine.SetAGCTop(FAGCTop);
FDSPEngine.SetAGC(TWDSPAGCMode(FAGCMode), 50.0);
end;
FSpecView.AGCTop := FAGCTop;
// CTUN
FCTun := E.LastCTun;
StyleButton(BtnCTun, FCTun);
// VFO на видимую частоту; FCenterFreq тоже visible
FVfoA := Vis;
FCenterFreq := Vis;
FreqDispA.Frequency := Round(FVfoA);
if FWDSPReady then FDSPEngine.SetShift(0.0);
// Применяем драйв (если есть кастомный TX power для этого XVTR)
// Сделано на стороне ActiveTXFreqHz / TX path — здесь только обновляем дисплей.
// Отправляем в сеть с трансляцией
if FNetwork.Connected and FNetwork.Running then
begin
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz),
FDriveLevel, FTransmitting, True, True);
FNetwork.SendFullHP;
end;
SyncSpecViewFreq;
FSpecView.DrawSpectrum;
PbSpectrum.Invalidate;
FSpecView.DrawWaterfall;
PbWaterfall.Invalidate;
if PbRuler <> nil then PbRuler.Invalidate;
PushXvtrToWeb;
end;
// Деактивирует XVTR — возвращает к HF-режиму.
// FCurrentBand был сохранён неизменным во время XVTR (см. ActivateXvtrBand,
// ApplyVfoA, SaveCurrentBand) — он указывает на HF-диапазон, с которого
// пользователь зашёл в трансвертер. RestoreBand вернёт VFO/Mode/Filter/AGC
// /CTUN/Span этого диапазона и обновит подсветку HF band-кнопки.
procedure TMainForm.DeactivateXvtr;
var i: Integer;
begin
if FCurrentXvtr < 0 then Exit;
// Запоминаем LastFreq для bandstack
if FCurrentXvtr < CFG_XVTR_COUNT then
begin
FXvtrSettings.Entries[FCurrentXvtr].LastFreq := FVfoA;
if FDevConnected then
begin
FSettings.SaveXvtr(FDevMAC, FXvtrSettings);
FSettings.Save;
end;
end;
FCurrentXvtr := -1;
// Сбрасываем подсветку XVTR-кнопок
for i := 0 to CFG_XVTR_COUNT - 1 do
if BtnXvtrBand[i] <> nil then
StyleButton(BtnXvtrBand[i], False);
ApplyXvtrToNetwork;
// Восстанавливаем HF band: VFO/Mode/Filter/AGC/CTUN/Span из кэша.
// RestoreBand сам обновит подсветку HF band-кнопки и сетевое состояние.
if (FCurrentBand >= 0) and (FCurrentBand < CFG_BAND_COUNT) then
RestoreBand(FCurrentBand);
PushXvtrToWeb;
end;
procedure TMainForm.OnXvtrSettingsChange(const X: TXvtrSettings);
begin
FXvtrSettings := X;
RebuildXvtrButtons;
// Если активный XVTR стал disabled — выйти из XVTR режима
if (FCurrentXvtr >= 0) and
((FCurrentXvtr >= CFG_XVTR_COUNT) or
(not FXvtrSettings.Entries[FCurrentXvtr].Enabled)) then
DeactivateXvtr;
// Перепрошить XVTR в сеть (если параметры текущего изменились)
ApplyXvtrToNetwork;
PushXvtrToWeb;
if FDevConnected then
begin
FSettings.SaveXvtr(FDevMAC, FXvtrSettings);
FSettings.Save;
end;
end;
procedure TMainForm.PushXvtrToWeb;
var
Arr: TWebXvtrArray;
i, Cnt: Integer;
begin
if FWebServer = nil then Exit;
Cnt := 0;
for i := 0 to CFG_XVTR_COUNT - 1 do
if FXvtrSettings.Entries[i].Enabled then Inc(Cnt);
SetLength(Arr, Cnt);
Cnt := 0;
for i := 0 to CFG_XVTR_COUNT - 1 do
if FXvtrSettings.Entries[i].Enabled then
begin
Arr[Cnt].Idx := i;
Arr[Cnt].Name := FXvtrSettings.Entries[i].ButtonText;
Inc(Cnt);
end;
FWebServer.SetXvtrBands(Arr, FCurrentXvtr);
end;
procedure TMainForm.TrkVolumeChange(Sender: TObject);
begin
FVolume := TrkVolume.Position;
if FWDSPReady then
FDSPEngine.SetVolume(FVolume / 100.0);
end;
// ===========================================================================
// Mouse wheel — перестройка VFO
// ===========================================================================
procedure TMainForm.FormMouseWheel(Sender: TObject; Shift: TShiftState;
WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean);
// Колёсико на форме/спектре/водопаде — меняет VFO с квантованием
// Шаг зависит от модификаторов: Ctrl+Shift=1МГц, Ctrl=1кГц, Shift=10Гц, иначе 100Гц
// Квантование: результат выровнен до кратного шагу (как в FreqDisplay)
// CTUN логика: через ApplyVfoA
var
Step: Int64;
Delta: Int64;
Base: Int64;
NewFreq: Int64;
begin
if (FMode = MODE_FM) and FFMStepOn and
not ((ssCtrl in Shift) or (ssShift in Shift)) then
Step := FM_STEP_HZ[FFMStepIdx]
else if (ssCtrl in Shift) and (ssShift in Shift) then Step := 1000000
else if ssCtrl in Shift then Step := 1000
else if ssShift in Shift then Step := 10
else Step := 100;
Delta := 1;
if WheelDelta < 0 then Delta := -1;
// Квантование (та же логика что в FreqDisplay.ChangeByDigit)
Base := (Round(FVfoA) div Step) * Step;
if Delta > 0 then
NewFreq := Base + Step
else
begin
if Round(FVfoA) = Base then
NewFreq := Base - Step
else
NewFreq := Base; // снэп к нижней границе
end;
// В XVTR-режиме HF-клэмп не применяем: ApplyVfoA сам ограничит по FreqBegin/FreqEnd
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and
FXvtrSettings.Entries[FCurrentXvtr].Enabled then
NewFreq := Max(0, NewFreq)
else
NewFreq := Max(30000, Min(60000000, NewFreq));
ApplyVfoA(NewFreq);
Handled := True;
end;
procedure TMainForm.BtnAGCModeClick(Sender: TObject);
const
AGCModes: array[0..4] of TWDSPAGCMode = (
agcFast, agcMedium, agcSlow, agcLong, agcOff);
var
i, N: Integer;
begin
N := (Sender as TFlatButton).Tag;
FAGCMode := N;
for i := 0 to 4 do StyleButton(BtnAGCMode[i], i = N);
if FWDSPReady then
FDSPEngine.SetAGC(AGCModes[N], 50.0);
FBandCache[FCurrentBand].AGCMode := FAGCMode;
end;
procedure TMainForm.TrkAGCChange(Sender: TObject);
begin
FAGCTop := TrkAGC.Position;
LblAGCTop.Caption := Format('%ddB', [FAGCTop]);
if FWDSPReady then
begin
FDSPEngine.SetAGCTop(FAGCTop);
FDSPEngine.SetAGC(TWDSPAGCMode(FAGCMode), 50.0);
end;
FBandCache[FCurrentBand].AGCTop := FAGCTop;
FSpecView.AGCTop := FAGCTop;
FSpecView.DrawSpectrum;
PbSpectrum.Invalidate;
end;
// ===========================================================================
// DSP/Audio callbacks — вызываются из рабочих потоков
// ===========================================================================
procedure TMainForm.OnAudioReady(const Left, Right: array of Single;
Count: Integer);
var
MonoBuf: array[0..1023] of Single;
i: Integer;
begin
if Assigned(FWebServer) and FWebServer.WebClientActive then
begin
for i := 0 to Count - 1 do
MonoBuf[i] := (Left[i] + Right[i]) * 0.5;
FWebServer.PushAudio(@MonoBuf[0], Count);
Exit;
end;
FAudioOut.Write(Left, Right, Count);
end;
procedure TMainForm.OnSpectrumReady(const Pixels: array of Single;
Count: Integer);
var
i, N: Integer;
begin
N := Min(Count, 1024);
for i := 0 to N - 1 do
FSpectrumBuf[i] := Pixels[i]; // local copy for WebServer
FSpectrumBufCount := N;
FSpecView.SetSpectrumData(Pixels, Count);
FSpectrumDirty := True;
end;
procedure TMainForm.OnWaterfallReady(const Pixels: array of Single;
Count: Integer);
var
i, N: Integer;
begin
N := Min(Count, 1024);
for i := 0 to N - 1 do
FWaterfallBuf[i] := Pixels[i]; // local copy for WebServer
FWaterfallBufCount := N;
Inc(FWaterfallFrameCounter);
if FWaterfallFrameCounter >= Max(1, FWaterfallFrameInterval) then
begin
FWaterfallFrameCounter := 0;
FSpecView.SetWaterfallData(Pixels, Count);
FWaterfallDirty := True;
end;
end;
// ===========================================================================
// Settings apply methods (вызываются из TSettingsForm)
// ===========================================================================
procedure TMainForm.ApplyDisplayParams(FFTSize, WinType, SpecDet, SpecAvgMode: Integer;
SpecAvgTimeMS: Double);
begin
if FWDSPReady then
FDSPEngine.SetSpectrumDisplay(SpecDet, SpecAvgMode, SpecAvgTimeMS);
if FWDSPReady then
FDSPEngine.SetFFTParams(FFTSize, WinType);
FSpectrumDirty := True;
end;
procedure TMainForm.ApplyWaterfallParams(WfDet, WfAvgMode: Integer;
WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double);
begin
if FWDSPReady then
FDSPEngine.SetWaterfallDisplay(WfDet, WfAvgMode, WfAvgTimeMS);
FWfManualHigh := WfHigh;
FWfManualLow := WfLow;
FWfAGCOffset := WfAGCOffset;
FSpecView.WfManualHigh := FWfManualHigh;
FSpecView.WfManualLow := FWfManualLow;
FSpecView.WfAGCOffset := FWfAGCOffset;
FSpecView.ResetWfAvgBuf;
FSpectrumDirty := True;
end;
procedure TMainForm.ApplyGridParams(RefLevel, Range, GridStep: Double);
begin
FSpecRefLevel := RefLevel;
FSpecRange := Range;
if GridStep > 0 then FSpecGridStep := GridStep;
// На передаче FSpecView показывает TX-сетку — RX-правка не должна её перетирать.
if not FTransmitting then ApplySpecViewGridFromState;
end;
procedure TMainForm.ApplySpecViewGridFromState;
begin
// При DUP во время TX данные приходят от RX-анализатора → используем RX-сетку.
if FTransmitting and not FDisplayDuplex then
begin
FSpecView.SpecRefLevel := FTXSpecRefLevel;
FSpecView.SpecRange := FTXSpecRange;
FSpecView.SpecGridStep := FTXSpecGridStep;
end else
begin
FSpecView.SpecRefLevel := FSpecRefLevel;
FSpecView.SpecRange := FSpecRange;
FSpecView.SpecGridStep := FSpecGridStep;
end;
InvalidateGridCache;
FSpectrumDirty := True;
end;
procedure TMainForm.ApplyAudioDevice(DevIndex: Integer; const DevName: string);
begin
if FAudioOut.IsOpen then
FAudioOut.Close;
FAudioOut.DeviceIndex := DevIndex;
FAudioOutDevName := DevName;
try
if not FAudioOut.Open then
StatusBar1.Panels[3].Text := 'Audio out: ' + FAudioOut.LastError
else
StatusBar1.Panels[3].Text := 'Audio out: ' + IfThen(DevName = '', 'Default', DevName);
except
on E: Exception do
StatusBar1.Panels[3].Text := 'Audio out err: ' + E.Message;
end;
end;
procedure TMainForm.ApplyAudioInputDevice(DevIndex: Integer; const DevName: string);
begin
if FAudioIn.IsOpen then
FAudioIn.Close;
FAudioIn.DeviceIndex := DevIndex;
FAudioInDevName := DevName;
if DevIndex >= 0 then
begin
try
if not FAudioIn.Open then
StatusBar1.Panels[3].Text := 'Mic: ' + FAudioIn.LastError
else
StatusBar1.Panels[3].Text := 'Mic: ' + DevName;
except
on E: Exception do
StatusBar1.Panels[3].Text := 'Mic err: ' + E.Message;
end;
// Автопереключение mic-источника: явный выбор PortAudio-устройства
// → берём mic со звуковой карты, иначе HW mic-стрим бы перебивал его.
FTXSettings.MicSource := 1; // SoundCard
end
else
begin
StatusBar1.Panels[3].Text := 'Mic: none';
// Нет выбранного soundcard mic → возвращаемся на HW mic-стрим.
FTXSettings.MicSource := 0; // Radio
end;
if FWDSPReady then
FDSPEngine.SetTXMicSource(TTXMicSource(FTXSettings.MicSource));
if FDevConnected then
begin
FSettings.SaveTX(FDevMAC, FTXSettings);
FSettings.Save;
end;
// Если открыт диалог настроек — синхронизировать UI
if Assigned(FSettingsForm) then
TSettingsForm(FSettingsForm).LoadTXSettings(FTXSettings);
end;
procedure TMainForm.ApplyVisibility(ShowSpectrum, ShowWaterfall: Boolean);
begin
FShowSpectrum := ShowSpectrum;
FShowWaterfall := ShowWaterfall;
ResizeSpectrumPanels;
end;
procedure TMainForm.ApplyFPS(FPS: Integer);
begin
if FPS < 1 then FPS := 1;
if FPS > 100 then FPS := 100;
FDisplayFPS := FPS;
FSpectrumTimer.Interval := 1000 div FPS;
// Thetis по умолчанию обновляет водопад через один display frame.
FWaterfallFrameInterval := 2;
FWaterfallFrameCounter := 0;
FWaterfallDirty := True;
FSpecView.WfFrameInterval := FWaterfallFrameInterval;
if FWDSPReady then
FDSPEngine.SetDisplayFPS(FPS);
end;
procedure TMainForm.ApplyFreqMhzDigits(Digits: Integer);
const
MaxFreqs: array[3..5] of Int64 = (999999999, 9999999999, 99999999999);
begin
Digits := EnsureRange(Digits, 3, 5);
FFreqMhzDigits := Digits;
FreqDispA.MinMhzDigits := Digits;
FreqDispB.MinMhzDigits := Digits;
FreqDispA.MaxFreq := MaxFreqs[Digits];
FreqDispB.MaxFreq := MaxFreqs[Digits];
if Assigned(FWebServer) then
FWebServer.FreqMhzDigits := Digits;
LayoutTopVfoBlock;
end;
procedure TMainForm.BtnSettingsClick(Sender: TObject);
var
SF: TSettingsForm;
begin
if not Assigned(FSettingsForm) then
begin
SF := TSettingsForm.Create(Self);
FSettingsForm := SF;
SF.OnDisplayChange := ApplyDisplayParams;
SF.OnWaterfallChange := ApplyWaterfallParams;
SF.OnGridChange := ApplyGridParams;
SF.OnAudioDevChange := ApplyAudioDevice;
SF.OnAudioInDevChange := ApplyAudioInputDevice;
SF.OnVisibilityChange := ApplyVisibility;
SF.OnFPSChange := ApplyFPS;
SF.OnPAChange := OnPASettingsChange;
SF.OnCATChange := OnCATSettingsChange;
SF.OnThemeChange := SetLightTheme;
SF.OnFreqMhzDigitsChange := ApplyFreqMhzDigits;
SF.OnTXChange := OnTXSettingsChange;
SF.OnAlexChange := OnAlexSettingsChange;
SF.OnXvtrChange := OnXvtrSettingsChange;
SF.OnWfAGCNFChange := ApplyWfAGCNF;
SF.OnADCChange := ApplyADCSettings;
SF.OnWebSettingsChange := ApplyWebSettings;
end;
SF := TSettingsForm(FSettingsForm);
SF.LoadTXSettings(FTXSettings);
SF.LoadAlexSettings(FAlexSettings,
IfThen(FNetwork.Connected, FNetwork.Device.BoardType, FPendingBoardType));
SF.LoadXvtrSettings(FXvtrSettings);
SF.LoadPASettings(FPAMaxPower, FPABandCal);
SF.LoadCATSettings(
FCATLastGlobal.CATSerialEnabled,
FCATLastGlobal.CATSerialPort,
FCATLastGlobal.CATSerialBaud,
FCATLastGlobal.CATSerialDataBits,
FCATLastGlobal.CATSerialStopBits,
FCATLastGlobal.CATSerialParity,
FCATLastGlobal.CATTcpEnabled,
FCATLastGlobal.CATTcpPort);
// Перечисляем PA устройства
SF.RefreshAudioDevices(FAudioOut, FAudioIn);
SF.LoadVisibility(FShowSpectrum, FShowWaterfall);
SF.LoadWfAGCNF(FWfAGCEnabled, FWfNFEnabled);
SF.LoadADCSettings(FDitherEnabled, FRandomEnabled);
SF.LoadWebSettings(FWebEnabled, FWebPort, FWebBindAddr, FWebUser, FWebPass);
SF.LoadFPS(FDisplayFPS);
SF.LoadLightTheme(FLightTheme);
SF.LoadFreqMhzDigits(FFreqMhzDigits);
// Загружаем текущие значения
if FWDSPReady then
SF.LoadValues(
FDSPEngine.FFTSize,
FDSPEngine.WindowType,
FDSPEngine.SpecDetector,
FDSPEngine.SpecAvgMode,
FDSPEngine.SpecAvgTimeMS,
FDSPEngine.WfDetector,
FDSPEngine.WfAvgMode,
FDSPEngine.WfAvgTimeMS,
FWfManualHigh,
FWfManualLow,
FWfAGCOffset,
FSpecRefLevel, FSpecRange, FSpecGridStep,
FAudioOutDevName, FAudioInDevName)
else
SF.LoadValues(
131072, 2, 0, 3, 30.0,
0, 3, 120.0, FWfManualHigh, FWfManualLow, FWfAGCOffset,
FSpecRefLevel, FSpecRange, FSpecGridStep,
FAudioOutDevName, FAudioInDevName);
SF.Show;
end;
procedure TMainForm.AfterShowTick(Sender: TObject);
begin
// Вызывается один раз через 200 мс после старта формы
FAfterShowTimer.Enabled := False;
// Открываем PortAudio — теперь FPC уже перехватывает сигналы
try
if not FAudioOut.Open then
StatusBar1.Panels[3].Text := 'Audio: ' + FAudioOut.LastError
else
StatusBar1.Panels[3].Text := 'Audio OK (PortAudio)';
except
on E: Exception do
StatusBar1.Panels[3].Text := 'Audio: ' + E.Message;
end;
EnsureWDSPWisdom;
end;
// ===========================================================================
// CAT subsystem
// ===========================================================================
procedure TMainForm.InitCATEngine;
var
Ctx: TCATContext;
begin
FillChar(Ctx, SizeOf(Ctx), 0);
Ctx.GetVfoA := CATGetVfoA;
Ctx.GetVfoB := CATGetVfoB;
Ctx.GetMode := CATGetMode;
Ctx.GetActiveVfo := CATGetActiveVfo;
Ctx.GetAGCMode := CATGetAGCMode;
Ctx.GetVolume := CATGetVolume;
Ctx.GetDriveLevel := CATGetDriveLevel;
Ctx.GetFilterIdx := CATGetFilterIdx;
Ctx.GetFilterBW := CATGetFilterBW;
Ctx.GetNRMode := CATGetNRMode;
Ctx.GetNBMode := CATGetNBMode;
Ctx.GetSNBEnabled := CATGetSNB;
Ctx.GetANFEnabled := CATGetANF;
Ctx.GetTransmitting := CATGetTX;
Ctx.GetRunning := CATGetRunning;
Ctx.GetSMeter := CATGetSMeter;
Ctx.GetCurrentBand := CATGetBand;
Ctx.SetVfoA := CATSetVfoA;
Ctx.SetVfoB := CATSetVfoB;
Ctx.SetMode := WebOnMode;
Ctx.SetActiveVfo := WebOnActiveVfo;
Ctx.SetAGCMode := WebOnAGC;
Ctx.SetVolume := WebOnVolume;
Ctx.SetDriveLevel := WebOnDrive;
Ctx.SetFilterIdx := CATSetFilterIdx;
Ctx.SetNRMode := WebOnNR;
Ctx.SetNBMode := WebOnNB;
Ctx.SetSNBEnabled := WebOnSNB;
Ctx.SetANFEnabled := WebOnANF;
Ctx.SetTransmitting := WebOnMOX;
Ctx.DoBandUp := CATDoBandUp;
Ctx.DoBandDown := CATDoBandDown;
Ctx.DoTuneUp := CATDoTuneUp;
Ctx.DoTuneDown := CATDoTuneDown;
Ctx.DoBandByIndex := WebOnBand;
FCATEngine := TCATEngine.Create(Ctx);
FCATSerial := TCATSerialManager.Create(FCATEngine);
FCATTcp := TCATTcpServer.Create(FCATEngine);
end;
procedure TMainForm.CATApplySettings(const G: TGlobalSettings);
var
Cfgs: array[0..3] of TCATSerialConfig;
i: Integer;
begin
for i := 0 to 3 do
begin
Cfgs[i].Enabled := G.CATSerialEnabled[i];
Cfgs[i].PortName := G.CATSerialPort[i];
Cfgs[i].BaudRate := G.CATSerialBaud[i];
Cfgs[i].DataBits := G.CATSerialDataBits[i];
Cfgs[i].StopBits := G.CATSerialStopBits[i];
case G.CATSerialParity[i] of
1: Cfgs[i].Parity := cspOdd;
2: Cfgs[i].Parity := cspEven;
else Cfgs[i].Parity := cspNone;
end;
end;
FCATSerial.ApplyConfig(Cfgs);
FCATTcp.Stop;
if G.CATTcpEnabled then
begin
FCATTcp.Port := G.CATTcpPort;
FCATTcp.Start;
end;
end;
procedure TMainForm.OnCATSettingsChange(
const SerEnabled: array of Boolean;
const SerPort: array of string;
const SerBaud, SerDataBits, SerStopBits, SerParity: array of Integer;
TcpEnabled: Boolean; TcpPort: Integer);
var i: Integer;
begin
for i := 0 to 3 do
begin
if i <= High(SerEnabled) then FCATLastGlobal.CATSerialEnabled[i] := SerEnabled[i];
if i <= High(SerPort) then FCATLastGlobal.CATSerialPort[i] := SerPort[i];
if i <= High(SerBaud) then FCATLastGlobal.CATSerialBaud[i] := SerBaud[i];
if i <= High(SerDataBits) then FCATLastGlobal.CATSerialDataBits[i] := SerDataBits[i];
if i <= High(SerStopBits) then FCATLastGlobal.CATSerialStopBits[i] := SerStopBits[i];
if i <= High(SerParity) then FCATLastGlobal.CATSerialParity[i] := SerParity[i];
end;
FCATLastGlobal.CATTcpEnabled := TcpEnabled;
FCATLastGlobal.CATTcpPort := TcpPort;
CATApplySettings(FCATLastGlobal);
FSettings.SaveCATSettings(FCATLastGlobal);
if FDevConnected then
FSettings.SaveGlobal(FDevMAC, MakeGlobalSettings);
FSettings.Save;
end;
// --- Getters (called from CAT thread — read only, no sync needed) -----------
function TMainForm.CATGetVfoA: Double; begin Result := FVfoA; end;
function TMainForm.CATGetVfoB: Double; begin Result := FVfoB; end;
function TMainForm.CATGetMode: Integer; begin Result := FMode; end;
function TMainForm.CATGetActiveVfo: Integer; begin Result := FActiveVfo; end;
function TMainForm.CATGetAGCMode: Integer; begin Result := FAGCMode; end;
function TMainForm.CATGetVolume: Integer; begin Result := FVolume; end;
function TMainForm.CATGetDriveLevel: Integer;begin Result := TrkDrive.Position; end;
function TMainForm.CATGetFilterIdx: Integer; begin Result := FFilter; end;
function TMainForm.CATGetFilterBW: Integer; begin Result := FFilterBW; end;
function TMainForm.CATGetNRMode: Integer; begin Result := BtnNR.Tag; end;
function TMainForm.CATGetNBMode: Integer; begin Result := BtnNB.Tag; end;
function TMainForm.CATGetSNB: Boolean; begin Result := BtnSNB.Tag <> 0; end;
function TMainForm.CATGetANF: Boolean; begin Result := BtnANF.Tag <> 0; end;
function TMainForm.CATGetTX: Boolean; begin Result := FTransmitting; end;
function TMainForm.CATGetRunning: Boolean; begin Result := FRunning; end;
function TMainForm.CATGetSMeter: Double; begin Result := FLastSMeter; end;
function TMainForm.CATGetBand: Integer; begin Result := FCurrentBand; end;
// --- Setters ----------------------------------------------------------------
procedure TMainForm.CATSetVfoA(V: Double);
begin
FCATSyncFreq := V;
TThread.Synchronize(nil, SyncCATVfoA);
end;
procedure TMainForm.CATSetVfoB(V: Double);
begin
FCATSyncFreq := V;
TThread.Synchronize(nil, SyncCATVfoB);
end;
procedure TMainForm.CATSetFilterIdx(V: Integer);
begin
// WebOnFilter: negative value encodes 0-based index as -(idx+1)
WebOnFilter(-(V + 1));
end;
procedure TMainForm.CATDoBandUp;
begin TThread.Synchronize(nil, SyncCATBandUp); end;
procedure TMainForm.CATDoBandDown;
begin TThread.Synchronize(nil, SyncCATBandDown); end;
procedure TMainForm.CATDoTuneUp;
begin TThread.Synchronize(nil, SyncCATTuneUp); end;
procedure TMainForm.CATDoTuneDown;
begin TThread.Synchronize(nil, SyncCATTuneDown); end;
// --- Sync methods (run in main thread) --------------------------------------
procedure TMainForm.SyncCATVfoA;
var BandIdx: Integer;
begin
FVfoA := FCATSyncFreq;
if FActiveVfo = 0 then
begin
ApplyVfoA(Round(FVfoA));
end
else
begin
// VFO A is not active — just update display and band indicator
FreqDispA.Frequency := Round(FVfoA);
BandIdx := FreqToBandIdx(FVfoA);
if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then
begin
StyleButton(BtnBand[FCurrentBand], False);
FCurrentBand := BandIdx;
StyleButton(BtnBand[FCurrentBand], True);
end;
end;
end;
procedure TMainForm.SyncCATVfoB;
var BandIdx: Integer;
begin
FVfoB := FCATSyncFreq;
FreqDispB.Frequency := Round(FVfoB);
if FActiveVfo = 1 then
begin
FCenterFreq := FVfoB;
if FWDSPReady then FDSPEngine.SetShift(0.0);
if FRunning then
FNetwork.SetRunAndFreq(True, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), FDriveLevel);
BandIdx := FreqToBandIdx(FVfoB);
if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then
begin
StyleButton(BtnBand[FCurrentBand], False);
FCurrentBand := BandIdx;
StyleButton(BtnBand[FCurrentBand], True);
end;
FSpecView.InvalidateRulerCache;
SyncSpecViewFreq;
FSpecView.DrawSpectrum; PbSpectrum.Invalidate;
if PbRuler <> nil then PbRuler.Invalidate;
UpdateVfoDisplay;
end;
end;
procedure TMainForm.SyncCATBandUp;
begin
if FCurrentBand < BAND_COUNT - 1 then
WebOnBand(FCurrentBand + 1);
end;
procedure TMainForm.SyncCATBandDown;
begin
if FCurrentBand > 0 then
WebOnBand(FCurrentBand - 1);
end;
procedure TMainForm.SyncCATTuneUp;
begin
if FActiveVfo = 0 then
ApplyVfoA(Round(FVfoA) + 10)
else
begin
FCATSyncFreq := FVfoB + 10;
SyncCATVfoB;
end;
end;
procedure TMainForm.SyncCATTuneDown;
begin
if FActiveVfo = 0 then
ApplyVfoA(Round(FVfoA) - 10)
else
begin
FCATSyncFreq := FVfoB - 10;
SyncCATVfoB;
end;
end;
end.