Files
ewsdr/SettingsForm.pas
T

3964 lines
144 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 SettingsForm;
{
SettingsForm.pas — диалог настроек EWSDR SDR приложения.
Вкладки: General | Audio | Display | DSP | Transmit | PA Settings | CAT
Display имеет подвкладки: General | RX1 | RX2 | TX
Все изменения применяются МГНОВЕННО через events:
OnDisplayChange — изменены FFT/Window/Detector/Avg параметры
OnWaterfallChange — изменены параметры водопада
OnGridChange — изменена сетка спектра
OnAudioChange — изменено аудио устройство/буфер
OnSampleRateChange — изменён audio output sample rate (только PortAudio, DDC не трогается)
Для применения настроек: создайте форму, зарегистрируйте callbacks,
потом вызовите Show или ShowModal.
}
{$IFDEF FPC}
{$MODE Delphi}
{$ENDIF}
interface
uses
Classes, SysUtils, Math,
Forms, Controls, Graphics, Dialogs,
StdCtrls, ExtCtrls,
FlatButton, FlatCheckBox, FlatComboBox, FlatEdit, FlatSpinEdit, FlatFloatSpinEdit,
FlatRadioButton,
EqualizerControl, AudioOutput, AudioInput, AppTheme, Settings, BoardUtils;
type
// PA: MaxPower в Вт, BandCal[0..10] — калибровка 38.8..100.0 на диапазон
TOnPASettingsChange = procedure(MaxPower: Double;
const BandCal: array of Double) of object;
// Pluto TX: аттенюация выходного тракта на drive=100% (потолок), дБ<0
TOnPlutoTxChange = procedure(MaxAttDb: Double) of object;
// Антенные разъёмы AD936x (Pluto/LibreSDR): таблица «диапазон → RX/TX разъём»
TOnAnt936xChange = procedure(const A: TAD936xAnt) of object;
// VHF (XVTR) PA calibration: 8 slots, 38.8..100.0
TOnVHFCalChange = procedure(const VHFCal: array of Double) of object;
// TX-настройки целиком (любая правка → один callback с актуальной структурой)
TOnTXSettingsChange = procedure(const T: TTXSettings) of object;
TOnFreqMhzDigitsChange = procedure(Digits: Integer) of object;
TOnDisplayParamChange = procedure(FFTSize, WindowType, SpecDetector,
SpecAvgMode: Integer; SpecAvgTimeMS: Double) of object;
TOnWaterfallParamChange = procedure(WfDetector, WfAvgMode: Integer;
WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double) of object;
TOnWfRenderChange = procedure(Palette: Integer; Gamma: Double;
ResLimit, SpeedDiv: Integer) of object;
TOnSpecMSAAChange = procedure(Samples: Integer) of object;
TOnGridParamChange = procedure(RefLevel, Range, GridStep: Double) of object;
TOnAudioDevChange = procedure(DevIndex: Integer; const DevName: string) of object;
TOnAudioInDevChange = procedure(DevIndex: Integer; const DevName: string) of object;
TOnAudioBufferChange = procedure(BufferSize: Integer) of object;
TOnSendAudioToRadioChange = procedure(Enabled: Boolean) of object;
TOnVisibilityChange = procedure(ShowSpectrum, ShowWaterfall, ShowWideband,
WidebandFill, SpectrumFill: Boolean) of object;
TOnFPSChange = procedure(FPS: Integer) of object;
TOnThemeChange = procedure(LightTheme: Boolean) of object;
TOnWfAGCNFChange = procedure(WfAGC, WfNF: Boolean) of object;
TOnADCChange = procedure(Dither, Random: Boolean) of object;
TOnWebSettingsChange = procedure(Enabled: Boolean; Port: Integer;
const BindAddr, User, Pass: string) of object;
TOnCATChange = procedure(
const SerEnabled: array of Boolean;
const SerPort: array of string;
const SerBaud: array of Integer;
const SerDataBits: array of Integer;
const SerStopBits: array of Integer;
const SerParity: array of Integer;
const SerAndromeda: array of Boolean;
TcpEnabled: Boolean; TcpPort: Integer) of object;
TOnAlexChange = procedure(const A: TAlexSettings) of object;
TOnXvtrChange = procedure(const X: TXvtrSettings) of object;
{ TSettingsForm }
TSettingsForm = class(TForm)
private
// ---- Layout ----
FNavPanel: TPanel;
FContentPanel: TPanel;
FPageAudio: TScrollBox;
FPageDisplay: TScrollBox;
FPageSpectrum: TScrollBox;
FPageWaterfall: TScrollBox;
FPagePA: TScrollBox;
FPageAdvanced: TScrollBox;
FPageCAT: TScrollBox;
FPageTransmit: TScrollBox;
FNavAudio: TFlatButton;
FNavDisplay: TFlatButton;
FNavSpectrum: TFlatButton;
FNavWaterfall: TFlatButton;
FNavPA: TFlatButton;
FNavAdvanced: TFlatButton;
FNavCAT: TFlatButton;
FNavTransmit: TFlatButton;
// ---- Audio tab controls ----
FCmbRXDev: TFlatComboBox;
FRXDevIndices: array[0..63] of Integer;
FRXDevCount: Integer;
FCmbTXDev: TFlatComboBox;
FTXDevIndices: array[0..63] of Integer;
FTXDevCount: Integer;
FCmbAudioBuffer: TFlatComboBox;
FChkSendAudioToRadio: TFlatCheckBox;
// ---- RX1 sub-tab controls ----
FCmbFFTSize: TFlatComboBox;
FCmbWindow: TFlatComboBox;
FCmbSpecDet: TFlatComboBox;
FCmbSpecAvg: TFlatComboBox;
FSpecAvgTime: TFlatFloatSpinEdit;
FCmbWfDet: TFlatComboBox;
FCmbWfAvg: TFlatComboBox;
FWfAvgTime: TFlatFloatSpinEdit;
FWfHigh: TFlatFloatSpinEdit;
FWfLow: TFlatFloatSpinEdit;
FWfAGCOffset: TFlatFloatSpinEdit;
FChkWfAGC: TFlatCheckBox;
FChkWfNF: TFlatCheckBox;
FCmbWfPalette: TFlatComboBox;
FWfGamma: TFlatFloatSpinEdit;
FCmbWfRes: TFlatComboBox;
FCmbWfSpeed: TFlatComboBox;
FCmbSpecMSAA: TFlatComboBox;
FEdRefLevel: TFlatEdit;
FEdRange: TFlatEdit;
FCmbGridStep: TFlatComboBox;
// ---- General display tab controls ----
FChkShowSpectrum: TFlatCheckBox;
FChkShowWaterfall: TFlatCheckBox;
FChkShowWideband: TFlatCheckBox;
FChkWidebandFill: TFlatCheckBox;
FChkSpectrumFill: TFlatCheckBox;
FCmbFPS: TFlatComboBox;
FChkLightTheme: TFlatCheckBox;
FCmbFreqMhzDigits: TFlatComboBox;
// ---- PA Settings tab controls ----
FEdMaxPower: TFlatSpinEdit;
FEdPlutoTxMaxAtt: TFlatFloatSpinEdit; // Pluto TX att на drive=100%
FEdBandCal: array[0..10] of TFlatFloatSpinEdit;
FEdVHFCal: array[0..CFG_XVTR_COUNT-1] of TFlatFloatSpinEdit;
FVHFCalLabels: array[0..CFG_XVTR_COUNT-1] of TLabel;
// ---- Transmit tab controls ----
// Текущее состояние, отдаётся в OnTXChange целиком при любой правке.
FTX: TTXSettings;
FCmbTXMicJack: TFlatComboBox; // "Mic In" / "Line In"
FChkTXMicBoost: TFlatCheckBox; // +20 dB (только Mic In)
FLblLineInGain: TLabel; // метка "Line In gain" (только Line In)
FEdLineInGain: TFlatFloatSpinEdit; // -34.5..+12 dB, шаг 1.5 (только Line In)
FChkTXMicBias: TFlatCheckBox;
FChkTXMicPTT: TFlatCheckBox;
FCmbTXMicTR: TFlatComboBox; // "Tip" / "Ring" (только Orion)
FLblTipRing: TLabel; // метка (только Orion)
FEdTXMicGain: TFlatFloatSpinEdit;
FEdTXAttOnTX: TFlatSpinEdit;
// Tune (TUN button)
FEdTUNLevel: TFlatSpinEdit;
FEdTUNFreq: TFlatSpinEdit;
// Filter
FEdTXFiltLow: TFlatSpinEdit;
FEdTXFiltHigh: TFlatSpinEdit;
FCmbTXFiltNC: TFlatComboBox;
FCmbTXFiltMP: TFlatComboBox;
FCmbTXFiltWindow: TFlatComboBox;
// Compressor
FChkTXCompOn: TFlatCheckBox;
FEdTXCompGain: TFlatFloatSpinEdit;
// Leveler
FChkTXLevOn: TFlatCheckBox;
FEdTXLevTop: TFlatFloatSpinEdit;
FEdTXLevDecay: TFlatSpinEdit;
// ALC
FChkTXALCOn: TFlatCheckBox;
FEdTXALCMaxGain: TFlatFloatSpinEdit;
FEdTXALCDecay: TFlatSpinEdit;
// Phase Rotator
FChkTXPHROn: TFlatCheckBox;
FEdTXPHRStages: TFlatSpinEdit;
FEdTXPHRFreq: TFlatFloatSpinEdit;
// EQ
FTXEQControl: TEqualizerControl;
// AM
FEdTXAMCarrier: TFlatFloatSpinEdit;
// FM
FEdTXFMDev: TFlatFloatSpinEdit;
FEdTXFMLow: TFlatSpinEdit;
FEdTXFMHigh: TFlatSpinEdit;
FCmbTXFMEmph: TFlatComboBox;
// CTCSS
FChkTXCTCSSOn: TFlatCheckBox;
FEdTXCTCSSFreq: TFlatFloatSpinEdit;
// TX Display (отдельный analyzer)
FCmbTXFFT: TFlatComboBox;
FCmbTXWindow: TFlatComboBox;
FCmbTXSpecDet: TFlatComboBox;
FCmbTXSpecAvg: TFlatComboBox;
FEdTXSpecAvgTime: TFlatFloatSpinEdit;
FCmbTXWfDet: TFlatComboBox;
FCmbTXWfAvg: TFlatComboBox;
FEdTXWfAvgTime: TFlatFloatSpinEdit;
// TX Grid (отдельные от RX grid в Display)
FEdTXRefLevel: TFlatEdit;
FEdTXRange: TFlatEdit;
FCmbTXGridStep: TFlatComboBox;
// ---- Alex / Antenna tab controls ----
FPageAlex: TScrollBox;
FNavAlex: TFlatButton;
// Per-band antenna radio buttons (bands 0..10 = 160m..6m)
FAlexRxAnt: array[0..10, 1..3] of TFlatRadioButton;
FAlexTxAnt: array[0..10, 1..3] of TFlatRadioButton;
// Per-band secondary RX (взаимно исключающие checkboxes)
FAlexRxByps: array[0..10] of TFlatCheckBox; // BYPS: Ext1+Bypass
FAlexRxExt1: array[0..10] of TFlatCheckBox; // EXT1: Ext2+Bypass
FAlexRxXvtr: array[0..10] of TFlatCheckBox; // XVTR: XvtrDDC+Bypass
// Per-band Do Not TX
FAlexDoNotTx: array[0..10] of TFlatCheckBox;
// Global TX routing flags
FAlexRxBypassOnTx: TFlatCheckBox;
FAlexExt1OnTx: TFlatCheckBox;
FAlexGndBpf2OnTx: TFlatCheckBox;
FAlexEnableXvtrHf: TFlatCheckBox;
// Группы вкладки Antenna: Alex-таблица (openHPSDR) vs выбор приёмного входа
// AD936x (Pluto/LibreSDR) — показывается одна из двух (SetAntennaBackend).
FAlexSubtitle: TLabel;
FAlexGrpAnt: TPanel;
FAlexGrpTX: TPanel;
FPlutoAntGrp: TPanel;
// На диапазон (VU-план, 12 слотов) и на слот трансвертера — по паре радио-
// кнопок RX (RX1/RX2) и TX (TX1/TX2).
FAntRx: array[0..CFG_BAND_COUNT-1, 0..1] of TFlatRadioButton;
FAntTx: array[0..CFG_BAND_COUNT-1, 0..1] of TFlatRadioButton;
FAntXvtrRx: array[0..CFG_XVTR_COUNT-1, 0..1] of TFlatRadioButton;
FAntXvtrTx: array[0..CFG_XVTR_COUNT-1, 0..1] of TFlatRadioButton;
FAntXvtrRow: array[0..CFG_XVTR_COUNT-1] of TPanel; // строка слота (скрыта, если выкл.)
FAntXvtrLbl: array[0..CFG_XVTR_COUNT-1] of TLabel;
FAntXvtrHdr: TLabel; // «Transverters» — виден при хотя бы одном включённом
FAnt936x: TAD936xAnt;
// Current Alex config state
FAlexConfig: TAlexSettings;
FAlexBoardType: Integer;
// Callback
FOnAlexChange: TOnAlexChange;
// ---- XVTR (Transverter) tab controls ----
FPageXvtr: TScrollBox;
FNavXvtr: TFlatButton;
FXvtrEn: array[0..CFG_XVTR_COUNT-1] of TFlatCheckBox;
FXvtrName: array[0..CFG_XVTR_COUNT-1] of TFlatEdit;
FXvtrBegin: array[0..CFG_XVTR_COUNT-1] of TFlatFloatSpinEdit; // MHz
FXvtrEnd: array[0..CFG_XVTR_COUNT-1] of TFlatFloatSpinEdit; // MHz
FXvtrLOOff: array[0..CFG_XVTR_COUNT-1] of TFlatFloatSpinEdit; // MHz
FXvtrLOErr: array[0..CFG_XVTR_COUNT-1] of TFlatFloatSpinEdit; // Hz (целые)
FXvtrRXGain: array[0..CFG_XVTR_COUNT-1] of TFlatFloatSpinEdit; // dB
FXvtrRXOnly: array[0..CFG_XVTR_COUNT-1] of TFlatCheckBox;
FXvtrPower: array[0..CFG_XVTR_COUNT-1] of TFlatSpinEdit; // 0..100
FXvtrDisPA: array[0..CFG_XVTR_COUNT-1] of TFlatCheckBox;
FXvtrRXAnt: array[0..CFG_XVTR_COUNT-1] of TFlatComboBox; // 0=def..3=ANT3
FChkXvtrTunPwr: TFlatCheckBox;
FXvtrConfig: TXvtrSettings;
FOnXvtrChange: TOnXvtrChange;
// ---- QO-100 tab (редактирует зарезервированный слот QO100_SLOT) ----
FPageQO: TScrollBox;
FNavQO: TFlatButton;
FQoEn: TFlatCheckBox;
FQoLnbLO: TFlatFloatSpinEdit; // МГц — LNB LO (= LOOffset, RX)
FQoLoErr: TFlatFloatSpinEdit; // Гц — LO Error (= LOError, живая калибровка LNB / beacon-lock)
FQoTxLO: TFlatFloatSpinEdit; // МГц — транспондер-offset (= TxLOOffset)
FQoBegin: TFlatFloatSpinEdit; // МГц — downlink begin
FQoEnd: TFlatFloatSpinEdit; // МГц — downlink end
FQoRXGain: TFlatFloatSpinEdit; // dB
FQoPower: TFlatSpinEdit; // 0..100 TX drive ceiling
FQoInfo: TLabel; // расчёт RX IF / TX freq
// ---- CAT tab controls ----
FCATSerialEn: array[0..3] of TFlatCheckBox;
FCATSerialPort: array[0..3] of TFlatEdit;
FCATSerialBaud: array[0..3] of TFlatComboBox;
FCATSerialDataBits: array[0..3] of TFlatComboBox;
FCATSerialStopBits: array[0..3] of TFlatComboBox;
FCATSerialParity: array[0..3] of TFlatComboBox;
FCATSerialAndromeda:array[0..3] of TFlatCheckBox;
FCATTcpEn: TFlatCheckBox;
FCATTcpPort: TFlatSpinEdit;
// ---- Advanced tab controls ----
FChkDither: TFlatCheckBox;
FChkRandom: TFlatCheckBox;
FChkWebEnabled: TFlatCheckBox;
FEdWebPort: TFlatSpinEdit;
FEdWebBind: TFlatEdit;
FEdWebUser: TFlatEdit;
FEdWebPass: TFlatEdit;
// ---- Close button ----
FBtnClose: TFlatButton;
// ---- Callbacks ----
FOnCATChange: TOnCATChange;
FOnPAChange: TOnPASettingsChange;
FOnPlutoTxChange: TOnPlutoTxChange;
FOnAnt936xChange: TOnAnt936xChange;
FOnVHFCalChange: TOnVHFCalChange;
FOnADCChange: TOnADCChange;
FOnWebSettingsChange: TOnWebSettingsChange;
FOnDisplayChange: TOnDisplayParamChange;
FOnWaterfallChange: TOnWaterfallParamChange;
FOnWfRenderChange: TOnWfRenderChange;
FOnSpecMSAAChange: TOnSpecMSAAChange;
FOnGridChange: TOnGridParamChange;
FOnAudioDevChange: TOnAudioDevChange;
FOnAudioInDevChange: TOnAudioInDevChange;
FOnAudioBufferChange: TOnAudioBufferChange;
FOnSendAudioToRadioChange: TOnSendAudioToRadioChange;
FOnVisibilityChange: TOnVisibilityChange;
FOnFPSChange: TOnFPSChange;
FOnThemeChange: TOnThemeChange;
FOnWfAGCNFChange: TOnWfAGCNFChange;
FOnFreqMhzDigitsChange: TOnFreqMhzDigitsChange;
FOnTXChange: TOnTXSettingsChange;
FLoading: Boolean;
procedure BuildUI;
procedure BuildAudioTab;
procedure BuildDisplayTab;
procedure BuildRX1Tab;
procedure BuildPATab;
procedure BuildCATTab;
procedure BuildTransmitTab;
procedure BuildAlexTab;
procedure BuildAdvancedTab;
procedure OnADCChkChange(Sender: TObject);
procedure OnWebAnyChange(Sender: TObject);
procedure CollectAlexFromUI;
procedure FireAlexChange;
procedure OnAlexAnyChange(Sender: TObject);
procedure OnAlexRxOnlyChange(Sender: TObject);
procedure BuildXvtrTab;
procedure BuildQOTab;
procedure CollectQOFromUI; // QO-виджеты → FXvtrConfig[QO100_SLOT]
procedure LoadQOFromConfig; // FXvtrConfig[QO100_SLOT] → QO-виджеты
procedure UpdateQOInfo; // пересчёт RX IF / TX freq в инфо-метке
procedure OnQOAnyChange(Sender: TObject);
procedure CollectXvtrFromUI;
procedure FireXvtrChange;
procedure OnXvtrAnyChange(Sender: TObject);
procedure UpdateXvtrRowEnabled(Idx: Integer);
procedure CollectTXFromUI;
procedure FireTXChange;
procedure OnTXAnyChange(Sender: TObject);
procedure OnMicJackChange(Sender: TObject);
procedure UpdateMicJackVisibility;
procedure ApplyTheme(const T: TAppTheme);
procedure NavClick(Sender: TObject);
procedure SelectPage(APage: TWinControl; ANav: TFlatButton);
function MakeLbl(AParent: TWinControl; const Cap: string;
ALeft, ATop, AW: Integer): TLabel;
function MakeCombo(AParent: TWinControl; ALeft, ATop, AW: Integer;
OnChange: TNotifyEvent): TFlatComboBox;
function MakeGroupPanel(AParent: TWinControl; const Cap: string;
ALeft, ATop, AW, AH: Integer): TPanel;
function MakeScrollPage: TScrollBox;
function MakeNavButton(const Cap: string; ATop: Integer): TFlatButton;
procedure OnFFTSizeChange(Sender: TObject);
procedure OnWindowChange(Sender: TObject);
procedure OnSpecDetChange(Sender: TObject);
procedure OnSpecAvgModeChange(Sender: TObject);
procedure OnSpecAvgTimeChange(Sender: TObject);
procedure OnWfDetChange(Sender: TObject);
procedure OnWfAvgModeChange(Sender: TObject);
procedure OnWfAvgTimeChange(Sender: TObject);
procedure OnWfThresholdChange(Sender: TObject);
procedure OnWfAGCNFChkChange(Sender: TObject);
procedure OnRefLevelChange(Sender: TObject);
procedure OnRangeChange(Sender: TObject);
procedure OnGridStepChange(Sender: TObject);
procedure OnRXDevChange(Sender: TObject);
procedure OnTXDevChange(Sender: TObject);
procedure OnAudioBufferCmbChange(Sender: TObject);
procedure OnSendAudioToRadioChkChange(Sender: TObject);
procedure OnVisibilityChkChange(Sender: TObject);
procedure OnFPSCmbChange(Sender: TObject);
procedure OnLightThemeChkChange(Sender: TObject);
procedure OnFreqMhzDigitsCmbChange(Sender: TObject);
procedure OnMaxPowerChange(Sender: TObject);
procedure OnPlutoTxMaxAttChange(Sender: TObject);
procedure Ant936xUIChange(Sender: TObject);
procedure OnBandCalChange(Sender: TObject);
procedure OnVHFCalEdit(Sender: TObject);
procedure OnCATSerialChange(Sender: TObject);
procedure OnCATTcpChange(Sender: TObject);
procedure FireCATChange;
procedure BtnCloseClick(Sender: TObject);
procedure FireDisplayChange;
procedure FireWaterfallChange;
procedure FireWfRenderChange;
procedure OnWfRenderCtlChange(Sender: TObject);
procedure OnSpecMSAACtlChange(Sender: TObject);
procedure FireGridChange;
function GetSpecAvgTimeMS: Double;
function GetWfAvgTimeMS: Double;
function GetWfHigh: Double;
function GetWfLow: Double;
function GetWfAGCOffset: Double;
function GetRefLevel: Double;
function GetRange: Double;
function GetGridStep: Double;
function GetFFTSize: Integer;
public
constructor Create(AOwner: TComponent); override;
procedure LoadValues(
FFTSize, WinType, SpecDet, SpecAvgMode: Integer;
SpecAvgTimeMS: Double;
WfDet, WfAvgMode: Integer;
WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double;
RefLevel, Range, GridStep: Double;
const AudioOutDevName: string;
const AudioInDevName: string);
procedure RefreshAudioDevices(AudioOut: TAudioOutput; AudioIn: TAudioInput);
procedure LoadAudioBufferSize(BufferSize: Integer);
procedure LoadSendAudioToRadio(Enabled: Boolean);
procedure LoadVisibility(ShowSpectrum, ShowWaterfall, ShowWideband,
WidebandFill, SpectrumFill: Boolean);
procedure LoadFPS(FPS: Integer);
procedure LoadLightTheme(ALight: Boolean);
procedure LoadFreqMhzDigits(Digits: Integer);
procedure LoadPASettings(MaxPower: Double; const BandCal: array of Double);
procedure LoadPlutoTxMaxAtt(MaxAttDb: Double);
// Вкладка Antenna: Alex-таблица (openHPSDR) либо таблица разъёмов AD936x.
procedure SetAntennaBackend(IsAD936x, HasAntSwitch: Boolean);
// Таблица разъёмов + имена/видимость слотов трансвертеров.
procedure LoadAnt936x(const A: TAD936xAnt; const X: TXvtrSettings);
procedure LoadVHFCalSettings(const VHFCal: array of Double);
procedure LoadCATSettings(
const SerEnabled: array of Boolean;
const SerPort: array of string;
const SerBaud, SerDataBits, SerStopBits, SerParity: array of Integer;
const SerAndromeda: array of Boolean;
TcpEnabled: Boolean; TcpPort: Integer);
procedure LoadTXSettings(const T: TTXSettings; BoardType: Integer = 0);
procedure LoadAlexSettings(const A: TAlexSettings; BoardType: Integer);
procedure LoadXvtrSettings(const X: TXvtrSettings);
procedure LoadWfAGCNF(AGC, NF: Boolean);
procedure LoadWfRender(Palette: Integer; Gamma: Double;
ResLimit, SpeedDiv: Integer);
procedure LoadSpecMSAA(Samples: Integer);
procedure LoadADCSettings(Dither, Random: Boolean);
procedure LoadWebSettings(Enabled: Boolean; Port: Integer;
const BindAddr, User, Pass: string);
property OnCATChange: TOnCATChange read FOnCATChange write FOnCATChange;
property OnAlexChange: TOnAlexChange read FOnAlexChange write FOnAlexChange;
property OnXvtrChange: TOnXvtrChange read FOnXvtrChange write FOnXvtrChange;
property OnPAChange: TOnPASettingsChange read FOnPAChange write FOnPAChange;
property OnPlutoTxChange: TOnPlutoTxChange read FOnPlutoTxChange write FOnPlutoTxChange;
property OnAnt936xChange: TOnAnt936xChange read FOnAnt936xChange write FOnAnt936xChange;
property OnVHFCalChange: TOnVHFCalChange read FOnVHFCalChange write FOnVHFCalChange;
property OnDisplayChange: TOnDisplayParamChange read FOnDisplayChange write FOnDisplayChange;
property OnWaterfallChange: TOnWaterfallParamChange read FOnWaterfallChange write FOnWaterfallChange;
property OnWfRenderChange: TOnWfRenderChange read FOnWfRenderChange write FOnWfRenderChange;
property OnSpecMSAAChange: TOnSpecMSAAChange read FOnSpecMSAAChange write FOnSpecMSAAChange;
property OnGridChange: TOnGridParamChange read FOnGridChange write FOnGridChange;
property OnAudioDevChange: TOnAudioDevChange read FOnAudioDevChange write FOnAudioDevChange;
property OnAudioInDevChange: TOnAudioInDevChange read FOnAudioInDevChange write FOnAudioInDevChange;
property OnAudioBufferChange: TOnAudioBufferChange read FOnAudioBufferChange write FOnAudioBufferChange;
property OnSendAudioToRadioChange: TOnSendAudioToRadioChange read FOnSendAudioToRadioChange write FOnSendAudioToRadioChange;
property OnVisibilityChange: TOnVisibilityChange read FOnVisibilityChange write FOnVisibilityChange;
property OnFPSChange: TOnFPSChange read FOnFPSChange write FOnFPSChange;
property OnThemeChange: TOnThemeChange read FOnThemeChange write FOnThemeChange;
property OnFreqMhzDigitsChange: TOnFreqMhzDigitsChange read FOnFreqMhzDigitsChange write FOnFreqMhzDigitsChange;
property OnTXChange: TOnTXSettingsChange read FOnTXChange write FOnTXChange;
property OnWfAGCNFChange: TOnWfAGCNFChange read FOnWfAGCNFChange write FOnWfAGCNFChange;
property OnADCChange: TOnADCChange read FOnADCChange write FOnADCChange;
property OnWebSettingsChange: TOnWebSettingsChange read FOnWebSettingsChange write FOnWebSettingsChange;
end;
implementation
const
CLR_BG = TColor($00121212);
CLR_PANEL = TColor($001A1A1A);
CLR_TEXT = TColor($00E0E0E0);
CLR_TEXTDIM = TColor($00888888);
CLR_BORDER = TColor($00303030);
CLR_ACCENT = TColor($0040FF80);
CLR_INPUT = TColor($00FFFFFF);
CLR_INPUT_TEXT = TColor($00202020);
{$IFDEF MSWINDOWS}
UI_FONT = 'Segoe UI';
{$ELSE}
UI_FONT = 'Sans';
{$ENDIF}
BTN_H = 24;
GRP_TITLE_H = 22; // высота заголовка в group-панелях (title label + divider)
CAT_BAUD_VALS: array[0..7] of Integer = (1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200);
CAT_BAUD_NAMES: array[0..7] of string = ('1200','2400','4800','9600','19200','38400','57600','115200');
FFT_SIZES: array[0..6] of Integer = (4096, 8192, 16384, 32768, 65536, 131072, 262144);
FFT_NAMES: array[0..6] of string = ('4096','8192','16384','32768','65536','131072','262144');
WINDOW_NAMES: array[0..6] of string = (
'Rectangular', '4-term Blackman-Harris', 'Hann',
'Flat-top', 'Hamming', 'Kaiser', '7-term Blackman-Harris');
DETECTOR_NAMES: array[0..4] of string = (
'Peak', 'Rosenfell', 'Average', 'Sample', 'RMS');
AVGMODE_NAMES: array[0..3] of string = (
'None', 'Recursive', 'Time Window', 'Log Recursive');
AVGMODE_VALUES: array[0..3] of Integer = (0, 1, 2, 3);
WF_AVGMODE_NAMES: array[0..3] of string = (
'None', 'Recursive', 'Time Window', 'Log Recursive');
WF_AVGMODE_VALUES: array[0..3] of Integer = (0, 1, 2, 3);
GRID_STEPS: array[0..4] of Double = (5.0, 10.0, 20.0, 30.0, 40.0);
GRID_STEP_NAMES: array[0..4] of string = ('5 dB','10 dB','20 dB','30 dB','40 dB');
WF_PALETTE_NAMES: array[0..2] of string = ('Classic', 'Inferno', 'Turbo');
DISPLAY_RES_VALUES: array[0..2] of Integer = (1024, 2048, 4096);
DISPLAY_RES_NAMES: array[0..2] of string = ('1024 px', '2048 px', '4096 px');
WF_SPEED_VALUES: array[0..3] of Integer = (1, 2, 4, 8);
WF_SPEED_NAMES: array[0..3] of string = ('Full rate', '1/2', '1/4', '1/8');
MSAA_VALUES: array[0..3] of Integer = (1, 2, 4, 8);
MSAA_NAMES: array[0..3] of string = ('Off', '2x', '4x', '8x');
FPS_VALUES: array[0..12] of Integer = (5, 10, 15, 20, 25, 30, 40, 50, 60, 75, 80, 90, 100);
FPS_NAMES: array[0..12] of string = ('5 fps','10 fps','15 fps','20 fps','25 fps','30 fps',
'40 fps','50 fps','60 fps','75 fps','80 fps','90 fps','100 fps');
PA_BAND_NAMES: array[0..10] of string =
('160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m');
{ TSettingsForm }
constructor TSettingsForm.Create(AOwner: TComponent);
begin
inherited CreateNew(AOwner);
Caption := 'Settings';
Width := 1180;
Height := 720;
Position := poScreenCenter;
BorderStyle := bsSizeable;
Color := CLR_BG;
Font.Name := UI_FONT;
Font.Size := 9;
Font.Color := CLR_TEXT;
FLoading := False;
FRXDevCount := 0;
BuildUI;
ApplyTheme(DarkTheme);
end;
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function TSettingsForm.MakeLbl(AParent: TWinControl; const Cap: string;
ALeft, ATop, AW: Integer): TLabel;
begin
Result := TLabel.Create(Self);
Result.Parent := AParent;
Result.Caption := Cap;
Result.Left := ALeft;
Result.Top := ATop;
if AW > 0 then Result.Width := AW;
Result.Font.Color := CLR_TEXTDIM;
Result.Font.Name := UI_FONT;
Result.Font.Size := 9;
end;
function TSettingsForm.MakeCombo(AParent: TWinControl; ALeft, ATop, AW: Integer;
OnChange: TNotifyEvent): TFlatComboBox;
begin
Result := TFlatComboBox.Create(Self);
Result.Parent := AParent;
Result.Left := ALeft;
Result.Top := ATop;
Result.Width := AW;
Result.Height := BTN_H;
Result.Color := CLR_INPUT;
Result.Font.Color := CLR_INPUT_TEXT;
Result.Font.Name := UI_FONT;
Result.Font.Size := 9;
Result.OnChange := OnChange;
end;
// Создаёт панель-группу с заголовком и разделителем (вместо TGroupBox).
// На Windows TGroupBox рисует фрейм системными цветами, TPanel — нет.
function TSettingsForm.MakeGroupPanel(AParent: TWinControl; const Cap: string;
ALeft, ATop, AW, AH: Integer): TPanel;
var
TitleLbl: TLabel;
Divider: TPanel;
begin
Result := TPanel.Create(Self);
Result.Parent := AParent;
Result.SetBounds(ALeft, ATop, AW, AH);
Result.BevelOuter := bvNone;
Result.Color := CLR_PANEL;
TitleLbl := TLabel.Create(Self);
TitleLbl.Parent := Result;
TitleLbl.Caption := Cap;
TitleLbl.SetBounds(8, 5, AW - 16, 14);
TitleLbl.Font.Color := CLR_ACCENT;
TitleLbl.Font.Name := UI_FONT;
TitleLbl.Font.Size := 9;
TitleLbl.Font.Style := [fsBold];
Divider := TPanel.Create(Self);
Divider.Parent := Result;
Divider.SetBounds(0, GRP_TITLE_H, AW + 200, 1);
Divider.BevelOuter := bvNone;
Divider.Color := CLR_BORDER;
end;
function TSettingsForm.MakeScrollPage: TScrollBox;
begin
Result := TScrollBox.Create(Self);
Result.Parent := FContentPanel;
Result.Align := alClient;
Result.BorderStyle := bsNone;
Result.Color := CLR_BG;
Result.AutoScroll := True;
Result.Visible := False;
end;
function TSettingsForm.MakeNavButton(const Cap: string; ATop: Integer): TFlatButton;
begin
Result := TFlatButton.Create(Self);
Result.Parent := FNavPanel;
Result.Caption := Cap;
Result.SetBounds(10, ATop, 166, 32);
Result.Anchors := [akLeft, akTop, akRight];
Result.OnClick := NavClick;
Result.ClrNorm := CLR_PANEL;
Result.ClrBorder := CLR_PANEL;
Result.ClrHot := TColor($00242424);
Result.ClrActive := TColor($00204030);
Result.ClrText := CLR_TEXT;
Result.ClrTextAct := CLR_ACCENT;
Result.Font.Name := UI_FONT;
Result.Font.Size := 9;
Result.Font.Style := [];
end;
// ---------------------------------------------------------------------------
// BuildUI
// ---------------------------------------------------------------------------
procedure TSettingsForm.BuildUI;
begin
FNavPanel := TPanel.Create(Self);
FNavPanel.Parent := Self;
FNavPanel.Align := alLeft;
FNavPanel.Width := 188;
FNavPanel.BevelOuter := bvNone;
FNavPanel.Color := TColor($00151515);
with TLabel.Create(Self) do
begin
Parent := FNavPanel;
Caption := 'Settings';
SetBounds(14, 14, 160, 24);
Font.Name := UI_FONT;
Font.Size := 14;
Font.Style := [fsBold];
Font.Color := CLR_TEXT;
end;
with TLabel.Create(Self) do
begin
Parent := FNavPanel;
Caption := 'EWSDR';
SetBounds(14, 39, 160, 18);
Font.Name := UI_FONT;
Font.Size := 9;
Font.Color := CLR_TEXTDIM;
end;
FNavAudio := MakeNavButton('Audio', 74);
FNavDisplay := MakeNavButton('Display', 110);
FNavSpectrum := MakeNavButton('Spectrum', 146);
FNavWaterfall := MakeNavButton('Waterfall', 182);
FNavTransmit := MakeNavButton('Transmit', 218);
FNavPA := MakeNavButton('Power Amplifier', 254);
FNavAdvanced := MakeNavButton('Advanced', 290);
FNavCAT := MakeNavButton('CAT', 326);
FNavAlex := MakeNavButton('Antenna / Alex', 362);
FNavXvtr := MakeNavButton('Transverter', 398);
FNavQO := MakeNavButton('QO-100', 434);
FContentPanel := TPanel.Create(Self);
FContentPanel.Parent := Self;
FContentPanel.Align := alClient;
FContentPanel.BevelOuter := bvNone;
FContentPanel.Color := CLR_BG;
FPageAudio := MakeScrollPage;
FPageDisplay := MakeScrollPage;
FPageSpectrum := MakeScrollPage;
FPageWaterfall := MakeScrollPage;
FPageTransmit := MakeScrollPage;
FPagePA := MakeScrollPage;
FPageAdvanced := MakeScrollPage;
FPageCAT := MakeScrollPage;
FPageAlex := MakeScrollPage;
FPageXvtr := MakeScrollPage;
FPageQO := MakeScrollPage;
FAlexBoardType := 0;
TSettingsManager.DefaultAlex(FAlexConfig);
TSettingsManager.DefaultXvtr(FXvtrConfig);
BuildAudioTab;
BuildDisplayTab;
BuildRX1Tab;
BuildTransmitTab;
BuildPATab;
BuildAdvancedTab;
BuildCATTab;
BuildAlexTab;
BuildXvtrTab;
BuildQOTab;
FBtnClose := TFlatButton.Create(Self);
FBtnClose.Parent := Self;
FBtnClose.Caption := 'Close';
FBtnClose.SetBounds(Width - 106, Height - 40, 92, 28);
FBtnClose.Anchors := [akRight, akBottom];
FBtnClose.OnClick := BtnCloseClick;
FBtnClose.ClrNorm := CLR_PANEL;
FBtnClose.ClrBorder := TColor($00404040);
FBtnClose.ClrHot := TColor($00303030);
FBtnClose.ClrActive := TColor($00003300);
FBtnClose.ClrText := CLR_TEXT;
FBtnClose.ClrTextAct:= CLR_ACCENT;
FBtnClose.Font.Name := UI_FONT;
FBtnClose.Font.Size := 9;
SelectPage(FPageAudio, FNavAudio);
end;
procedure TSettingsForm.NavClick(Sender: TObject);
begin
if Sender = FNavAudio then SelectPage(FPageAudio, FNavAudio)
else if Sender = FNavDisplay then SelectPage(FPageDisplay, FNavDisplay)
else if Sender = FNavSpectrum then SelectPage(FPageSpectrum, FNavSpectrum)
else if Sender = FNavWaterfall then SelectPage(FPageWaterfall, FNavWaterfall)
else if Sender = FNavTransmit then SelectPage(FPageTransmit, FNavTransmit)
else if Sender = FNavPA then SelectPage(FPagePA, FNavPA)
else if Sender = FNavAdvanced then SelectPage(FPageAdvanced, FNavAdvanced)
else if Sender = FNavCAT then SelectPage(FPageCAT, FNavCAT)
else if Sender = FNavAlex then SelectPage(FPageAlex, FNavAlex)
else if Sender = FNavXvtr then SelectPage(FPageXvtr, FNavXvtr)
else if Sender = FNavQO then SelectPage(FPageQO, FNavQO);
end;
procedure TSettingsForm.SelectPage(APage: TWinControl; ANav: TFlatButton);
begin
FPageAudio.Visible := APage = FPageAudio;
FPageDisplay.Visible := APage = FPageDisplay;
FPageSpectrum.Visible := APage = FPageSpectrum;
FPageWaterfall.Visible:= APage = FPageWaterfall;
FPageTransmit.Visible := APage = FPageTransmit;
FPagePA.Visible := APage = FPagePA;
FPageAdvanced.Visible := APage = FPageAdvanced;
FPageCAT.Visible := APage = FPageCAT;
FPageAlex.Visible := APage = FPageAlex;
FPageXvtr.Visible := APage = FPageXvtr;
FPageQO.Visible := APage = FPageQO;
FNavAudio.Active := ANav = FNavAudio;
FNavDisplay.Active := ANav = FNavDisplay;
FNavSpectrum.Active := ANav = FNavSpectrum;
FNavWaterfall.Active:= ANav = FNavWaterfall;
FNavTransmit.Active := ANav = FNavTransmit;
FNavPA.Active := ANav = FNavPA;
FNavAdvanced.Active := ANav = FNavAdvanced;
FNavCAT.Active := ANav = FNavCAT;
FNavAlex.Active := ANav = FNavAlex;
FNavXvtr.Active := ANav = FNavXvtr;
FNavQO.Active := ANav = FNavQO;
end;
// ---------------------------------------------------------------------------
// BuildAudioTab
// ---------------------------------------------------------------------------
procedure TSettingsForm.BuildAudioTab;
const
MARGIN = 22;
PAD = 18;
R1 = 42;
STEP = 42;
LW = 150;
CX = PAD + LW + 12;
CMB_W = 460;
var
Grp: TPanel;
begin
with TLabel.Create(Self) do
begin
Parent := FPageAudio;
Caption := 'Audio';
SetBounds(MARGIN, 18, 400, 28);
Font.Name := UI_FONT;
Font.Size := 16;
Font.Style := [fsBold];
Font.Color := CLR_TEXT;
end;
with TLabel.Create(Self) do
begin
Parent := FPageAudio;
Caption := 'Input and output devices used by the receiver and transmitter.';
SetBounds(MARGIN, 48, 560, 20);
Font.Name := UI_FONT;
Font.Size := 9;
Font.Color := CLR_TEXTDIM;
end;
Grp := MakeGroupPanel(FPageAudio, 'Audio Devices', MARGIN, 86, 650, 184);
MakeLbl(Grp, 'TX input device', PAD, R1 + 5, LW);
FCmbTXDev := MakeCombo(Grp, CX, R1, CMB_W, OnTXDevChange);
FCmbTXDev.Items.Add('None');
MakeLbl(Grp, 'RX output device', PAD, R1 + STEP + 5, LW);
FCmbRXDev := MakeCombo(Grp, CX, R1 + STEP, CMB_W, OnRXDevChange);
FCmbRXDev.Items.Add('Default');
MakeLbl(Grp, 'Output buffer', PAD, R1 + STEP * 2 + 5, LW);
FCmbAudioBuffer := MakeCombo(Grp, CX, R1 + STEP * 2, 160, OnAudioBufferCmbChange);
FCmbAudioBuffer.Items.Add('128 frames');
FCmbAudioBuffer.Items.Add('256 frames');
FCmbAudioBuffer.Items.Add('512 frames');
FCmbAudioBuffer.ItemIndex := 0;
Grp := MakeGroupPanel(FPageAudio, 'Transceiver Audio', MARGIN, 290, 650, 92);
FChkSendAudioToRadio := TFlatCheckBox.Create(Self);
FChkSendAudioToRadio.Parent := Grp;
FChkSendAudioToRadio.Caption := 'Send audio stream to radio (built-in speaker)';
FChkSendAudioToRadio.SetBounds(PAD, R1, 420, 22);
FChkSendAudioToRadio.Checked := False;
FChkSendAudioToRadio.Font.Color := CLR_TEXT;
FChkSendAudioToRadio.Font.Name := UI_FONT;
FChkSendAudioToRadio.Font.Size := 9;
FChkSendAudioToRadio.OnChange := OnSendAudioToRadioChkChange;
end;
// ---------------------------------------------------------------------------
// BuildDisplayTab
// ---------------------------------------------------------------------------
procedure TSettingsForm.BuildDisplayTab;
const
MARGIN = 22;
PAD = 18;
R1 = 42;
CHKW = 220;
LW = 150;
CX = PAD + LW + 12;
var
i: Integer;
Grp: TPanel;
begin
with TLabel.Create(Self) do
begin
Parent := FPageDisplay;
Caption := 'Display';
SetBounds(MARGIN, 18, 400, 28);
Font.Name := UI_FONT;
Font.Size := 16;
Font.Style := [fsBold];
Font.Color := CLR_TEXT;
end;
with TLabel.Create(Self) do
begin
Parent := FPageDisplay;
Caption := 'General visual options. Spectrum and waterfall processing are split into their own pages.';
SetBounds(MARGIN, 48, 680, 20);
Font.Name := UI_FONT;
Font.Size := 9;
Font.Color := CLR_TEXTDIM;
end;
Grp := MakeGroupPanel(FPageDisplay, 'Visible Panes', MARGIN, 86, 650, 112);
FChkShowSpectrum := TFlatCheckBox.Create(Self);
FChkShowSpectrum.Parent := Grp;
FChkShowSpectrum.Caption := 'Spectrum';
FChkShowSpectrum.SetBounds(PAD, R1, CHKW, 22);
FChkShowSpectrum.Checked := True;
FChkShowSpectrum.Font.Color := CLR_TEXT;
FChkShowSpectrum.Font.Name := UI_FONT;
FChkShowSpectrum.Font.Size := 9;
FChkShowSpectrum.OnChange := OnVisibilityChkChange;
FChkShowWaterfall := TFlatCheckBox.Create(Self);
FChkShowWaterfall.Parent := Grp;
FChkShowWaterfall.Caption := 'Waterfall';
FChkShowWaterfall.SetBounds(PAD + CHKW + 20, R1, CHKW, 22);
FChkShowWaterfall.Checked := True;
FChkShowWaterfall.Font.Color := CLR_TEXT;
FChkShowWaterfall.Font.Name := UI_FONT;
FChkShowWaterfall.Font.Size := 9;
FChkShowWaterfall.OnChange := OnVisibilityChkChange;
FChkShowWideband := TFlatCheckBox.Create(Self);
FChkShowWideband.Parent := Grp;
FChkShowWideband.Caption := 'Wideband';
FChkShowWideband.SetBounds(PAD + (CHKW + 20) * 2, R1, CHKW, 22);
FChkShowWideband.Checked := False;
FChkShowWideband.Font.Color := CLR_TEXT;
FChkShowWideband.Font.Name := UI_FONT;
FChkShowWideband.Font.Size := 9;
FChkShowWideband.OnChange := OnVisibilityChkChange;
FChkWidebandFill := TFlatCheckBox.Create(Self);
FChkWidebandFill.Parent := Grp;
FChkWidebandFill.Caption := 'Wideband gradient fill';
FChkWidebandFill.SetBounds(PAD + (CHKW + 20) * 2, R1 + 28, CHKW, 22);
FChkWidebandFill.Checked := False;
FChkWidebandFill.Font.Color := CLR_TEXT;
FChkWidebandFill.Font.Name := UI_FONT;
FChkWidebandFill.Font.Size := 9;
FChkWidebandFill.OnChange := OnVisibilityChkChange;
FChkSpectrumFill := TFlatCheckBox.Create(Self);
FChkSpectrumFill.Parent := Grp;
FChkSpectrumFill.Caption := 'Spectrum gradient fill';
FChkSpectrumFill.SetBounds(PAD, R1 + 28, CHKW, 22);
FChkSpectrumFill.Checked := True;
FChkSpectrumFill.Font.Color := CLR_TEXT;
FChkSpectrumFill.Font.Name := UI_FONT;
FChkSpectrumFill.Font.Size := 9;
FChkSpectrumFill.OnChange := OnVisibilityChkChange;
Grp := MakeGroupPanel(FPageDisplay, 'Performance', MARGIN, 212, 650, 92);
MakeLbl(Grp, 'Refresh rate', PAD, R1 + 5, LW);
FCmbFPS := MakeCombo(Grp, CX, R1, 140, OnFPSCmbChange);
for i := 0 to High(FPS_NAMES) do
FCmbFPS.Items.Add(FPS_NAMES[i]);
FCmbFPS.ItemIndex := 8;
Grp := MakeGroupPanel(FPageDisplay, 'Appearance', MARGIN, 318, 650, 68);
FChkLightTheme := TFlatCheckBox.Create(Self);
FChkLightTheme.Parent := Grp;
FChkLightTheme.Caption := 'Light theme';
FChkLightTheme.SetBounds(PAD, R1, CHKW, 22);
FChkLightTheme.Checked := False;
FChkLightTheme.Font.Color := CLR_TEXT;
FChkLightTheme.Font.Name := UI_FONT;
FChkLightTheme.Font.Size := 9;
FChkLightTheme.OnChange := OnLightThemeChkChange;
Grp := MakeGroupPanel(FPageDisplay, 'VFO Display', MARGIN, 404, 650, 92);
MakeLbl(Grp, 'Frequency range', PAD, R1 + 5, LW);
FCmbFreqMhzDigits := MakeCombo(Grp, CX, R1, 240, OnFreqMhzDigitsCmbChange);
FCmbFreqMhzDigits.Items.Add('up to 999 MHz (3 digits)');
FCmbFreqMhzDigits.Items.Add('up to 9.999 GHz (4 digits)');
FCmbFreqMhzDigits.Items.Add('up to 99.999 GHz (5 digits)');
FCmbFreqMhzDigits.ItemIndex := 0;
end;
// ---------------------------------------------------------------------------
// BuildRX1Tab
// ---------------------------------------------------------------------------
procedure TSettingsForm.BuildRX1Tab;
const
MARGIN = 22;
PAD = 18;
LW = 150;
CX = PAD + LW + 12;
CMB_W = 230;
CMB_W_LONG = 300;
SPW = 96;
EDT_W = 96;
STEP = 42;
R1 = 42;
var
Grp: TPanel;
i: Integer;
function MkFloat(AX, AY, AW: Integer; AValue, AMin, AMax, AInc: Double;
AOnChange: TNotifyEvent): TFlatFloatSpinEdit;
begin
Result := TFlatFloatSpinEdit.Create(Self);
Result.Parent := Grp;
Result.SetBounds(AX, AY, AW, BTN_H);
Result.Value := AValue; Result.MinValue := AMin;
Result.MaxValue := AMax; Result.Increment := AInc;
Result.DecimalPlaces := 1;
Result.Color := CLR_INPUT; Result.Font.Color := CLR_INPUT_TEXT;
Result.Font.Name := UI_FONT; Result.Font.Size := 9;
Result.OnChange := AOnChange;
end;
function MkEdit(AX, AY, AW: Integer; const AText: string;
AOnExit: TNotifyEvent): TFlatEdit;
begin
Result := TFlatEdit.Create(Self);
Result.Parent := Grp;
Result.SetBounds(AX, AY, AW, BTN_H);
Result.Color := CLR_INPUT; Result.Font.Color := CLR_INPUT_TEXT;
Result.Font.Name := UI_FONT; Result.Font.Size := 9;
Result.Text := AText; Result.OnExit := AOnExit;
end;
begin
with TLabel.Create(Self) do
begin
Parent := FPageSpectrum;
Caption := 'Spectrum';
SetBounds(MARGIN, 18, 400, 28);
Font.Name := UI_FONT;
Font.Size := 16;
Font.Style := [fsBold];
Font.Color := CLR_TEXT;
end;
with TLabel.Create(Self) do
begin
Parent := FPageSpectrum;
Caption := 'Analyzer, detector, averaging and grid parameters for RX1.';
SetBounds(MARGIN, 48, 620, 20);
Font.Name := UI_FONT;
Font.Size := 9;
Font.Color := CLR_TEXTDIM;
end;
Grp := MakeGroupPanel(FPageSpectrum, 'Analyzer', MARGIN, 86, 650, 128);
MakeLbl(Grp, 'FFT Size:', PAD, R1 + 5, LW);
FCmbFFTSize := MakeCombo(Grp, CX, R1, CMB_W, OnFFTSizeChange);
for i := 0 to High(FFT_NAMES) do FCmbFFTSize.Items.Add(FFT_NAMES[i]);
MakeLbl(Grp, 'Window:', PAD, R1 + STEP + 5, LW);
FCmbWindow := MakeCombo(Grp, CX, R1 + STEP, CMB_W_LONG, OnWindowChange);
for i := 0 to High(WINDOW_NAMES) do FCmbWindow.Items.Add(WINDOW_NAMES[i]);
Grp := MakeGroupPanel(FPageSpectrum, 'Detection && Averaging', MARGIN, 230, 650, 170);
MakeLbl(Grp, 'Detector:', PAD, R1 + 5, LW);
FCmbSpecDet := MakeCombo(Grp, CX, R1, CMB_W, OnSpecDetChange);
for i := 0 to High(DETECTOR_NAMES) do FCmbSpecDet.Items.Add(DETECTOR_NAMES[i]);
MakeLbl(Grp, 'Avg Mode:', PAD, R1 + STEP + 5, LW);
FCmbSpecAvg := MakeCombo(Grp, CX, R1 + STEP, CMB_W, OnSpecAvgModeChange);
for i := 0 to High(AVGMODE_NAMES) do FCmbSpecAvg.Items.Add(AVGMODE_NAMES[i]);
MakeLbl(Grp, 'Avg Time (ms):', PAD, R1 + 2 * STEP + 5, LW);
FSpecAvgTime := MkFloat(CX, R1 + 2 * STEP, SPW, 30.0, 0.0, 5000.0, 10.0, OnSpecAvgTimeChange);
Grp := MakeGroupPanel(FPageSpectrum, 'Rendering', MARGIN, 602, 650, 128);
MakeLbl(Grp, 'Anti-aliasing:', PAD, R1 + 5, LW);
FCmbSpecMSAA := MakeCombo(Grp, CX, R1, 130, OnSpecMSAACtlChange);
for i := 0 to High(MSAA_NAMES) do FCmbSpecMSAA.Items.Add(MSAA_NAMES[i]);
FCmbSpecMSAA.ItemIndex := 2; // 4x
MakeLbl(Grp, '(GL spectrum curve smoothing, MSAA)', CX + 140, R1 + 5, 300);
Grp := MakeGroupPanel(FPageSpectrum, 'Grid', MARGIN, 416, 650, 170);
MakeLbl(Grp, 'Reference level:', PAD, R1 + 5, LW);
FEdRefLevel := MkEdit(CX, R1, EDT_W, '-20', OnRefLevelChange);
MakeLbl(Grp, 'Range (dB):', PAD, R1 + STEP + 5, LW);
FEdRange := MkEdit(CX, R1 + STEP, EDT_W, '110', OnRangeChange);
MakeLbl(Grp, 'Grid step:', PAD, R1 + 2 * STEP + 5, LW);
FCmbGridStep := MakeCombo(Grp, CX, R1 + 2 * STEP, 130, OnGridStepChange);
for i := 0 to High(GRID_STEP_NAMES) do FCmbGridStep.Items.Add(GRID_STEP_NAMES[i]);
FCmbGridStep.ItemIndex := 1;
with TLabel.Create(Self) do
begin
Parent := FPageWaterfall;
Caption := 'Waterfall';
SetBounds(MARGIN, 18, 400, 28);
Font.Name := UI_FONT;
Font.Size := 16;
Font.Style := [fsBold];
Font.Color := CLR_TEXT;
end;
with TLabel.Create(Self) do
begin
Parent := FPageWaterfall;
Caption := 'Waterfall detector, smoothing and color scale thresholds.';
SetBounds(MARGIN, 48, 620, 20);
Font.Name := UI_FONT;
Font.Size := 9;
Font.Color := CLR_TEXTDIM;
end;
Grp := MakeGroupPanel(FPageWaterfall, 'Detection && Averaging', MARGIN, 86, 650, 170);
MakeLbl(Grp, 'Detector:', PAD, R1 + 5, LW);
FCmbWfDet := MakeCombo(Grp, CX, R1, CMB_W, OnWfDetChange);
for i := 0 to High(DETECTOR_NAMES) do FCmbWfDet.Items.Add(DETECTOR_NAMES[i]);
MakeLbl(Grp, 'Avg Mode:', PAD, R1 + STEP + 5, LW);
FCmbWfAvg := MakeCombo(Grp, CX, R1 + STEP, CMB_W, OnWfAvgModeChange);
for i := 0 to High(WF_AVGMODE_NAMES) do FCmbWfAvg.Items.Add(WF_AVGMODE_NAMES[i]);
MakeLbl(Grp, 'Avg Time (ms):', PAD, R1 + 2 * STEP + 5, LW);
FWfAvgTime := MkFloat(CX, R1 + 2 * STEP, SPW, 120.0, 0.0, 5000.0, 10.0, OnWfAvgTimeChange);
Grp := MakeGroupPanel(FPageWaterfall, 'Color Scale', MARGIN, 272, 650, 170);
MakeLbl(Grp, 'High (dBm):', PAD, R1 + 5, LW);
FWfHigh := MkFloat(CX, R1, SPW, -80.0, -160.0, 20.0, 1.0, OnWfThresholdChange);
MakeLbl(Grp, 'Low (dBm):', PAD, R1 + STEP + 5, LW);
FWfLow := MkFloat(CX, R1 + STEP, SPW, -130.0, -180.0, 0.0, 1.0, OnWfThresholdChange);
MakeLbl(Grp, 'AGC Offset (dB):', PAD, R1 + 2 * STEP + 5, LW);
FWfAGCOffset := MkFloat(CX, R1 + 2 * STEP, SPW, 0.0, -40.0, 40.0, 0.5, OnWfThresholdChange);
Grp := MakeGroupPanel(FPageWaterfall, 'AGC && Noise Floor', MARGIN, 458, 650, 140);
FChkWfAGC := TFlatCheckBox.Create(Self);
FChkWfAGC.Parent := Grp;
FChkWfAGC.Caption := 'Waterfall AGC';
FChkWfAGC.SetBounds(PAD, R1, 200, 22);
FChkWfAGC.Checked := True;
FChkWfAGC.Font.Color := CLR_TEXT;
FChkWfAGC.Font.Name := UI_FONT;
FChkWfAGC.Font.Size := 9;
FChkWfAGC.OnChange := OnWfAGCNFChkChange;
FChkWfNF := TFlatCheckBox.Create(Self);
FChkWfNF.Parent := Grp;
FChkWfNF.Caption := 'Waterfall NF';
FChkWfNF.SetBounds(PAD, R1 + STEP, 200, 22);
FChkWfNF.Checked := False;
FChkWfNF.Font.Color := CLR_TEXT;
FChkWfNF.Font.Name := UI_FONT;
FChkWfNF.Font.Size := 9;
FChkWfNF.OnChange := OnWfAGCNFChkChange;
Grp := MakeGroupPanel(FPageWaterfall, 'Rendering', MARGIN, 614, 650, 212);
MakeLbl(Grp, 'Palette:', PAD, R1 + 5, LW);
FCmbWfPalette := MakeCombo(Grp, CX, R1, CMB_W, OnWfRenderCtlChange);
for i := 0 to High(WF_PALETTE_NAMES) do FCmbWfPalette.Items.Add(WF_PALETTE_NAMES[i]);
FCmbWfPalette.ItemIndex := 0;
MakeLbl(Grp, 'Gamma:', PAD, R1 + STEP + 5, LW);
FWfGamma := MkFloat(CX, R1 + STEP, SPW, 1.0, 0.3, 3.0, 0.05, OnWfRenderCtlChange);
FWfGamma.DecimalPlaces := 2;
MakeLbl(Grp, 'Resolution:', PAD, R1 + 2 * STEP + 5, LW);
FCmbWfRes := MakeCombo(Grp, CX, R1 + 2 * STEP, 130, OnWfRenderCtlChange);
for i := 0 to High(DISPLAY_RES_NAMES) do FCmbWfRes.Items.Add(DISPLAY_RES_NAMES[i]);
FCmbWfRes.ItemIndex := High(DISPLAY_RES_NAMES);
MakeLbl(Grp, 'Speed:', PAD, R1 + 3 * STEP + 5, LW);
FCmbWfSpeed := MakeCombo(Grp, CX, R1 + 3 * STEP, 130, OnWfRenderCtlChange);
for i := 0 to High(WF_SPEED_NAMES) do FCmbWfSpeed.Items.Add(WF_SPEED_NAMES[i]);
FCmbWfSpeed.ItemIndex := 0;
end;
// ---------------------------------------------------------------------------
// BuildTransmitTab — все настройки TX-тракта (по образцу Thetis Setup→Transmit)
// ---------------------------------------------------------------------------
procedure TSettingsForm.BuildTransmitTab;
const
MARGIN = 22;
PAD = 18;
LW = 150;
CX = PAD + LW + 12;
STEP = 42;
R1 = 42;
SPW = 96;
GRP_W = 760;
var
Grp: TPanel;
Y, i: Integer;
function MkSpin(AParent: TWinControl; AX, AY: Integer; AMin, AMax, AVal: Integer): TFlatSpinEdit;
begin
Result := TFlatSpinEdit.Create(Self);
Result.Parent := AParent;
Result.SetBounds(AX, AY, SPW, BTN_H);
Result.MinValue := AMin;
Result.MaxValue := AMax;
Result.Value := AVal;
Result.Color := CLR_INPUT;
Result.Font.Color := CLR_INPUT_TEXT;
Result.Font.Name := UI_FONT;
Result.Font.Size := 9;
Result.OnChange := OnTXAnyChange;
end;
function MkFloat(AParent: TWinControl; AX, AY: Integer;
AVal, AMin, AMax, AInc: Double): TFlatFloatSpinEdit;
begin
Result := TFlatFloatSpinEdit.Create(Self);
Result.Parent := AParent;
Result.SetBounds(AX, AY, SPW, BTN_H);
Result.MinValue := AMin;
Result.MaxValue := AMax;
Result.Increment := AInc;
Result.Value := AVal;
Result.DecimalPlaces := 2;
Result.Color := CLR_INPUT;
Result.Font.Color := CLR_INPUT_TEXT;
Result.Font.Name := UI_FONT;
Result.Font.Size := 9;
Result.OnChange := OnTXAnyChange;
end;
function MkChk(AParent: TWinControl; AX, AY, AW: Integer; const ACap: string): TFlatCheckBox;
begin
Result := TFlatCheckBox.Create(Self);
Result.Parent := AParent;
Result.Caption := ACap;
Result.SetBounds(AX, AY, AW, 22);
Result.Font.Color := CLR_TEXT;
Result.Font.Name := UI_FONT;
Result.Font.Size := 9;
Result.OnChange := OnTXAnyChange;
end;
function MkCmb(AParent: TWinControl; AX, AY, AW: Integer): TFlatComboBox;
begin
Result := MakeCombo(AParent, AX, AY, AW, OnTXAnyChange);
end;
begin
with TLabel.Create(Self) do
begin
Parent := FPageTransmit;
Caption := 'Transmit';
SetBounds(MARGIN, 18, 400, 28);
Font.Name := UI_FONT;
Font.Size := 16;
Font.Style := [fsBold];
Font.Color := CLR_TEXT;
end;
with TLabel.Create(Self) do
begin
Parent := FPageTransmit;
Caption := 'TX-цепь WDSP: микрофон, фильтр, динамическая обработка, модуляция.';
SetBounds(MARGIN, 48, 700, 20);
Font.Name := UI_FONT;
Font.Size := 9;
Font.Color := CLR_TEXTDIM;
end;
// ---- Mic options ----
// Mic source (Radio vs SoundCard) определяется автоматически по источнику PTT.
Y := 86;
Grp := MakeGroupPanel(FPageTransmit, 'Microphone', MARGIN, Y, GRP_W, 188);
// Row 0: Mic jack selector | Boost (Mic In) или LineInGain (Line In) | Bias | PTT
MakeLbl(Grp, 'Mic jack', PAD, R1 + 5, 65);
FCmbTXMicJack := MkCmb(Grp, PAD + 68, R1, 110);
FCmbTXMicJack.Items.Add('Mic In');
FCmbTXMicJack.Items.Add('Line In');
FCmbTXMicJack.ItemIndex := 0;
FCmbTXMicJack.OnChange := OnMicJackChange;
FChkTXMicBoost := MkChk(Grp, PAD + 190, R1, 165, 'Mic boost +20 dB');
FLblLineInGain := MakeLbl(Grp, 'Line In gain', PAD + 190, R1 + 5, 80);
FEdLineInGain := MkFloat(Grp, PAD + 190 + 82, R1, 0.0, -34.5, 12.0, 1.5);
FEdLineInGain.DecimalPlaces := 1;
FChkTXMicBias := MkChk(Grp, PAD + 375, R1, 105, 'Mic bias');
FChkTXMicPTT := MkChk(Grp, PAD + 490, R1, 160, 'Mic-jack PTT');
// Row 1: Connector Tip/Ring (только Orion boards 4 и 5)
FLblTipRing := MakeLbl(Grp, 'Connector', PAD, R1 + STEP + 5, 70);
FCmbTXMicTR := MkCmb(Grp, PAD + 73, R1 + STEP, 90);
FCmbTXMicTR.Items.Add('Tip'); // MicTipRing=False, bit3=0
FCmbTXMicTR.Items.Add('Ring'); // MicTipRing=True, bit3=1
FCmbTXMicTR.ItemIndex := 0;
FLblTipRing.Visible := False;
FCmbTXMicTR.Visible := False;
// Row 2: Mic gain | ATT on TX
MakeLbl(Grp, 'Mic gain (dB)', PAD, R1 + 2 * STEP + 5, LW);
FEdTXMicGain := MkFloat(Grp, CX, R1 + 2 * STEP, 10.0, -20.0, 60.0, 0.5);
MakeLbl(Grp, 'ATT on TX (dB)', PAD + 320, R1 + 2 * STEP + 5, 110);
FEdTXAttOnTX := MkSpin(Grp, PAD + 320 + 120, R1 + 2 * STEP, 0, 31, 31);
// ---- Tune (TUN button) ----
Inc(Y, 188 + 12);
Grp := MakeGroupPanel(FPageTransmit, 'Tune (TUN)', MARGIN, Y, GRP_W, 100);
MakeLbl(Grp, 'Tune level (%)', PAD, R1 + 5, LW);
FEdTUNLevel := MkSpin(Grp, CX, R1, 0, 100, 10);
MakeLbl(Grp, 'Tone freq (Hz)', PAD + 320, R1 + 5, 110);
FEdTUNFreq := MkSpin(Grp, PAD + 320 + 120, R1, 100, 5000, 750);
// ---- TX bandpass filter ----
Inc(Y, 100 + 12);
Grp := MakeGroupPanel(FPageTransmit, 'TX Filter (bandpass)', MARGIN, Y, GRP_W, 220);
MakeLbl(Grp, 'Low cut (Hz)', PAD, R1 + 5, LW);
FEdTXFiltLow := MkSpin(Grp, CX, R1, 0, 8000, 100);
MakeLbl(Grp, 'High cut (Hz)', PAD + 320, R1 + 5, 90);
FEdTXFiltHigh := MkSpin(Grp, PAD + 320 + 100, R1, 100, 12000, 2800);
MakeLbl(Grp, 'FIR length', PAD, R1 + STEP + 5, LW);
FCmbTXFiltNC := MkCmb(Grp, CX, R1 + STEP, 140);
FCmbTXFiltNC.Items.AddStrings(['256','512','1024','2048','4096']);
FCmbTXFiltNC.ItemIndex := 2;
MakeLbl(Grp, 'Phase mode', PAD, R1 + 2 * STEP + 5, LW);
FCmbTXFiltMP := MkCmb(Grp, CX, R1 + 2 * STEP, 200);
FCmbTXFiltMP.Items.AddStrings(['Linear Phase','Low Latency']);
FCmbTXFiltMP.ItemIndex := 1;
MakeLbl(Grp, 'Window', PAD, R1 + 3 * STEP + 5, LW);
FCmbTXFiltWindow := MkCmb(Grp, CX, R1 + 3 * STEP, 240);
for i := 0 to High(WINDOW_NAMES) do
FCmbTXFiltWindow.Items.Add(WINDOW_NAMES[i]);
FCmbTXFiltWindow.ItemIndex := 1;
// ---- Compressor / Leveler / ALC / Phase Rotator ----
Inc(Y, 220 + 12);
Grp := MakeGroupPanel(FPageTransmit, 'Dynamic processing', MARGIN, Y, GRP_W, 220);
FChkTXCompOn := MkChk(Grp, PAD, R1, 220, 'Compressor');
MakeLbl(Grp, 'Gain (dB)', PAD + 230, R1 + 5, 80);
FEdTXCompGain := MkFloat(Grp, PAD + 320, R1, 5.0, 0.0, 20.0, 0.5);
FChkTXLevOn := MkChk(Grp, PAD, R1 + STEP, 220, 'Leveler');
MakeLbl(Grp, 'Top (dB)', PAD + 230, R1 + STEP + 5, 80);
FEdTXLevTop := MkFloat(Grp, PAD + 320, R1 + STEP, 5.0, 0.0, 20.0, 0.5);
MakeLbl(Grp, 'Decay (ms)', PAD + 430, R1 + STEP + 5, 80);
FEdTXLevDecay := MkSpin(Grp, PAD + 520, R1 + STEP, 1, 5000, 500);
FChkTXALCOn := MkChk(Grp, PAD, R1 + 2 * STEP, 220, 'ALC');
MakeLbl(Grp, 'Max gain', PAD + 230, R1 + 2 * STEP + 5, 80);
FEdTXALCMaxGain := MkFloat(Grp, PAD + 320, R1 + 2 * STEP, 0.0, -20.0, 20.0, 0.5);
MakeLbl(Grp, 'Decay (ms)', PAD + 430, R1 + 2 * STEP + 5, 80);
FEdTXALCDecay := MkSpin(Grp, PAD + 520, R1 + 2 * STEP, 1, 5000, 10);
FChkTXPHROn := MkChk(Grp, PAD, R1 + 3 * STEP, 220, 'Phase Rotator');
MakeLbl(Grp, 'Stages', PAD + 230, R1 + 3 * STEP + 5, 80);
FEdTXPHRStages := MkSpin(Grp, PAD + 320, R1 + 3 * STEP, 2, 16, 8);
MakeLbl(Grp, 'Corner (Hz)', PAD + 430, R1 + 3 * STEP + 5, 80);
FEdTXPHRFreq := MkFloat(Grp, PAD + 520, R1 + 3 * STEP, 338.0, 50.0, 3000.0, 1.0);
// ---- TX EQ ----
Inc(Y, 220 + 12);
Grp := MakeGroupPanel(FPageTransmit, 'TX Equalizer', MARGIN, Y, GRP_W, 390);
FTXEQControl := TEqualizerControl.Create(Self);
FTXEQControl.Parent := Grp;
FTXEQControl.SetBounds(PAD, R1, GRP_W - 2 * PAD, 326);
FTXEQControl.Font.Name := UI_FONT;
FTXEQControl.Font.Size := 9;
FTXEQControl.SetThemeColors(CLR_PANEL, TColor($00202020), CLR_BORDER,
TColor($00383838), CLR_TEXT, CLR_TEXTDIM, CLR_ACCENT, TColor($0055DDEE));
FTXEQControl.OnChange := OnTXAnyChange;
// ---- AM / FM / CTCSS ----
Inc(Y, 390 + 12);
Grp := MakeGroupPanel(FPageTransmit, 'AM / FM / CTCSS', MARGIN, Y, GRP_W, 260);
MakeLbl(Grp, 'AM carrier level', PAD, R1 + 5, LW);
FEdTXAMCarrier := MkFloat(Grp, CX, R1, 0.5, 0.0, 1.0, 0.05);
MakeLbl(Grp, 'FM deviation (Hz)', PAD, R1 + STEP + 5, LW);
FEdTXFMDev := MkFloat(Grp, CX, R1 + STEP, 5000.0, 1000.0, 25000.0, 100.0);
MakeLbl(Grp, 'FM AF low cut', PAD, R1 + 2 * STEP + 5, LW);
FEdTXFMLow := MkSpin(Grp, CX, R1 + 2 * STEP, 0, 5000, 300);
MakeLbl(Grp, 'FM AF high cut', PAD + 320, R1 + 2 * STEP + 5, 100);
FEdTXFMHigh := MkSpin(Grp, PAD + 320 + 110, R1 + 2 * STEP, 100, 8000, 3000);
MakeLbl(Grp, 'FM emphasis', PAD, R1 + 3 * STEP + 5, LW);
FCmbTXFMEmph := MkCmb(Grp, CX, R1 + 3 * STEP, 180);
FCmbTXFMEmph.Items.AddStrings(['Pre-EQ','Post-EQ']);
FCmbTXFMEmph.ItemIndex := 1;
FChkTXCTCSSOn := MkChk(Grp, PAD, R1 + 4 * STEP, 220, 'CTCSS tone');
MakeLbl(Grp, 'Freq (Hz)', PAD + 230, R1 + 4 * STEP + 5, 80);
FEdTXCTCSSFreq := MkFloat(Grp, PAD + 320, R1 + 4 * STEP, 100.0, 50.0, 300.0, 0.1);
// ---- TX Display (отдельный analyzer для TX-спектра/водопада) ----
Inc(Y, 260 + 12);
Grp := MakeGroupPanel(FPageTransmit, 'TX Display (Analyzer)', MARGIN, Y, GRP_W, 318);
MakeLbl(Grp, 'FFT size', PAD, R1 + 5, LW);
FCmbTXFFT := MkCmb(Grp, CX, R1, 140);
for i := 0 to High(FFT_NAMES) do FCmbTXFFT.Items.Add(FFT_NAMES[i]);
FCmbTXFFT.ItemIndex := 2;
MakeLbl(Grp, 'Window', PAD, R1 + STEP + 5, LW);
FCmbTXWindow := MkCmb(Grp, CX, R1 + STEP, 240);
for i := 0 to High(WINDOW_NAMES) do FCmbTXWindow.Items.Add(WINDOW_NAMES[i]);
FCmbTXWindow.ItemIndex := 2;
MakeLbl(Grp, 'Spec detector',PAD, R1 + 2 * STEP + 5, LW);
FCmbTXSpecDet := MkCmb(Grp, CX, R1 + 2 * STEP, 200);
for i := 0 to High(DETECTOR_NAMES) do FCmbTXSpecDet.Items.Add(DETECTOR_NAMES[i]);
FCmbTXSpecDet.ItemIndex := 0;
MakeLbl(Grp, 'Spec average', PAD, R1 + 3 * STEP + 5, LW);
FCmbTXSpecAvg := MkCmb(Grp, CX, R1 + 3 * STEP, 200);
for i := 0 to High(AVGMODE_NAMES) do FCmbTXSpecAvg.Items.Add(AVGMODE_NAMES[i]);
FCmbTXSpecAvg.ItemIndex := 3;
MakeLbl(Grp, 'Avg time (ms)', PAD + 430, R1 + 3 * STEP + 5, 100);
FEdTXSpecAvgTime := MkFloat(Grp, PAD + 540, R1 + 3 * STEP, 30.0, 1.0, 5000.0, 1.0);
MakeLbl(Grp, 'WF detector', PAD, R1 + 4 * STEP + 5, LW);
FCmbTXWfDet := MkCmb(Grp, CX, R1 + 4 * STEP, 200);
for i := 0 to High(DETECTOR_NAMES) do FCmbTXWfDet.Items.Add(DETECTOR_NAMES[i]);
FCmbTXWfDet.ItemIndex := 0;
MakeLbl(Grp, 'WF average', PAD, R1 + 5 * STEP + 5, LW);
FCmbTXWfAvg := MkCmb(Grp, CX, R1 + 5 * STEP, 200);
for i := 0 to High(WF_AVGMODE_NAMES) do FCmbTXWfAvg.Items.Add(WF_AVGMODE_NAMES[i]);
FCmbTXWfAvg.ItemIndex := 3;
MakeLbl(Grp, 'Avg time (ms)', PAD + 430, R1 + 5 * STEP + 5, 100);
FEdTXWfAvgTime := MkFloat(Grp, PAD + 540, R1 + 5 * STEP, 120.0, 1.0, 5000.0, 1.0);
// ---- TX Grid (RefLevel / Range / GridStep — отдельные от RX) ----
Inc(Y, 318 + 12);
Grp := MakeGroupPanel(FPageTransmit, 'TX Grid', MARGIN, Y, GRP_W, 170);
MakeLbl(Grp, 'Reference level', PAD, R1 + 5, LW);
FEdTXRefLevel := TFlatEdit.Create(Self);
FEdTXRefLevel.Parent := Grp;
FEdTXRefLevel.SetBounds(CX, R1, 96, BTN_H);
FEdTXRefLevel.Color := CLR_INPUT;
FEdTXRefLevel.Font.Color := CLR_INPUT_TEXT;
FEdTXRefLevel.Font.Name := UI_FONT;
FEdTXRefLevel.Font.Size := 9;
FEdTXRefLevel.Text := '0';
FEdTXRefLevel.OnExit := OnTXAnyChange;
MakeLbl(Grp, 'Range (dB)', PAD, R1 + STEP + 5, LW);
FEdTXRange := TFlatEdit.Create(Self);
FEdTXRange.Parent := Grp;
FEdTXRange.SetBounds(CX, R1 + STEP, 96, BTN_H);
FEdTXRange.Color := CLR_INPUT;
FEdTXRange.Font.Color := CLR_INPUT_TEXT;
FEdTXRange.Font.Name := UI_FONT;
FEdTXRange.Font.Size := 9;
FEdTXRange.Text := '80';
FEdTXRange.OnExit := OnTXAnyChange;
MakeLbl(Grp, 'Grid step', PAD, R1 + 2 * STEP + 5, LW);
FCmbTXGridStep := MkCmb(Grp, CX, R1 + 2 * STEP, 130);
for i := 0 to High(GRID_STEP_NAMES) do FCmbTXGridStep.Items.Add(GRID_STEP_NAMES[i]);
FCmbTXGridStep.ItemIndex := 1;
end;
procedure TSettingsForm.CollectTXFromUI;
var i: Integer;
begin
if FCmbTXMicJack <> nil then FTX.MicLineIn := FCmbTXMicJack.ItemIndex = 1;
if FChkTXMicBoost <> nil then FTX.MicBoost := FChkTXMicBoost.Checked;
if FEdLineInGain <> nil then FTX.LineInGainDB := FEdLineInGain.Value;
if FChkTXMicBias <> nil then FTX.MicBias := FChkTXMicBias.Checked;
if FChkTXMicPTT <> nil then FTX.MicPTTEnabled := FChkTXMicPTT.Checked;
if FCmbTXMicTR <> nil then FTX.MicTipRing := FCmbTXMicTR.ItemIndex = 1;
if FEdTXMicGain <> nil then FTX.MicGainDB := FEdTXMicGain.Value;
if FEdTXAttOnTX <> nil then FTX.AttOnTX := FEdTXAttOnTX.Value;
if FEdTUNLevel <> nil then FTX.TUNLevel := FEdTUNLevel.Value;
if FEdTUNFreq <> nil then FTX.TUNFreq := FEdTUNFreq.Value;
if FEdTXFiltLow <> nil then FTX.FilterLow := FEdTXFiltLow.Value;
if FEdTXFiltHigh <> nil then FTX.FilterHigh := FEdTXFiltHigh.Value;
if FCmbTXFiltNC <> nil then
FTX.FilterNC := StrToIntDef(FCmbTXFiltNC.Text, 1024);
if FCmbTXFiltMP <> nil then FTX.FilterMP := Max(0, FCmbTXFiltMP.ItemIndex);
if FCmbTXFiltWindow<> nil then FTX.FilterWindow := Max(0, FCmbTXFiltWindow.ItemIndex);
if FChkTXCompOn <> nil then FTX.CompressorOn := FChkTXCompOn.Checked;
if FEdTXCompGain <> nil then FTX.CompressorGain := FEdTXCompGain.Value;
if FChkTXLevOn <> nil then FTX.LevelerOn := FChkTXLevOn.Checked;
if FEdTXLevTop <> nil then FTX.LevelerTop := FEdTXLevTop.Value;
if FEdTXLevDecay <> nil then FTX.LevelerDecay := FEdTXLevDecay.Value;
if FChkTXALCOn <> nil then FTX.ALCOn := FChkTXALCOn.Checked;
if FEdTXALCMaxGain <> nil then FTX.ALCMaxGain := FEdTXALCMaxGain.Value;
if FEdTXALCDecay <> nil then FTX.ALCDecay := FEdTXALCDecay.Value;
if FChkTXPHROn <> nil then FTX.PhaseRotOn := FChkTXPHROn.Checked;
if FEdTXPHRStages <> nil then FTX.PhaseRotStages := FEdTXPHRStages.Value;
if FEdTXPHRFreq <> nil then FTX.PhaseRotFreq := FEdTXPHRFreq.Value;
if FTXEQControl <> nil then
begin
FTX.EQOn := FTXEQControl.EQEnabled;
FTX.EQNumBands := FTXEQControl.BandCount;
FTX.EQGains[0] := FTXEQControl.PreampGain;
for i := 1 to 10 do
begin
FTX.EQGains[i] := FTXEQControl.BandGain[i];
FTX.EQFreqs[i] := FTXEQControl.BandFreq[i];
end;
end;
if FEdTXAMCarrier <> nil then FTX.AMCarrierLevel := FEdTXAMCarrier.Value;
if FEdTXFMDev <> nil then FTX.FMDeviation := FEdTXFMDev.Value;
if FEdTXFMLow <> nil then FTX.FMLowCut := FEdTXFMLow.Value;
if FEdTXFMHigh <> nil then FTX.FMHighCut := FEdTXFMHigh.Value;
if FCmbTXFMEmph <> nil then FTX.FMEmphPosition := Max(0, FCmbTXFMEmph.ItemIndex);
if FChkTXCTCSSOn <> nil then FTX.CTCSSOn := FChkTXCTCSSOn.Checked;
if FEdTXCTCSSFreq <> nil then FTX.CTCSSFreq := FEdTXCTCSSFreq.Value;
if (FCmbTXFFT <> nil) and (FCmbTXFFT.ItemIndex >= 0) then
FTX.TXFFTSize := FFT_SIZES[FCmbTXFFT.ItemIndex];
if FCmbTXWindow <> nil then FTX.TXWindowType := Max(0, FCmbTXWindow.ItemIndex);
if FCmbTXSpecDet <> nil then FTX.TXSpecDetector := Max(0, FCmbTXSpecDet.ItemIndex);
if (FCmbTXSpecAvg <> nil) and (FCmbTXSpecAvg.ItemIndex >= 0) then
FTX.TXSpecAvgMode := AVGMODE_VALUES[FCmbTXSpecAvg.ItemIndex];
if FEdTXSpecAvgTime <> nil then FTX.TXSpecAvgTimeMS := FEdTXSpecAvgTime.Value;
if FCmbTXWfDet <> nil then FTX.TXWfDetector := Max(0, FCmbTXWfDet.ItemIndex);
if (FCmbTXWfAvg <> nil) and (FCmbTXWfAvg.ItemIndex >= 0) then
FTX.TXWfAvgMode := WF_AVGMODE_VALUES[FCmbTXWfAvg.ItemIndex];
if FEdTXWfAvgTime <> nil then FTX.TXWfAvgTimeMS := FEdTXWfAvgTime.Value;
if FEdTXRefLevel <> nil then
begin
Val(FEdTXRefLevel.Text, FTX.TXSpecRefLevel, i);
if i <> 0 then FTX.TXSpecRefLevel := 0.0;
end;
if FEdTXRange <> nil then
begin
Val(FEdTXRange.Text, FTX.TXSpecRange, i);
if i <> 0 then FTX.TXSpecRange := 80.0;
end;
if (FCmbTXGridStep <> nil) and (FCmbTXGridStep.ItemIndex >= 0)
and (FCmbTXGridStep.ItemIndex <= High(GRID_STEPS)) then
FTX.TXSpecGridStep := GRID_STEPS[FCmbTXGridStep.ItemIndex];
end;
procedure TSettingsForm.FireTXChange;
begin
if FLoading then Exit;
CollectTXFromUI;
if Assigned(FOnTXChange) then
FOnTXChange(FTX);
end;
procedure TSettingsForm.OnTXAnyChange(Sender: TObject);
begin
FireTXChange;
end;
procedure TSettingsForm.UpdateMicJackVisibility;
var LineIn: Boolean;
begin
if FCmbTXMicJack = nil then Exit;
LineIn := FCmbTXMicJack.ItemIndex = 1;
if FChkTXMicBoost <> nil then FChkTXMicBoost.Visible := not LineIn;
if FLblLineInGain <> nil then FLblLineInGain.Visible := LineIn;
if FEdLineInGain <> nil then FEdLineInGain.Visible := LineIn;
end;
procedure TSettingsForm.OnMicJackChange(Sender: TObject);
begin
UpdateMicJackVisibility;
FireTXChange;
end;
procedure TSettingsForm.LoadTXSettings(const T: TTXSettings; BoardType: Integer = 0);
var i: Integer; IsOrion: Boolean;
begin
FLoading := True;
try
FTX := T;
if FCmbTXMicJack <> nil then
FCmbTXMicJack.ItemIndex := IfThen(T.MicLineIn, 1, 0);
UpdateMicJackVisibility;
if FChkTXMicBoost <> nil then FChkTXMicBoost.Checked := T.MicBoost;
if FEdLineInGain <> nil then FEdLineInGain.Value := EnsureRange(T.LineInGainDB, -34.5, 12.0);
if FChkTXMicBias <> nil then FChkTXMicBias.Checked := T.MicBias;
if FChkTXMicPTT <> nil then FChkTXMicPTT.Checked := T.MicPTTEnabled;
// Tip/Ring (Orion boards 4 и 5)
IsOrion := BoardType in [4, 5];
if FLblTipRing <> nil then FLblTipRing.Visible := IsOrion;
if FCmbTXMicTR <> nil then
begin
FCmbTXMicTR.Visible := IsOrion;
FCmbTXMicTR.ItemIndex := IfThen(T.MicTipRing, 1, 0);
end;
if FEdTXMicGain <> nil then FEdTXMicGain.Value := T.MicGainDB;
if FEdTXAttOnTX <> nil then FEdTXAttOnTX.Value := EnsureRange(T.AttOnTX, 0, 31);
if FEdTUNLevel <> nil then FEdTUNLevel.Value := EnsureRange(T.TUNLevel, 0, 100);
if FEdTUNFreq <> nil then FEdTUNFreq.Value := EnsureRange(T.TUNFreq, 100, 5000);
if FEdTXFiltLow <> nil then FEdTXFiltLow.Value := T.FilterLow;
if FEdTXFiltHigh <> nil then FEdTXFiltHigh.Value := T.FilterHigh;
if FCmbTXFiltNC <> nil then FCmbTXFiltNC.Text := IntToStr(T.FilterNC);
if FCmbTXFiltMP <> nil then
FCmbTXFiltMP.ItemIndex := EnsureRange(T.FilterMP, 0, FCmbTXFiltMP.Items.Count - 1);
if FCmbTXFiltWindow<> nil then
FCmbTXFiltWindow.ItemIndex := EnsureRange(T.FilterWindow, 0, FCmbTXFiltWindow.Items.Count - 1);
if FChkTXCompOn <> nil then FChkTXCompOn.Checked := T.CompressorOn;
if FEdTXCompGain <> nil then FEdTXCompGain.Value := T.CompressorGain;
if FChkTXLevOn <> nil then FChkTXLevOn.Checked := T.LevelerOn;
if FEdTXLevTop <> nil then FEdTXLevTop.Value := T.LevelerTop;
if FEdTXLevDecay <> nil then FEdTXLevDecay.Value := T.LevelerDecay;
if FChkTXALCOn <> nil then FChkTXALCOn.Checked := T.ALCOn;
if FEdTXALCMaxGain <> nil then FEdTXALCMaxGain.Value := T.ALCMaxGain;
if FEdTXALCDecay <> nil then FEdTXALCDecay.Value := T.ALCDecay;
if FChkTXPHROn <> nil then FChkTXPHROn.Checked := T.PhaseRotOn;
if FEdTXPHRStages <> nil then FEdTXPHRStages.Value := T.PhaseRotStages;
if FEdTXPHRFreq <> nil then FEdTXPHRFreq.Value := T.PhaseRotFreq;
if FTXEQControl <> nil then
begin
FTXEQControl.EQEnabled := T.EQOn;
FTXEQControl.BandCount := T.EQNumBands;
FTXEQControl.PreampGain := T.EQGains[0];
for i := 1 to 10 do
FTXEQControl.SetBand(i, T.EQFreqs[i], T.EQGains[i]);
end;
if FEdTXAMCarrier <> nil then FEdTXAMCarrier.Value := T.AMCarrierLevel;
if FEdTXFMDev <> nil then FEdTXFMDev.Value := T.FMDeviation;
if FEdTXFMLow <> nil then FEdTXFMLow.Value := T.FMLowCut;
if FEdTXFMHigh <> nil then FEdTXFMHigh.Value := T.FMHighCut;
if FCmbTXFMEmph <> nil then
FCmbTXFMEmph.ItemIndex := EnsureRange(T.FMEmphPosition, 0, FCmbTXFMEmph.Items.Count - 1);
if FChkTXCTCSSOn <> nil then FChkTXCTCSSOn.Checked := T.CTCSSOn;
if FEdTXCTCSSFreq <> nil then FEdTXCTCSSFreq.Value := T.CTCSSFreq;
if FCmbTXFFT <> nil then
begin
FCmbTXFFT.ItemIndex := 2;
for i := 0 to High(FFT_SIZES) do
if FFT_SIZES[i] = T.TXFFTSize then
begin
FCmbTXFFT.ItemIndex := i;
Break;
end;
end;
if FCmbTXWindow <> nil then
FCmbTXWindow.ItemIndex := EnsureRange(T.TXWindowType, 0, FCmbTXWindow.Items.Count - 1);
if FCmbTXSpecDet <> nil then
FCmbTXSpecDet.ItemIndex := EnsureRange(T.TXSpecDetector, 0, FCmbTXSpecDet.Items.Count - 1);
if FCmbTXSpecAvg <> nil then
begin
FCmbTXSpecAvg.ItemIndex := 3;
for i := 0 to High(AVGMODE_VALUES) do
if AVGMODE_VALUES[i] = T.TXSpecAvgMode then
begin
FCmbTXSpecAvg.ItemIndex := i;
Break;
end;
end;
if FEdTXSpecAvgTime <> nil then FEdTXSpecAvgTime.Value := T.TXSpecAvgTimeMS;
if FCmbTXWfDet <> nil then
FCmbTXWfDet.ItemIndex := EnsureRange(T.TXWfDetector, 0, FCmbTXWfDet.Items.Count - 1);
if FCmbTXWfAvg <> nil then
begin
FCmbTXWfAvg.ItemIndex := 3;
for i := 0 to High(WF_AVGMODE_VALUES) do
if WF_AVGMODE_VALUES[i] = T.TXWfAvgMode then
begin
FCmbTXWfAvg.ItemIndex := i;
Break;
end;
end;
if FEdTXWfAvgTime <> nil then FEdTXWfAvgTime.Value := T.TXWfAvgTimeMS;
if FEdTXRefLevel <> nil then FEdTXRefLevel.Text := FormatFloat('0.#', T.TXSpecRefLevel);
if FEdTXRange <> nil then FEdTXRange.Text := FormatFloat('0.#', T.TXSpecRange);
if FCmbTXGridStep <> nil then
begin
FCmbTXGridStep.ItemIndex := 1; // дефолт 10 dB
for i := 0 to High(GRID_STEPS) do
if Abs(GRID_STEPS[i] - T.TXSpecGridStep) < 0.5 then
begin
FCmbTXGridStep.ItemIndex := i;
Break;
end;
end;
finally
FLoading := False;
end;
end;
// ---------------------------------------------------------------------------
// BuildPATab
// ---------------------------------------------------------------------------
procedure TSettingsForm.BuildPATab;
const
MARGIN = 22;
LBL_W = 54;
ED_W = 72;
COL_W = LBL_W + ED_W + 14;
COLS = 4;
VHF_LBL_W = 170;
VHF_COL_W = VHF_LBL_W + ED_W + 48;
VHF_COLS = 2;
ROW_H = 32;
GRP_PAD = 18;
R1 = 42;
GRP_H = 222;
function MakeFloatSpin(AParent: TWinControl; ALeft, ATop: Integer;
AChange: TNotifyEvent): TFlatFloatSpinEdit;
var E: TFlatFloatSpinEdit;
begin
E := TFlatFloatSpinEdit.Create(Self);
E.Parent := AParent;
E.SetBounds(ALeft, ATop, ED_W, BTN_H + 2);
E.Color := CLR_INPUT;
E.Font.Color := CLR_INPUT_TEXT;
E.Font.Name := UI_FONT;
E.Font.Size := 9;
E.MinValue := 38.8;
E.MaxValue := 100.0;
E.Increment := 0.1;
E.DecimalPlaces := 1;
E.Value := 100.0;
E.OnChange := AChange;
Result := E;
end;
var
Grp: TPanel;
Lbl: TLabel;
Spin: TFlatSpinEdit;
i, r, c, X, Y: Integer;
begin
with TLabel.Create(Self) do
begin
Parent := FPagePA;
Caption := 'Power Amplifier';
SetBounds(MARGIN, 18, 400, 28);
Font.Name := UI_FONT;
Font.Size := 16;
Font.Style := [fsBold];
Font.Color := CLR_TEXT;
end;
with TLabel.Create(Self) do
begin
Parent := FPagePA;
Caption := 'Output limit and per-band calibration values.';
SetBounds(MARGIN, 48, 620, 20);
Font.Name := UI_FONT;
Font.Size := 9;
Font.Color := CLR_TEXTDIM;
end;
Grp := MakeGroupPanel(FPagePA, 'Power && Calibration', MARGIN, 86, 700, GRP_H);
// Max Power
Lbl := TLabel.Create(Self);
Lbl.Parent := Grp;
Lbl.Caption := 'Max power (W)';
Lbl.SetBounds(GRP_PAD, R1 + 6, 132, 18);
Lbl.Font.Color := CLR_TEXTDIM;
Lbl.Font.Name := UI_FONT;
Lbl.Font.Size := 9;
Spin := TFlatSpinEdit.Create(Self);
Spin.Parent := Grp;
Spin.SetBounds(GRP_PAD + 150, R1, ED_W, BTN_H + 2);
Spin.Color := CLR_INPUT;
Spin.Font.Color := CLR_INPUT_TEXT;
Spin.Font.Name := UI_FONT;
Spin.Font.Size := 9;
Spin.MinValue := 5;
Spin.MaxValue := 200;
Spin.Increment := 1;
Spin.Value := 100;
Spin.OnChange := OnMaxPowerChange;
FEdMaxPower := Spin;
// Pluto TX max attenuation @ drive 100% (потолок под внешний усилитель), дБ<0
Lbl := TLabel.Create(Self);
Lbl.Parent := Grp;
Lbl.Caption := 'Pluto TX att @100% (dB)';
Lbl.SetBounds(GRP_PAD + 320, R1 + 6, 180, 18);
Lbl.Font.Color := CLR_TEXTDIM;
Lbl.Font.Name := UI_FONT;
Lbl.Font.Size := 9;
FEdPlutoTxMaxAtt := TFlatFloatSpinEdit.Create(Self);
FEdPlutoTxMaxAtt.Parent := Grp;
FEdPlutoTxMaxAtt.SetBounds(GRP_PAD + 510, R1, ED_W, BTN_H + 2);
FEdPlutoTxMaxAtt.Color := CLR_INPUT;
FEdPlutoTxMaxAtt.Font.Color := CLR_INPUT_TEXT;
FEdPlutoTxMaxAtt.Font.Name := UI_FONT;
FEdPlutoTxMaxAtt.Font.Size := 9;
FEdPlutoTxMaxAtt.DecimalPlaces := 2;
FEdPlutoTxMaxAtt.MinValue := -50;
FEdPlutoTxMaxAtt.MaxValue := 0;
FEdPlutoTxMaxAtt.Increment := 0.25;
FEdPlutoTxMaxAtt.Value := -10;
FEdPlutoTxMaxAtt.OnChange := OnPlutoTxMaxAttChange;
// Calibration label
Lbl := TLabel.Create(Self);
Lbl.Parent := Grp;
Lbl.Caption := 'Calibration per band, 38.8 ... 100.0';
Lbl.SetBounds(GRP_PAD, R1 + ROW_H + 12, 320, 18);
Lbl.Font.Color := CLR_TEXTDIM;
Lbl.Font.Name := UI_FONT;
Lbl.Font.Size := 9;
// Band grid 4 columns × 3 rows
for i := 0 to 10 do
begin
r := i div COLS;
c := i mod COLS;
X := GRP_PAD + c * (COL_W + 4);
Y := R1 + ROW_H + 34 + r * ROW_H;
Lbl := TLabel.Create(Self);
Lbl.Parent := Grp;
Lbl.Caption := PA_BAND_NAMES[i] + ':';
Lbl.SetBounds(X, Y + 5, LBL_W, 18);
Lbl.Font.Color := CLR_TEXTDIM;
Lbl.Font.Name := UI_FONT;
Lbl.Font.Size := 9;
FEdBandCal[i] := MakeFloatSpin(Grp, X + LBL_W, Y, OnBandCalChange);
FEdBandCal[i].Tag := i;
end;
// ── VHF Calibration (Transverters) ──────────────────────────────────────
Grp := MakeGroupPanel(FPagePA, 'VHF Calibration (Transverters)',
MARGIN, 86 + GRP_H + 14, 700, R1 + 4 * ROW_H + 44);
Lbl := TLabel.Create(Self);
Lbl.Parent := Grp;
Lbl.Caption := 'Calibration per transverter slot, 38.8 ... 100.0';
Lbl.SetBounds(GRP_PAD, R1, 420, 18);
Lbl.Font.Color := CLR_TEXTDIM;
Lbl.Font.Name := UI_FONT;
Lbl.Font.Size := 9;
for i := 0 to CFG_XVTR_COUNT - 1 do
begin
r := i div VHF_COLS;
c := i mod VHF_COLS;
X := GRP_PAD + c * VHF_COL_W;
Y := R1 + 24 + r * ROW_H;
Lbl := TLabel.Create(Self);
Lbl.Parent := Grp;
Lbl.Caption := 'VHF' + IntToStr(i) + ':';
Lbl.AutoSize := False;
Lbl.SetBounds(X, Y + 5, VHF_LBL_W, 18);
Lbl.Font.Color := CLR_TEXTDIM;
Lbl.Font.Name := UI_FONT;
Lbl.Font.Size := 9;
Lbl.ShowHint := True;
FVHFCalLabels[i] := Lbl;
FEdVHFCal[i] := MakeFloatSpin(Grp, X + VHF_LBL_W + 8, Y, OnVHFCalEdit);
FEdVHFCal[i].Tag := i;
end;
end;
procedure TSettingsForm.BuildAdvancedTab;
const
MARGIN = 22;
GRP_PAD = 18;
LBL_W = 110;
ED_W = 200;
ROW_H = 42;
R1 = 42;
var
Grp: TPanel;
Chk: TFlatCheckBox;
Ed: TFlatEdit;
Spin: TFlatSpinEdit;
Y: Integer;
begin
with TLabel.Create(Self) do
begin
Parent := FPageAdvanced;
Caption := 'Advanced';
SetBounds(MARGIN, 18, 400, 28);
Font.Name := UI_FONT;
Font.Size := 16;
Font.Style := [fsBold];
Font.Color := CLR_TEXT;
end;
// ── ADC ───────────────────────────────────────────────────────────────────
Grp := MakeGroupPanel(FPageAdvanced, 'ADC', MARGIN, 86, 420, 128);
Chk := TFlatCheckBox.Create(Self);
Chk.Parent := Grp;
Chk.Caption := 'Dither Enabled';
Chk.SetBounds(GRP_PAD, R1, 260, 22);
Chk.Font.Name := UI_FONT;
Chk.Font.Size := 9;
Chk.Font.Color := CLR_TEXT;
Chk.Checked := True;
Chk.OnChange := OnADCChkChange;
FChkDither := Chk;
Chk := TFlatCheckBox.Create(Self);
Chk.Parent := Grp;
Chk.Caption := 'Random Enabled';
Chk.SetBounds(GRP_PAD, R1 + ROW_H, 260, 22);
Chk.Font.Name := UI_FONT;
Chk.Font.Size := 9;
Chk.Font.Color := CLR_TEXT;
Chk.Checked := True;
Chk.OnChange := OnADCChkChange;
FChkRandom := Chk;
// ── Web Server ────────────────────────────────────────────────────────────
Grp := MakeGroupPanel(FPageAdvanced, 'Web Server', MARGIN, 230, 500, 252);
Y := R1;
Chk := TFlatCheckBox.Create(Self);
Chk.Parent := Grp;
Chk.Caption := 'Enabled';
Chk.SetBounds(GRP_PAD, Y, 260, 22);
Chk.Font.Name := UI_FONT;
Chk.Font.Size := 9;
Chk.Font.Color := CLR_TEXT;
Chk.Checked := True;
Chk.OnChange := OnWebAnyChange;
FChkWebEnabled := Chk;
Y := Y + ROW_H;
MakeLbl(Grp, 'Port:', GRP_PAD, Y + 4, LBL_W);
Spin := TFlatSpinEdit.Create(Self);
Spin.Parent := Grp;
Spin.SetBounds(GRP_PAD + LBL_W + 12, Y, 90, BTN_H);
Spin.Color := CLR_INPUT;
Spin.Font.Color := CLR_INPUT_TEXT;
Spin.Font.Name := UI_FONT;
Spin.Font.Size := 9;
Spin.MinValue := 1;
Spin.MaxValue := 65535;
Spin.Value := 8080;
Spin.OnChange := OnWebAnyChange;
FEdWebPort := Spin;
Y := Y + ROW_H;
MakeLbl(Grp, 'Interface:', GRP_PAD, Y + 4, LBL_W);
Ed := TFlatEdit.Create(Self);
Ed.Parent := Grp;
Ed.SetBounds(GRP_PAD + LBL_W + 12, Y, ED_W, BTN_H);
Ed.Color := CLR_INPUT;
Ed.Font.Color := CLR_INPUT_TEXT;
Ed.Font.Name := UI_FONT;
Ed.Font.Size := 9;
Ed.Text := '0.0.0.0';
Ed.OnChange := OnWebAnyChange;
FEdWebBind := Ed;
Y := Y + ROW_H;
MakeLbl(Grp, 'Username:', GRP_PAD, Y + 4, LBL_W);
Ed := TFlatEdit.Create(Self);
Ed.Parent := Grp;
Ed.SetBounds(GRP_PAD + LBL_W + 12, Y, ED_W, BTN_H);
Ed.Color := CLR_INPUT;
Ed.Font.Color := CLR_INPUT_TEXT;
Ed.Font.Name := UI_FONT;
Ed.Font.Size := 9;
Ed.Text := 'admin';
Ed.OnChange := OnWebAnyChange;
FEdWebUser := Ed;
Y := Y + ROW_H;
MakeLbl(Grp, 'Password:', GRP_PAD, Y + 4, LBL_W);
Ed := TFlatEdit.Create(Self);
Ed.Parent := Grp;
Ed.SetBounds(GRP_PAD + LBL_W + 12, Y, ED_W, BTN_H);
Ed.Color := CLR_INPUT;
Ed.Font.Color := CLR_INPUT_TEXT;
Ed.Font.Name := UI_FONT;
Ed.Font.Size := 9;
Ed.PasswordChar := '*';
Ed.Text := 'hpsdr';
Ed.OnChange := OnWebAnyChange;
FEdWebPass := Ed;
end;
procedure TSettingsForm.OnADCChkChange(Sender: TObject);
begin
if FLoading then Exit;
if Assigned(FOnADCChange) then
FOnADCChange(FChkDither.Checked, FChkRandom.Checked);
end;
procedure TSettingsForm.OnWebAnyChange(Sender: TObject);
begin
if FLoading then Exit;
if Assigned(FOnWebSettingsChange) then
FOnWebSettingsChange(
FChkWebEnabled.Checked,
FEdWebPort.Value,
FEdWebBind.Text,
FEdWebUser.Text,
FEdWebPass.Text);
end;
procedure TSettingsForm.LoadADCSettings(Dither, Random: Boolean);
begin
FLoading := True;
try
FChkDither.Checked := Dither;
FChkRandom.Checked := Random;
finally
FLoading := False;
end;
end;
procedure TSettingsForm.LoadWebSettings(Enabled: Boolean; Port: Integer;
const BindAddr, User, Pass: string);
begin
FLoading := True;
try
FChkWebEnabled.Checked := Enabled;
FEdWebPort.Value := EnsureRange(Port, 1, 65535);
FEdWebBind.Text := BindAddr;
FEdWebUser.Text := User;
FEdWebPass.Text := Pass;
finally
FLoading := False;
end;
end;
procedure TSettingsForm.ApplyTheme(const T: TAppTheme);
procedure StyleBtn(B: TFlatButton);
begin
B.ClrNorm := T.Panel;
B.ClrBorder := T.Panel;
B.ClrHot := T.BG;
B.ClrActive := T.Panel;
B.ClrText := T.Text;
B.ClrTextAct := T.FreqActive;
B.Invalidate;
end;
procedure WalkControls(C: TWinControl);
var i: Integer; Ctrl: TControl;
begin
for i := 0 to C.ControlCount - 1 do
begin
Ctrl := C.Controls[i];
if Ctrl is TEqualizerControl then
TEqualizerControl(Ctrl).SetAppTheme(T)
else if Ctrl is TFlatButton then
StyleBtn(TFlatButton(Ctrl))
else if Ctrl is TScrollBox then
begin
TScrollBox(Ctrl).Color := T.BG;
WalkControls(TScrollBox(Ctrl));
end
else if Ctrl is TPanel then
begin
if TPanel(Ctrl).Height = 1 then
TPanel(Ctrl).Color := T.Border
else
TPanel(Ctrl).Color := T.Panel;
WalkControls(TPanel(Ctrl));
end
else if Ctrl is TLabel then
begin
if Ctrl.Font.Size >= 12 then
TLabel(Ctrl).Font.Color := T.Text
else if fsBold in Ctrl.Font.Style then
TLabel(Ctrl).Font.Color := T.FreqActive
else
TLabel(Ctrl).Font.Color := T.TextDim;
end
else if Ctrl is TFlatCheckBox then
TFlatCheckBox(Ctrl).SetAppTheme(T)
else if Ctrl is TFlatComboBox then
TFlatComboBox(Ctrl).SetAppTheme(T)
else if Ctrl is TFlatEdit then
TFlatEdit(Ctrl).SetAppTheme(T)
else if Ctrl is TFlatSpinEdit then
TFlatSpinEdit(Ctrl).SetAppTheme(T)
else if Ctrl is TFlatFloatSpinEdit then
TFlatFloatSpinEdit(Ctrl).SetAppTheme(T)
else if Ctrl is TFlatRadioButton then
TFlatRadioButton(Ctrl).SetAppTheme(T)
else if Ctrl is TListBox then
begin
TListBox(Ctrl).Color := T.BG;
TListBox(Ctrl).Font.Color := T.Text;
end
else if Ctrl is TWinControl then
WalkControls(TWinControl(Ctrl));
end;
end;
begin
Color := T.BG;
Font.Color := T.Text;
WalkControls(Self);
Invalidate;
end;
// ---------------------------------------------------------------------------
// LoadValues
// ---------------------------------------------------------------------------
procedure TSettingsForm.LoadValues(
FFTSize, WinType, SpecDet, SpecAvgMode: Integer;
SpecAvgTimeMS: Double;
WfDet, WfAvgMode: Integer;
WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double;
RefLevel, Range, GridStep: Double;
const AudioOutDevName: string;
const AudioInDevName: string);
var
i, Idx: Integer;
begin
FLoading := True;
try
Idx := 5;
for i := 0 to High(FFT_SIZES) do
if FFT_SIZES[i] = FFTSize then begin Idx := i; Break; end;
FCmbFFTSize.ItemIndex := Idx;
if (WinType >= 0) and (WinType < FCmbWindow.Items.Count) then
FCmbWindow.ItemIndex := WinType
else
FCmbWindow.ItemIndex := 2;
if (SpecDet >= 0) and (SpecDet < FCmbSpecDet.Items.Count) then
FCmbSpecDet.ItemIndex := SpecDet
else
FCmbSpecDet.ItemIndex := 0;
Idx := 3;
for i := 0 to High(AVGMODE_VALUES) do
if AVGMODE_VALUES[i] = SpecAvgMode then begin Idx := i; Break; end;
FCmbSpecAvg.ItemIndex := Idx;
FSpecAvgTime.Value := EnsureRange(SpecAvgTimeMS, 0.0, 5000.0);
if (WfDet >= 0) and (WfDet < FCmbWfDet.Items.Count) then
FCmbWfDet.ItemIndex := WfDet
else
FCmbWfDet.ItemIndex := 0;
Idx := 3;
for i := 0 to High(WF_AVGMODE_VALUES) do
if WF_AVGMODE_VALUES[i] = WfAvgMode then begin Idx := i; Break; end;
FCmbWfAvg.ItemIndex := Idx;
FWfAvgTime.Value := EnsureRange(WfAvgTimeMS, 0.0, 5000.0);
FWfHigh.Value := EnsureRange(WfHigh, -160.0, 20.0);
FWfLow.Value := EnsureRange(WfLow, -180.0, 0.0);
FWfAGCOffset.Value := EnsureRange(WfAGCOffset, -40.0, 40.0);
FEdRefLevel.Text := FormatFloat('0.#', RefLevel);
FEdRange.Text := FormatFloat('0.#', Range);
Idx := 1;
for i := 0 to High(GRID_STEPS) do
if Abs(GRID_STEPS[i] - GridStep) < 0.5 then begin Idx := i; Break; end;
FCmbGridStep.ItemIndex := Idx;
if AudioOutDevName = '' then
FCmbRXDev.ItemIndex := 0
else
begin
Idx := FCmbRXDev.Items.IndexOf(AudioOutDevName);
if Idx >= 0 then FCmbRXDev.ItemIndex := Idx
else FCmbRXDev.ItemIndex := 0;
end;
if AudioInDevName = '' then
FCmbTXDev.ItemIndex := 0
else
begin
Idx := FCmbTXDev.Items.IndexOf(AudioInDevName);
if Idx >= 0 then FCmbTXDev.ItemIndex := Idx
else FCmbTXDev.ItemIndex := 0;
end;
finally
FLoading := False;
end;
end;
// ---------------------------------------------------------------------------
// LoadAudioBufferSize
// ---------------------------------------------------------------------------
procedure TSettingsForm.LoadAudioBufferSize(BufferSize: Integer);
begin
FLoading := True;
try
case BufferSize of
256: FCmbAudioBuffer.ItemIndex := 1;
512: FCmbAudioBuffer.ItemIndex := 2;
else
FCmbAudioBuffer.ItemIndex := 0;
end;
finally
FLoading := False;
end;
end;
procedure TSettingsForm.LoadSendAudioToRadio(Enabled: Boolean);
begin
FLoading := True;
try
FChkSendAudioToRadio.Checked := Enabled;
finally
FLoading := False;
end;
end;
// ---------------------------------------------------------------------------
// RefreshAudioDevices
// ---------------------------------------------------------------------------
procedure TSettingsForm.RefreshAudioDevices(AudioOut: TAudioOutput; AudioIn: TAudioInput);
var
Names: TStringList;
Cnt, i: Integer;
OldRXSel: string;
OldTXSel: string;
begin
Names := TStringList.Create;
FLoading := True;
try
if AudioOut <> nil then
begin
OldRXSel := FCmbRXDev.Text;
FCmbRXDev.Items.Clear;
FCmbRXDev.Items.Add('Default');
FRXDevCount := 0;
Cnt := 0;
Names.Clear;
if AudioOut.EnumOutputDevices(Names, FRXDevIndices, Cnt) then
begin
FRXDevCount := Cnt;
for i := 0 to Cnt - 1 do
FCmbRXDev.Items.Add(Names[i]);
end;
if (FRXDevCount = 0) and (AudioOut.LastError <> '') then
FCmbRXDev.Items.Add('PortAudio: ' + AudioOut.LastError);
i := FCmbRXDev.Items.IndexOf(OldRXSel);
if i >= 0 then FCmbRXDev.ItemIndex := i
else FCmbRXDev.ItemIndex := 0;
end;
if AudioIn <> nil then
begin
OldTXSel := FCmbTXDev.Text;
FCmbTXDev.Items.Clear;
FCmbTXDev.Items.Add('None');
FTXDevCount := 0;
Cnt := 0;
Names.Clear;
if AudioIn.EnumInputDevices(Names, FTXDevIndices, Cnt) then
begin
FTXDevCount := Cnt;
for i := 0 to Cnt - 1 do
FCmbTXDev.Items.Add(Names[i]);
end;
if (FTXDevCount = 0) and (AudioIn.LastError <> '') then
FCmbTXDev.Items.Add('PortAudio: ' + AudioIn.LastError);
i := FCmbTXDev.Items.IndexOf(OldTXSel);
if i >= 0 then FCmbTXDev.ItemIndex := i
else FCmbTXDev.ItemIndex := 0;
end;
finally
FLoading := False;
Names.Free;
end;
end;
// ---------------------------------------------------------------------------
// Value accessors
// ---------------------------------------------------------------------------
function TSettingsForm.GetFFTSize: Integer;
var Idx: Integer;
begin
Idx := FCmbFFTSize.ItemIndex;
if (Idx >= 0) and (Idx <= High(FFT_SIZES)) then
Result := FFT_SIZES[Idx]
else
Result := 4096;
end;
function TSettingsForm.GetSpecAvgTimeMS: Double;
begin
Result := EnsureRange(FSpecAvgTime.Value, 0.0, 5000.0);
end;
function TSettingsForm.GetWfAvgTimeMS: Double;
begin
Result := EnsureRange(FWfAvgTime.Value, 0.0, 5000.0);
end;
function TSettingsForm.GetWfHigh: Double;
begin
Result := EnsureRange(FWfHigh.Value, -160.0, 20.0);
end;
function TSettingsForm.GetWfLow: Double;
begin
Result := EnsureRange(FWfLow.Value, -180.0, 0.0);
end;
function TSettingsForm.GetWfAGCOffset: Double;
begin
Result := EnsureRange(FWfAGCOffset.Value, -40.0, 40.0);
end;
function TSettingsForm.GetRefLevel: Double;
var V: Double; Err: Integer;
begin
Val(FEdRefLevel.Text, V, Err);
if Err <> 0 then V := -20.0;
Result := EnsureRange(V, -200.0, 0.0);
end;
function TSettingsForm.GetRange: Double;
var V: Double; Err: Integer;
begin
Val(FEdRange.Text, V, Err);
if Err <> 0 then V := 110.0;
Result := EnsureRange(V, 10.0, 200.0);
end;
function TSettingsForm.GetGridStep: Double;
var Idx: Integer;
begin
Idx := FCmbGridStep.ItemIndex;
if (Idx >= 0) and (Idx <= High(GRID_STEPS)) then
Result := GRID_STEPS[Idx]
else
Result := 10.0;
end;
// ---------------------------------------------------------------------------
// Fire helpers
// ---------------------------------------------------------------------------
procedure TSettingsForm.FireDisplayChange;
var
AvgIdx: Integer;
AvgVal: Integer;
begin
if FLoading then Exit;
if not Assigned(FOnDisplayChange) then Exit;
AvgIdx := FCmbSpecAvg.ItemIndex;
if (AvgIdx >= 0) and (AvgIdx <= High(AVGMODE_VALUES)) then
AvgVal := AVGMODE_VALUES[AvgIdx]
else
AvgVal := 3;
FOnDisplayChange(
GetFFTSize,
FCmbWindow.ItemIndex,
FCmbSpecDet.ItemIndex,
AvgVal,
GetSpecAvgTimeMS);
end;
procedure TSettingsForm.FireWaterfallChange;
var
AvgIdx: Integer;
AvgVal: Integer;
begin
if FLoading then Exit;
if not Assigned(FOnWaterfallChange) then Exit;
AvgIdx := FCmbWfAvg.ItemIndex;
if (AvgIdx >= 0) and (AvgIdx <= High(WF_AVGMODE_VALUES)) then
AvgVal := WF_AVGMODE_VALUES[AvgIdx]
else
AvgVal := 1;
FOnWaterfallChange(
FCmbWfDet.ItemIndex,
AvgVal,
GetWfAvgTimeMS,
GetWfHigh,
GetWfLow,
GetWfAGCOffset);
end;
procedure TSettingsForm.FireWfRenderChange;
var
ResV, SpeedV: Integer;
begin
if FLoading then Exit;
if not Assigned(FOnWfRenderChange) then Exit;
if (FCmbWfRes.ItemIndex >= 0) and (FCmbWfRes.ItemIndex <= High(DISPLAY_RES_VALUES)) then
ResV := DISPLAY_RES_VALUES[FCmbWfRes.ItemIndex]
else
ResV := DISPLAY_RES_VALUES[High(DISPLAY_RES_VALUES)];
if (FCmbWfSpeed.ItemIndex >= 0) and (FCmbWfSpeed.ItemIndex <= High(WF_SPEED_VALUES)) then
SpeedV := WF_SPEED_VALUES[FCmbWfSpeed.ItemIndex]
else
SpeedV := 1;
FOnWfRenderChange(Max(0, FCmbWfPalette.ItemIndex), FWfGamma.Value, ResV, SpeedV);
end;
procedure TSettingsForm.OnWfRenderCtlChange(Sender: TObject);
begin FireWfRenderChange; end;
procedure TSettingsForm.OnSpecMSAACtlChange(Sender: TObject);
begin
if FLoading then Exit;
if not Assigned(FOnSpecMSAAChange) then Exit;
if (FCmbSpecMSAA.ItemIndex >= 0) and (FCmbSpecMSAA.ItemIndex <= High(MSAA_VALUES)) then
FOnSpecMSAAChange(MSAA_VALUES[FCmbSpecMSAA.ItemIndex]);
end;
procedure TSettingsForm.LoadWfRender(Palette: Integer; Gamma: Double;
ResLimit, SpeedDiv: Integer);
var i: Integer;
begin
FLoading := True;
try
if (Palette >= 0) and (Palette < FCmbWfPalette.Items.Count) then
FCmbWfPalette.ItemIndex := Palette
else
FCmbWfPalette.ItemIndex := 0;
FWfGamma.Value := EnsureRange(Gamma, 0.3, 3.0);
FCmbWfRes.ItemIndex := High(DISPLAY_RES_VALUES);
for i := 0 to High(DISPLAY_RES_VALUES) do
if DISPLAY_RES_VALUES[i] = ResLimit then
begin FCmbWfRes.ItemIndex := i; Break; end;
FCmbWfSpeed.ItemIndex := 0;
for i := 0 to High(WF_SPEED_VALUES) do
if WF_SPEED_VALUES[i] = SpeedDiv then
begin FCmbWfSpeed.ItemIndex := i; Break; end;
finally
FLoading := False;
end;
end;
procedure TSettingsForm.LoadSpecMSAA(Samples: Integer);
var i: Integer;
begin
FLoading := True;
try
FCmbSpecMSAA.ItemIndex := 0;
for i := 0 to High(MSAA_VALUES) do
if MSAA_VALUES[i] = Samples then
begin FCmbSpecMSAA.ItemIndex := i; Break; end;
finally
FLoading := False;
end;
end;
procedure TSettingsForm.FireGridChange;
begin
if FLoading then Exit;
if not Assigned(FOnGridChange) then Exit;
FOnGridChange(GetRefLevel, GetRange, GetGridStep);
end;
// ---------------------------------------------------------------------------
// Event handlers
// ---------------------------------------------------------------------------
procedure TSettingsForm.OnFFTSizeChange(Sender: TObject);
begin FireDisplayChange; end;
procedure TSettingsForm.OnWindowChange(Sender: TObject);
begin FireDisplayChange; end;
procedure TSettingsForm.OnSpecDetChange(Sender: TObject);
begin FireDisplayChange; end;
procedure TSettingsForm.OnSpecAvgModeChange(Sender: TObject);
begin FireDisplayChange; end;
procedure TSettingsForm.OnSpecAvgTimeChange(Sender: TObject);
begin FireDisplayChange; end;
procedure TSettingsForm.OnWfDetChange(Sender: TObject);
begin FireWaterfallChange; end;
procedure TSettingsForm.OnWfAvgModeChange(Sender: TObject);
begin FireWaterfallChange; end;
procedure TSettingsForm.OnWfAvgTimeChange(Sender: TObject);
begin FireWaterfallChange; end;
procedure TSettingsForm.OnWfThresholdChange(Sender: TObject);
begin FireWaterfallChange; end;
procedure TSettingsForm.OnWfAGCNFChkChange(Sender: TObject);
begin
if FLoading then Exit;
if not Assigned(FOnWfAGCNFChange) then Exit;
FOnWfAGCNFChange(FChkWfAGC.Checked, FChkWfNF.Checked);
end;
procedure TSettingsForm.OnRefLevelChange(Sender: TObject);
begin FireGridChange; end;
procedure TSettingsForm.OnRangeChange(Sender: TObject);
begin FireGridChange; end;
procedure TSettingsForm.OnGridStepChange(Sender: TObject);
begin FireGridChange; end;
procedure TSettingsForm.OnRXDevChange(Sender: TObject);
var
Idx: Integer;
DevIdx: Integer;
DevName: string;
begin
if FLoading then Exit;
if not Assigned(FOnAudioDevChange) then Exit;
Idx := FCmbRXDev.ItemIndex;
if Idx <= 0 then
FOnAudioDevChange(-1, '')
else
begin
DevName := FCmbRXDev.Items[Idx];
DevIdx := Idx - 1;
if (DevIdx >= 0) and (DevIdx < FRXDevCount) then
FOnAudioDevChange(FRXDevIndices[DevIdx], DevName)
else
FOnAudioDevChange(-1, '');
end;
end;
procedure TSettingsForm.OnTXDevChange(Sender: TObject);
var
Idx: Integer;
DevIdx: Integer;
DevName: string;
begin
if FLoading then Exit;
if not Assigned(FOnAudioInDevChange) then Exit;
Idx := FCmbTXDev.ItemIndex;
if Idx <= 0 then
FOnAudioInDevChange(-1, '')
else
begin
DevName := FCmbTXDev.Items[Idx];
DevIdx := Idx - 1;
if (DevIdx >= 0) and (DevIdx < FTXDevCount) then
FOnAudioInDevChange(FTXDevIndices[DevIdx], DevName)
else
FOnAudioInDevChange(-1, '');
end;
end;
procedure TSettingsForm.OnAudioBufferCmbChange(Sender: TObject);
var
BufferSize: Integer;
begin
if FLoading then Exit;
if not Assigned(FOnAudioBufferChange) then Exit;
case FCmbAudioBuffer.ItemIndex of
1: BufferSize := 256;
2: BufferSize := 512;
else
BufferSize := 128;
end;
FOnAudioBufferChange(BufferSize);
end;
procedure TSettingsForm.OnSendAudioToRadioChkChange(Sender: TObject);
begin
if FLoading then Exit;
if Assigned(FOnSendAudioToRadioChange) then
FOnSendAudioToRadioChange(FChkSendAudioToRadio.Checked);
end;
procedure TSettingsForm.LoadVisibility(ShowSpectrum, ShowWaterfall,
ShowWideband, WidebandFill, SpectrumFill: Boolean);
begin
FLoading := True;
try
FChkShowSpectrum.Checked := ShowSpectrum;
FChkShowWaterfall.Checked := ShowWaterfall;
FChkShowWideband.Checked := ShowWideband;
FChkWidebandFill.Checked := WidebandFill;
FChkSpectrumFill.Checked := SpectrumFill;
finally
FLoading := False;
end;
end;
procedure TSettingsForm.LoadWfAGCNF(AGC, NF: Boolean);
begin
FLoading := True;
try
FChkWfAGC.Checked := AGC;
FChkWfNF.Checked := NF;
finally
FLoading := False;
end;
end;
procedure TSettingsForm.OnVisibilityChkChange(Sender: TObject);
begin
if FLoading then Exit;
if not Assigned(FOnVisibilityChange) then Exit;
FOnVisibilityChange(FChkShowSpectrum.Checked, FChkShowWaterfall.Checked,
FChkShowWideband.Checked, FChkWidebandFill.Checked, FChkSpectrumFill.Checked);
end;
procedure TSettingsForm.OnLightThemeChkChange(Sender: TObject);
begin
if FLoading then Exit;
if FChkLightTheme.Checked then ApplyTheme(LightTheme)
else ApplyTheme(DarkTheme);
if Assigned(FOnThemeChange) then
FOnThemeChange(FChkLightTheme.Checked);
end;
procedure TSettingsForm.OnFPSCmbChange(Sender: TObject);
var
Idx: Integer;
begin
if FLoading then Exit;
if not Assigned(FOnFPSChange) then Exit;
Idx := FCmbFPS.ItemIndex;
if (Idx >= 0) and (Idx <= High(FPS_VALUES)) then
FOnFPSChange(FPS_VALUES[Idx]);
end;
procedure TSettingsForm.LoadFPS(FPS: Integer);
var
i, Idx: Integer;
begin
FLoading := True;
try
Idx := 8;
for i := 0 to High(FPS_VALUES) do
if FPS_VALUES[i] = FPS then begin Idx := i; Break; end;
FCmbFPS.ItemIndex := Idx;
finally
FLoading := False;
end;
end;
procedure TSettingsForm.LoadLightTheme(ALight: Boolean);
begin
FLoading := True;
try
FChkLightTheme.Checked := ALight;
finally
FLoading := False;
end;
if ALight then ApplyTheme(LightTheme)
else ApplyTheme(DarkTheme);
end;
procedure TSettingsForm.LoadFreqMhzDigits(Digits: Integer);
begin
FLoading := True;
try
FCmbFreqMhzDigits.ItemIndex := EnsureRange(Digits - 3, 0, 2);
finally
FLoading := False;
end;
end;
procedure TSettingsForm.OnFreqMhzDigitsCmbChange(Sender: TObject);
begin
if FLoading then Exit;
if Assigned(FOnFreqMhzDigitsChange) then
FOnFreqMhzDigitsChange(FCmbFreqMhzDigits.ItemIndex + 3);
end;
procedure TSettingsForm.LoadPASettings(MaxPower: Double;
const BandCal: array of Double);
var i, n: Integer;
begin
FLoading := True;
try
FEdMaxPower.Value := Round(EnsureRange(MaxPower, 5.0, 200.0));
n := Min(10, High(BandCal));
for i := 0 to n do
FEdBandCal[i].Value := EnsureRange(BandCal[i], 38.8, 100.0);
finally
FLoading := False;
end;
end;
procedure TSettingsForm.LoadPlutoTxMaxAtt(MaxAttDb: Double);
begin
FLoading := True;
try
FEdPlutoTxMaxAtt.Value := EnsureRange(MaxAttDb, -50.0, 0.0);
finally
FLoading := False;
end;
end;
procedure TSettingsForm.OnMaxPowerChange(Sender: TObject);
var
BandCal: array[0..10] of Double;
i: Integer;
begin
if FLoading then Exit;
if not Assigned(FOnPAChange) then Exit;
for i := 0 to 10 do
BandCal[i] := FEdBandCal[i].Value;
FOnPAChange(FEdMaxPower.Value, BandCal);
end;
procedure TSettingsForm.OnPlutoTxMaxAttChange(Sender: TObject);
begin
if FLoading then Exit;
if Assigned(FOnPlutoTxChange) then FOnPlutoTxChange(FEdPlutoTxMaxAtt.Value);
end;
procedure TSettingsForm.OnBandCalChange(Sender: TObject);
var
BandCal: array[0..10] of Double;
i: Integer;
begin
if FLoading then Exit;
if not Assigned(FOnPAChange) then Exit;
for i := 0 to 10 do
BandCal[i] := FEdBandCal[i].Value;
FOnPAChange(FEdMaxPower.Value, BandCal);
end;
procedure TSettingsForm.LoadVHFCalSettings(const VHFCal: array of Double);
var i, n: Integer;
begin
FLoading := True;
try
n := Min(CFG_XVTR_COUNT - 1, High(VHFCal));
for i := 0 to n do
FEdVHFCal[i].Value := EnsureRange(VHFCal[i], 38.8, 100.0);
finally
FLoading := False;
end;
end;
procedure TSettingsForm.OnVHFCalEdit(Sender: TObject);
var
VHFCal: array[0..CFG_XVTR_COUNT-1] of Double;
i: Integer;
begin
if FLoading then Exit;
if not Assigned(FOnVHFCalChange) then Exit;
for i := 0 to CFG_XVTR_COUNT - 1 do
VHFCal[i] := FEdVHFCal[i].Value;
FOnVHFCalChange(VHFCal);
end;
procedure TSettingsForm.BtnCloseClick(Sender: TObject);
begin
Close;
end;
// ---------------------------------------------------------------------------
// BuildCATTab
// ---------------------------------------------------------------------------
procedure TSettingsForm.BuildCATTab;
const
MARGIN = 22;
PAD = 18;
LW = 120;
CX = PAD + LW + 8;
GRP_W = 320;
GRP_H = 272;
GRP_GAP = 16;
CMB_W = 140;
EDT_W = 160;
R1 = 42;
STEP = 32;
var
i, col, row, gx, gy, j: Integer;
Grp: TPanel;
Chk: TFlatCheckBox;
Ed: TFlatEdit;
Cmb: TFlatComboBox;
Spin: TFlatSpinEdit;
begin
with TLabel.Create(Self) do
begin
Parent := FPageCAT;
Caption := 'CAT';
SetBounds(MARGIN, 18, 400, 28);
Font.Name := UI_FONT; Font.Size := 16;
Font.Style := [fsBold]; Font.Color := CLR_TEXT;
end;
with TLabel.Create(Self) do
begin
Parent := FPageCAT;
Caption := 'Computer Aided Transceiver — serial port and TCP server settings.';
SetBounds(MARGIN, 48, 680, 20);
Font.Name := UI_FONT; Font.Size := 9; Font.Color := CLR_TEXTDIM;
end;
for i := 0 to 3 do
begin
row := i div 2;
col := i mod 2;
gx := MARGIN + col * (GRP_W + GRP_GAP);
gy := 86 + row * (GRP_H + GRP_GAP);
Grp := MakeGroupPanel(FPageCAT, 'Serial Port ' + IntToStr(i + 1), gx, gy, GRP_W, GRP_H);
Chk := TFlatCheckBox.Create(Self);
Chk.Parent := Grp;
Chk.Caption := 'Enable';
Chk.SetBounds(PAD, R1, GRP_W - PAD * 2, 22);
Chk.Font.Color := CLR_TEXT;
Chk.Font.Name := UI_FONT; Chk.Font.Size := 9;
Chk.Tag := i;
Chk.OnChange := OnCATSerialChange;
FCATSerialEn[i] := Chk;
MakeLbl(Grp, 'Port:', PAD, R1 + STEP + 6, LW);
Ed := TFlatEdit.Create(Self);
Ed.Parent := Grp;
Ed.SetBounds(CX, R1 + STEP, EDT_W, BTN_H);
Ed.Color := CLR_INPUT; Ed.Font.Color := CLR_INPUT_TEXT;
Ed.Font.Name := UI_FONT; Ed.Font.Size := 9;
Ed.Tag := i;
Ed.OnChange := OnCATSerialChange;
{$IFDEF MSWINDOWS}
Ed.Text := 'COM' + IntToStr(i + 1);
{$ELSE}
Ed.Text := '/dev/ttyS' + IntToStr(i);
{$ENDIF}
FCATSerialPort[i] := Ed;
MakeLbl(Grp, 'Baud rate:', PAD, R1 + 2 * STEP + 6, LW);
Cmb := MakeCombo(Grp, CX, R1 + 2 * STEP, CMB_W, OnCATSerialChange);
Cmb.Tag := i;
for j := 0 to High(CAT_BAUD_NAMES) do Cmb.Items.Add(CAT_BAUD_NAMES[j]);
Cmb.ItemIndex := 3; // 9600
FCATSerialBaud[i] := Cmb;
MakeLbl(Grp, 'Data bits:', PAD, R1 + 3 * STEP + 6, LW);
Cmb := MakeCombo(Grp, CX, R1 + 3 * STEP, CMB_W, OnCATSerialChange);
Cmb.Tag := i;
Cmb.Items.Add('7'); Cmb.Items.Add('8');
Cmb.ItemIndex := 1; // 8
FCATSerialDataBits[i] := Cmb;
MakeLbl(Grp, 'Stop bits:', PAD, R1 + 4 * STEP + 6, LW);
Cmb := MakeCombo(Grp, CX, R1 + 4 * STEP, CMB_W, OnCATSerialChange);
Cmb.Tag := i;
Cmb.Items.Add('1'); Cmb.Items.Add('2');
Cmb.ItemIndex := 0; // 1
FCATSerialStopBits[i] := Cmb;
MakeLbl(Grp, 'Parity:', PAD, R1 + 5 * STEP + 6, LW);
Cmb := MakeCombo(Grp, CX, R1 + 5 * STEP, CMB_W, OnCATSerialChange);
Cmb.Tag := i;
Cmb.Items.Add('None'); Cmb.Items.Add('Odd'); Cmb.Items.Add('Even');
Cmb.ItemIndex := 0; // None
FCATSerialParity[i] := Cmb;
Chk := TFlatCheckBox.Create(Self);
Chk.Parent := Grp;
Chk.Caption := 'Andromeda / G2 front panel';
Chk.SetBounds(PAD, R1 + 6 * STEP, GRP_W - PAD * 2, 22);
Chk.Font.Color := CLR_TEXT;
Chk.Font.Name := UI_FONT; Chk.Font.Size := 9;
Chk.Tag := i;
Chk.OnChange := OnCATSerialChange;
FCATSerialAndromeda[i] := Chk;
end;
// TCP server group below the two rows of serial port panels
gy := 86 + 2 * (GRP_H + GRP_GAP);
Grp := MakeGroupPanel(FPageCAT, 'TCP CAT Server', MARGIN, gy, GRP_W * 2 + GRP_GAP, 116);
FCATTcpEn := TFlatCheckBox.Create(Self);
FCATTcpEn.Parent := Grp;
FCATTcpEn.Caption := 'Enable TCP server (default port 19090)';
FCATTcpEn.SetBounds(PAD, R1, 300, 22);
FCATTcpEn.Font.Color := CLR_TEXT;
FCATTcpEn.Font.Name := UI_FONT; FCATTcpEn.Font.Size := 9;
FCATTcpEn.OnChange := OnCATTcpChange;
MakeLbl(Grp, 'Port:', PAD, R1 + STEP + 6, LW);
Spin := TFlatSpinEdit.Create(Self);
Spin.Parent := Grp;
Spin.SetBounds(CX, R1 + STEP, 100, BTN_H + 2);
Spin.Color := CLR_INPUT; Spin.Font.Color := CLR_INPUT_TEXT;
Spin.Font.Name := UI_FONT; Spin.Font.Size := 9;
Spin.MinValue := 1; Spin.MaxValue := 65535;
Spin.Value := 19090;
Spin.OnChange := OnCATTcpChange;
FCATTcpPort := Spin;
end;
// ---------------------------------------------------------------------------
// CAT event handlers
// ---------------------------------------------------------------------------
procedure TSettingsForm.FireCATChange;
var
SerEn: array[0..3] of Boolean;
SerPort: array[0..3] of string;
SerBaud, SerData, SerStop, SerPar: array[0..3] of Integer;
SerAndr: array[0..3] of Boolean;
i, Idx: Integer;
begin
if FLoading then Exit;
if not Assigned(FOnCATChange) then Exit;
for i := 0 to 3 do
begin
SerEn[i] := FCATSerialEn[i].Checked;
SerPort[i] := FCATSerialPort[i].Text;
Idx := FCATSerialBaud[i].ItemIndex;
if (Idx >= 0) and (Idx <= High(CAT_BAUD_VALS)) then
SerBaud[i] := CAT_BAUD_VALS[Idx]
else
SerBaud[i] := 9600;
if FCATSerialDataBits[i].ItemIndex = 0 then SerData[i] := 7 else SerData[i] := 8;
if FCATSerialStopBits[i].ItemIndex = 1 then SerStop[i] := 2 else SerStop[i] := 1;
SerPar[i] := FCATSerialParity[i].ItemIndex;
SerAndr[i] := FCATSerialAndromeda[i].Checked;
end;
FOnCATChange(SerEn, SerPort, SerBaud, SerData, SerStop, SerPar, SerAndr,
FCATTcpEn.Checked, FCATTcpPort.Value);
end;
procedure TSettingsForm.OnCATSerialChange(Sender: TObject);
begin FireCATChange; end;
procedure TSettingsForm.OnCATTcpChange(Sender: TObject);
begin FireCATChange; end;
// ---------------------------------------------------------------------------
// LoadCATSettings
// ---------------------------------------------------------------------------
procedure TSettingsForm.LoadCATSettings(
const SerEnabled: array of Boolean;
const SerPort: array of string;
const SerBaud, SerDataBits, SerStopBits, SerParity: array of Integer;
const SerAndromeda: array of Boolean;
TcpEnabled: Boolean; TcpPort: Integer);
var
i, j, Idx: Integer;
begin
FLoading := True;
try
for i := 0 to 3 do
begin
if i <= High(SerEnabled) then FCATSerialEn[i].Checked := SerEnabled[i];
if i <= High(SerPort) then FCATSerialPort[i].Text := SerPort[i];
if i <= High(SerBaud) then
begin
Idx := 3;
for j := 0 to High(CAT_BAUD_VALS) do
if CAT_BAUD_VALS[j] = SerBaud[i] then begin Idx := j; Break; end;
FCATSerialBaud[i].ItemIndex := Idx;
end;
if i <= High(SerDataBits) then
begin
if SerDataBits[i] = 7 then FCATSerialDataBits[i].ItemIndex := 0
else FCATSerialDataBits[i].ItemIndex := 1;
end;
if i <= High(SerStopBits) then
begin
if SerStopBits[i] = 2 then FCATSerialStopBits[i].ItemIndex := 1
else FCATSerialStopBits[i].ItemIndex := 0;
end;
if i <= High(SerParity) then
FCATSerialParity[i].ItemIndex := EnsureRange(SerParity[i], 0, 2);
if i <= High(SerAndromeda) then
FCATSerialAndromeda[i].Checked := SerAndromeda[i];
end;
FCATTcpEn.Checked := TcpEnabled;
FCATTcpPort.Value := EnsureRange(TcpPort, 1, 65535);
finally
FLoading := False;
end;
end;
// ---------------------------------------------------------------------------
// BuildAlexTab
// ---------------------------------------------------------------------------
procedure TSettingsForm.BuildAlexTab;
const
MARGIN = 22;
PAD = 12;
HDR1_H = 22; // height of "RX Ant / TX Ant" header row
HDR2_H = 20; // height of "1 2 3 / BYPS EXT1 XVTR" sub-header row
ROW_H = 24;
BANDS = 11;
BAND_X = PAD;
RX1_X = PAD + 50;
RX2_X = PAD + 76;
RX3_X = PAD + 102;
BYPS_X = PAD + 136;
EXT1_X = PAD + 192;
XVTR_X = PAD + 248;
TX1_X = PAD + 308;
TX2_X = PAD + 334;
TX3_X = PAD + 360;
DNTX_X = PAD + 398;
GRP_W = DNTX_X + 80;
// Таблица разъёмов AD936x (Pluto/LibreSDR): диапазон | RX 1/2 | TX 1/2.
ANT_ROW_H = 26;
ANT_HDR_H = 34; // строка «RX / TX» + подстрока «1 2» под ней
ANT_RX1_X = PAD + 96;
ANT_RX2_X = ANT_RX1_X + 34;
ANT_TX1_X = ANT_RX2_X + 52;
ANT_TX2_X = ANT_TX1_X + 34;
ANT_GRP_W = ANT_TX2_X + 42;
GRP_H = GRP_TITLE_H + HDR1_H + HDR2_H + BANDS * ROW_H + PAD;
DATA_Y0 = GRP_TITLE_H + HDR1_H + HDR2_H + 4;
TXGRP_H = GRP_TITLE_H + 4 * 28 + PAD;
BAND_NAMES: array[0..10] of string =
('160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m');
var
Grp, GrpTX: TPanel;
b: Integer;
Y: Integer;
Chk: TFlatCheckBox;
RxGrp, TxGrp: TPanel;
// In Lazarus, TRadioButton has no GroupIndex; buttons are grouped by parent.
// We use small invisible panels as parents so each band's RX and TX groups
// are independent of each other.
function MkRbGroup(AParent: TWinControl; AX, AY, AW: Integer): TPanel;
begin
Result := TPanel.Create(Self);
Result.Parent := AParent;
Result.SetBounds(AX, AY, AW, 20);
Result.BevelOuter := bvNone;
Result.Color := CLR_PANEL;
Result.Caption := '';
end;
function MkRb(AParent: TWinControl; AX: Integer): TFlatRadioButton;
begin
Result := TFlatRadioButton.Create(Self);
Result.Parent := AParent;
Result.Caption := '';
Result.SetBounds(AX, 0, 22, 20);
Result.Font.Color := CLR_TEXT;
Result.Font.Name := UI_FONT;
Result.Font.Size := 9;
Result.OnChange := OnAlexAnyChange;
end;
function MkCh(AParent: TWinControl; const ACap: string;
AX, AY, AW: Integer): TFlatCheckBox;
begin
Result := TFlatCheckBox.Create(Self);
Result.Parent := AParent;
Result.Caption := ACap;
Result.SetBounds(AX, AY, AW, 20);
Result.Font.Color := CLR_TEXT;
Result.Font.Name := UI_FONT;
Result.Font.Size := 9;
Result.OnChange := OnAlexAnyChange;
end;
// ---- Таблица разъёмов AD936x ----
function MkAntRb(AParent: TWinControl; AX: Integer): TFlatRadioButton;
begin
Result := TFlatRadioButton.Create(Self);
Result.Parent := AParent;
Result.Caption := '';
Result.SetBounds(AX, 0, 22, 20);
Result.Font.Color := CLR_TEXT;
Result.Font.Name := UI_FONT;
Result.Font.Size := 9;
Result.OnChange := Ant936xUIChange;
end;
function MkAntLbl(AParent: TWinControl; const ACap: string;
AX, AY: Integer): TLabel;
begin
Result := TLabel.Create(Self);
Result.Parent := AParent;
Result.Caption := ACap;
Result.SetBounds(AX, AY, 84, 18);
Result.Font.Name := UI_FONT;
Result.Font.Size := 9;
Result.Font.Color := CLR_TEXT;
end;
procedure MkAntHdr(AParent: TWinControl; const ACap: string; AX, AY: Integer);
begin
with TLabel.Create(Self) do
begin
Parent := AParent;
Caption := ACap;
SetBounds(AX, AY, 30, 16);
Font.Name := UI_FONT;
Font.Size := 9;
Font.Color := CLR_TEXTDIM;
end;
end;
begin
FLoading := True;
try
with TLabel.Create(Self) do
begin
Parent := FPageAlex;
Caption := 'Antenna / Alex';
SetBounds(MARGIN, 18, 500, 28);
Font.Name := UI_FONT; Font.Size := 16;
Font.Style := [fsBold]; Font.Color := CLR_TEXT;
end;
FAlexSubtitle := TLabel.Create(Self);
FAlexSubtitle.Parent := FPageAlex;
FAlexSubtitle.Caption :=
'Per-band RX/TX antenna and secondary path routing for the Alex filter board.';
FAlexSubtitle.SetBounds(MARGIN, 48, 680, 20);
FAlexSubtitle.Font.Name := UI_FONT;
FAlexSubtitle.Font.Size := 9;
FAlexSubtitle.Font.Color := CLR_TEXTDIM;
// ---- Antenna grid group ----
Grp := MakeGroupPanel(FPageAlex, 'Antenna Selection', MARGIN, 86, GRP_W, GRP_H);
// Header row 1 — group labels
MakeLbl(Grp, 'Band', BAND_X, GRP_TITLE_H + 2, 44);
MakeLbl(Grp, 'RX Antenna', RX1_X, GRP_TITLE_H + 2, 80);
MakeLbl(Grp, 'Secondary RX', BYPS_X, GRP_TITLE_H + 2, 170);
MakeLbl(Grp, 'TX Antenna', TX1_X, GRP_TITLE_H + 2, 80);
MakeLbl(Grp, 'No TX', DNTX_X, GRP_TITLE_H + 2, 56);
// Header row 2 — sub-column labels
MakeLbl(Grp, '1', RX1_X + 3, GRP_TITLE_H + HDR1_H + 2, 20);
MakeLbl(Grp, '2', RX2_X + 3, GRP_TITLE_H + HDR1_H + 2, 20);
MakeLbl(Grp, '3', RX3_X + 3, GRP_TITLE_H + HDR1_H + 2, 20);
MakeLbl(Grp, 'BYPS', BYPS_X, GRP_TITLE_H + HDR1_H + 2, 52);
MakeLbl(Grp, 'EXT1', EXT1_X, GRP_TITLE_H + HDR1_H + 2, 52);
MakeLbl(Grp, 'XVTR', XVTR_X, GRP_TITLE_H + HDR1_H + 2, 52);
MakeLbl(Grp, '1', TX1_X + 3, GRP_TITLE_H + HDR1_H + 2, 20);
MakeLbl(Grp, '2', TX2_X + 3, GRP_TITLE_H + HDR1_H + 2, 20);
MakeLbl(Grp, '3', TX3_X + 3, GRP_TITLE_H + HDR1_H + 2, 20);
// Data rows (bands 0..10 = 160m..6m)
for b := 0 to BANDS - 1 do
begin
Y := DATA_Y0 + b * ROW_H;
MakeLbl(Grp, BAND_NAMES[b], BAND_X, Y + 2, 44);
// RX antenna — 3 radio buttons in a dedicated panel so they are mutually
// exclusive only within that band row (Lazarus groups by parent, not GroupIndex)
RxGrp := MkRbGroup(Grp, RX1_X, Y, RX3_X - RX1_X + 26);
FAlexRxAnt[b, 1] := MkRb(RxGrp, 0);
FAlexRxAnt[b, 2] := MkRb(RxGrp, RX2_X - RX1_X);
FAlexRxAnt[b, 3] := MkRb(RxGrp, RX3_X - RX1_X);
FAlexRxAnt[b, 1].Checked := True;
// Secondary RX checkboxes — mutually exclusive, handled in OnAlexRxOnlyChange
// Tag encodes: b=BYPS, b+100=EXT1, b+200=XVTR
Chk := MkCh(Grp, '', BYPS_X, Y, 22);
Chk.Tag := b;
Chk.OnChange := OnAlexRxOnlyChange;
FAlexRxByps[b] := Chk;
Chk := MkCh(Grp, '', EXT1_X, Y, 22);
Chk.Tag := b + 100;
Chk.OnChange := OnAlexRxOnlyChange;
FAlexRxExt1[b] := Chk;
Chk := MkCh(Grp, '', XVTR_X, Y, 22);
Chk.Tag := b + 200;
Chk.OnChange := OnAlexRxOnlyChange;
FAlexRxXvtr[b] := Chk;
// TX antenna — same pattern: dedicated panel per band row
TxGrp := MkRbGroup(Grp, TX1_X, Y, TX3_X - TX1_X + 26);
FAlexTxAnt[b, 1] := MkRb(TxGrp, 0);
FAlexTxAnt[b, 2] := MkRb(TxGrp, TX2_X - TX1_X);
FAlexTxAnt[b, 3] := MkRb(TxGrp, TX3_X - TX1_X);
FAlexTxAnt[b, 1].Checked := True;
// Do Not TX checkbox
FAlexDoNotTx[b] := MkCh(Grp, '', DNTX_X, Y, 22);
end;
// ---- TX Routing flags group ----
GrpTX := MakeGroupPanel(FPageAlex, 'TX Routing',
MARGIN, 86 + GRP_H + 12, 480, TXGRP_H);
FAlexRxBypassOnTx := MkCh(GrpTX, 'Rx BYPASS on TX',
PAD, GRP_TITLE_H + 8, 440);
FAlexExt1OnTx := MkCh(GrpTX, 'Ext 1 on TX',
PAD, GRP_TITLE_H + 8 + 28, 440);
FAlexGndBpf2OnTx := MkCh(GrpTX, 'Ground BPF2 on TX (ANAN-8000DLE)',
PAD, GRP_TITLE_H + 8 + 56, 440);
FAlexEnableXvtrHf := MkCh(GrpTX, 'Enable XVTR HF',
PAD, GRP_TITLE_H + 8 + 84, 440);
FAlexGrpAnt := Grp;
FAlexGrpTX := GrpTX;
// ---- Антенные разъёмы AD936x (Pluto/LibreSDR) ----
// Alex-таблицы у таких плат нет; «антенна» = какой из физических разъёмов
// (RX1/RX2, TX1/TX2) используется на диапазоне. Строки — VU-план + слоты
// трансвертеров (QO-100 и пр.). Группа занимает место Alex-групп и
// показывается вместо них (SetAntennaBackend).
FPlutoAntGrp := MakeGroupPanel(FPageAlex, 'Antenna Ports',
MARGIN, 86, ANT_GRP_W,
GRP_TITLE_H + ANT_HDR_H + (CFG_BAND_COUNT + 1 + CFG_XVTR_COUNT) * ANT_ROW_H + PAD);
FPlutoAntGrp.Visible := False;
// Шапка колонок: «RX»/«TX» над своей парой, под ними номера разъёмов.
Y := GRP_TITLE_H + 4;
MkAntHdr(FPlutoAntGrp, 'RX', (ANT_RX1_X + ANT_RX2_X) div 2 - 2, Y);
MkAntHdr(FPlutoAntGrp, 'TX', (ANT_TX1_X + ANT_TX2_X) div 2 - 2, Y);
MkAntHdr(FPlutoAntGrp, '1', ANT_RX1_X + 5, Y + 16);
MkAntHdr(FPlutoAntGrp, '2', ANT_RX2_X + 5, Y + 16);
MkAntHdr(FPlutoAntGrp, '1', ANT_TX1_X + 5, Y + 16);
MkAntHdr(FPlutoAntGrp, '2', ANT_TX2_X + 5, Y + 16);
Y := GRP_TITLE_H + ANT_HDR_H;
for b := 0 to CFG_BAND_COUNT - 1 do
begin
if BandName(b, True) = '' then Continue; // пустой слот плана — не строка
MkAntLbl(FPlutoAntGrp, BandName(b, True), BAND_X, Y + 3);
RxGrp := MkRbGroup(FPlutoAntGrp, ANT_RX1_X, Y, ANT_RX2_X - ANT_RX1_X + 24);
FAntRx[b, 0] := MkAntRb(RxGrp, 0);
FAntRx[b, 1] := MkAntRb(RxGrp, ANT_RX2_X - ANT_RX1_X);
TxGrp := MkRbGroup(FPlutoAntGrp, ANT_TX1_X, Y, ANT_TX2_X - ANT_TX1_X + 24);
FAntTx[b, 0] := MkAntRb(TxGrp, 0);
FAntTx[b, 1] := MkAntRb(TxGrp, ANT_TX2_X - ANT_TX1_X);
Inc(Y, ANT_ROW_H);
end;
// Секция трансвертеров: заголовок + строка на слот. Видимость и вертикальную
// упаковку (только включённые слоты, без дыр) делает LoadAnt936x.
FAntXvtrHdr := MkAntLbl(FPlutoAntGrp, 'Transverters', BAND_X, Y + 6);
FAntXvtrHdr.Font.Color := CLR_TEXTDIM;
FAntXvtrHdr.Visible := False;
Inc(Y, ANT_ROW_H);
for b := 0 to CFG_XVTR_COUNT - 1 do
begin
FAntXvtrRow[b] := TPanel.Create(Self);
FAntXvtrRow[b].Parent := FPlutoAntGrp;
FAntXvtrRow[b].SetBounds(0, Y, ANT_GRP_W, ANT_ROW_H);
FAntXvtrRow[b].BevelOuter := bvNone;
FAntXvtrRow[b].Color := CLR_PANEL;
FAntXvtrRow[b].Caption := '';
FAntXvtrRow[b].Visible := False;
FAntXvtrLbl[b] := MkAntLbl(FAntXvtrRow[b], '', BAND_X, 3);
RxGrp := MkRbGroup(FAntXvtrRow[b], ANT_RX1_X, 0, ANT_RX2_X - ANT_RX1_X + 24);
FAntXvtrRx[b, 0] := MkAntRb(RxGrp, 0);
FAntXvtrRx[b, 1] := MkAntRb(RxGrp, ANT_RX2_X - ANT_RX1_X);
TxGrp := MkRbGroup(FAntXvtrRow[b], ANT_TX1_X, 0, ANT_TX2_X - ANT_TX1_X + 24);
FAntXvtrTx[b, 0] := MkAntRb(TxGrp, 0);
FAntXvtrTx[b, 1] := MkAntRb(TxGrp, ANT_TX2_X - ANT_TX1_X);
Inc(Y, ANT_ROW_H);
end;
finally
FLoading := False;
end;
end;
procedure TSettingsForm.SetAntennaBackend(IsAD936x, HasAntSwitch: Boolean);
// Переключает вкладку Antenna: Alex-таблица (openHPSDR) либо таблица разъёмов
// AD936x. HasAntSwitch=False (прошивка не даёт доступа к мультиплексору) —
// Alex-таблица тоже не нужна: показываем пустую вкладку с поясняющим подзаголовком.
begin
FLoading := True;
try
if FAlexGrpAnt <> nil then FAlexGrpAnt.Visible := not IsAD936x;
if FAlexGrpTX <> nil then FAlexGrpTX.Visible := not IsAD936x;
if FPlutoAntGrp <> nil then FPlutoAntGrp.Visible := IsAD936x and HasAntSwitch;
if FAlexSubtitle <> nil then
begin
if IsAD936x and HasAntSwitch then
FAlexSubtitle.Caption :=
'Which physical connector (RX1/RX2, TX1/TX2) is used on each band.'
else if IsAD936x then
FAlexSubtitle.Caption := 'This board has a single antenna connector pair.'
else
FAlexSubtitle.Caption :=
'Per-band RX/TX antenna and secondary path routing for the Alex filter board.';
end;
finally
FLoading := False;
end;
end;
procedure TSettingsForm.LoadAnt936x(const A: TAD936xAnt; const X: TXvtrSettings);
// Таблица разъёмов + секция трансвертеров. Строки выключенных слотов не просто
// прячутся, а исключаются из раскладки (иначе между диапазонами и QO-100 зияла
// дыра в 8 строк); высота группы считается по фактическому числу строк.
const
ANT_ROW_H = 26;
PAD = 12;
var
b, Y, NVis, Bands: Integer;
begin
FLoading := True;
try
FAnt936x := A;
for b := 0 to CFG_BAND_COUNT - 1 do
begin
if FAntRx[b, 0] = nil then Continue;
FAntRx[b, 0].Checked := A.RxAnt[b] = 0;
FAntRx[b, 1].Checked := A.RxAnt[b] = 1;
FAntTx[b, 0].Checked := A.TxAnt[b] = 0;
FAntTx[b, 1].Checked := A.TxAnt[b] = 1;
end;
// Первая строка трансвертеров — сразу под заголовком секции.
if FAntXvtrHdr <> nil then Y := FAntXvtrHdr.Top + ANT_ROW_H - 6
else Y := 0;
NVis := 0;
for b := 0 to CFG_XVTR_COUNT - 1 do
begin
if FAntXvtrRow[b] = nil then Continue;
FAntXvtrRow[b].Visible := X.Entries[b].Enabled;
if not X.Entries[b].Enabled then Continue;
if X.Entries[b].ButtonText <> '' then
FAntXvtrLbl[b].Caption := string(X.Entries[b].ButtonText)
else
FAntXvtrLbl[b].Caption := Format('XVTR %d', [b + 1]);
FAntXvtrRx[b, 0].Checked := A.XvtrRxAnt[b] = 0;
FAntXvtrRx[b, 1].Checked := A.XvtrRxAnt[b] = 1;
FAntXvtrTx[b, 0].Checked := A.XvtrTxAnt[b] = 0;
FAntXvtrTx[b, 1].Checked := A.XvtrTxAnt[b] = 1;
FAntXvtrRow[b].Top := Y; // упаковка подряд, без дыр от выключенных слотов
Inc(Y, ANT_ROW_H);
Inc(NVis);
end;
if FAntXvtrHdr <> nil then FAntXvtrHdr.Visible := NVis > 0;
// Высота группы — по последней видимой строке (иначе внизу пустое поле).
if FPlutoAntGrp <> nil then
begin
if NVis = 0 then
begin
Bands := 0;
for b := 0 to CFG_BAND_COUNT - 1 do
if FAntRx[b, 0] <> nil then Inc(Bands);
Y := FPlutoAntGrp.Height - PAD; // fallback, если заголовка нет
if (FAntXvtrHdr <> nil) and (Bands > 0) then Y := FAntXvtrHdr.Top - 6;
end;
FPlutoAntGrp.Height := Y + PAD;
end;
finally
FLoading := False;
end;
end;
procedure TSettingsForm.Ant936xUIChange(Sender: TObject);
// Любая радиокнопка таблицы разъёмов: собрать состояние и отдать наружу.
var b: Integer;
begin
if FLoading then Exit;
for b := 0 to CFG_BAND_COUNT - 1 do
begin
if FAntRx[b, 0] = nil then Continue;
if FAntRx[b, 1].Checked then FAnt936x.RxAnt[b] := 1 else FAnt936x.RxAnt[b] := 0;
if FAntTx[b, 1].Checked then FAnt936x.TxAnt[b] := 1 else FAnt936x.TxAnt[b] := 0;
end;
for b := 0 to CFG_XVTR_COUNT - 1 do
begin
if FAntXvtrRx[b, 0] = nil then Continue;
if FAntXvtrRx[b, 1].Checked then FAnt936x.XvtrRxAnt[b] := 1
else FAnt936x.XvtrRxAnt[b] := 0;
if FAntXvtrTx[b, 1].Checked then FAnt936x.XvtrTxAnt[b] := 1
else FAnt936x.XvtrTxAnt[b] := 0;
end;
if Assigned(FOnAnt936xChange) then FOnAnt936xChange(FAnt936x);
end;
// ---------------------------------------------------------------------------
// Alex event handlers and data collectors
// ---------------------------------------------------------------------------
procedure TSettingsForm.CollectAlexFromUI;
var b, a: Integer;
begin
for b := 0 to 10 do
begin
FAlexConfig.RxAnt[b] := 1;
for a := 1 to 3 do
if (FAlexRxAnt[b, a] <> nil) and FAlexRxAnt[b, a].Checked then
begin
FAlexConfig.RxAnt[b] := a;
Break;
end;
FAlexConfig.TxAnt[b] := 1;
for a := 1 to 3 do
if (FAlexTxAnt[b, a] <> nil) and FAlexTxAnt[b, a].Checked then
begin
FAlexConfig.TxAnt[b] := a;
Break;
end;
if (FAlexRxXvtr[b] <> nil) and FAlexRxXvtr[b].Checked then
FAlexConfig.RxOnly[b] := 3
else if (FAlexRxExt1[b] <> nil) and FAlexRxExt1[b].Checked then
FAlexConfig.RxOnly[b] := 2
else if (FAlexRxByps[b] <> nil) and FAlexRxByps[b].Checked then
FAlexConfig.RxOnly[b] := 1
else
FAlexConfig.RxOnly[b] := 0;
FAlexConfig.DoNotTx[b] := (FAlexDoNotTx[b] <> nil) and FAlexDoNotTx[b].Checked;
end;
FAlexConfig.RxBypassOnTx := (FAlexRxBypassOnTx <> nil) and FAlexRxBypassOnTx.Checked;
FAlexConfig.Ext1OnTx := (FAlexExt1OnTx <> nil) and FAlexExt1OnTx.Checked;
FAlexConfig.GndBpf2OnTx := (FAlexGndBpf2OnTx <> nil) and FAlexGndBpf2OnTx.Checked;
FAlexConfig.EnableXvtrHf := (FAlexEnableXvtrHf <> nil) and FAlexEnableXvtrHf.Checked;
end;
procedure TSettingsForm.FireAlexChange;
begin
if FLoading then Exit;
CollectAlexFromUI;
if Assigned(FOnAlexChange) then
FOnAlexChange(FAlexConfig);
end;
procedure TSettingsForm.OnAlexAnyChange(Sender: TObject);
begin
FireAlexChange;
end;
procedure TSettingsForm.OnAlexRxOnlyChange(Sender: TObject);
var
Tag, b: Integer;
begin
if FLoading then Exit;
Tag := TFlatCheckBox(Sender).Tag;
if Tag >= 200 then
begin
b := Tag - 200; // XVTR
if TFlatCheckBox(Sender).Checked then
begin
FLoading := True;
FAlexRxByps[b].Checked := False;
FAlexRxExt1[b].Checked := False;
FLoading := False;
end;
end
else if Tag >= 100 then
begin
b := Tag - 100; // EXT1
if TFlatCheckBox(Sender).Checked then
begin
FLoading := True;
FAlexRxByps[b].Checked := False;
FAlexRxXvtr[b].Checked := False;
FLoading := False;
end;
end
else
begin
b := Tag; // BYPS
if TFlatCheckBox(Sender).Checked then
begin
FLoading := True;
FAlexRxExt1[b].Checked := False;
FAlexRxXvtr[b].Checked := False;
FLoading := False;
end;
end;
FireAlexChange;
end;
procedure TSettingsForm.LoadAlexSettings(const A: TAlexSettings; BoardType: Integer);
var b, ant: Integer;
begin
FLoading := True;
try
FAlexConfig := A;
FAlexBoardType := BoardType;
for b := 0 to 10 do
begin
for ant := 1 to 3 do
if FAlexRxAnt[b, ant] <> nil then
FAlexRxAnt[b, ant].Checked := (A.RxAnt[b] = ant);
for ant := 1 to 3 do
if FAlexTxAnt[b, ant] <> nil then
FAlexTxAnt[b, ant].Checked := (A.TxAnt[b] = ant);
if FAlexRxByps[b] <> nil then FAlexRxByps[b].Checked := (A.RxOnly[b] = 1);
if FAlexRxExt1[b] <> nil then FAlexRxExt1[b].Checked := (A.RxOnly[b] = 2);
if FAlexRxXvtr[b] <> nil then FAlexRxXvtr[b].Checked := (A.RxOnly[b] = 3);
if FAlexDoNotTx[b] <> nil then FAlexDoNotTx[b].Checked := A.DoNotTx[b];
end;
if FAlexRxBypassOnTx <> nil then FAlexRxBypassOnTx.Checked := A.RxBypassOnTx;
if FAlexExt1OnTx <> nil then FAlexExt1OnTx.Checked := A.Ext1OnTx;
if FAlexGndBpf2OnTx <> nil then FAlexGndBpf2OnTx.Checked := A.GndBpf2OnTx;
if FAlexEnableXvtrHf <> nil then FAlexEnableXvtrHf.Checked := A.EnableXvtrHf;
finally
FLoading := False;
end;
end;
// ===========================================================================
// XVTR (Transverter) tab
// ===========================================================================
procedure TSettingsForm.BuildXvtrTab;
const
MARGIN = 22;
ROW_H = 30;
HDR_H = 38; // место под двухстрочные заголовки столбцов
// X-координаты колонок (внутри Grp). Подобраны так чтобы помещаться
// в content-area формы (~880px при ширине 1080).
EN_X = 14;
NAME_X = 40; NAME_W = 104; // подпись на band-кнопке (макс. 15 символов)
BEG_X = 152; BEG_W = 80; // 3 десятичных, "144.000"
END_X = 240; END_W = 80;
LO_X = 328; LO_W = 90; // 6 десятичных, "116.000000"
ERR_X = 426; ERR_W = 70; // целое Hz
GAIN_X = 504; GAIN_W = 62; // 1 десятичная, dB
RXO_X = 574; RXO_W = 56; // chk центрирован
PWR_X = 638; PWR_W = 58; // целое %
PA_X = 704; PA_W = 66;
ANT_X = 778; ANT_W = 78; // combo default/ANT1/ANT2/ANT3
GRP_W = ANT_X + ANT_W + 14; // 934
var
Grp, GrpOpts: TPanel;
i, Y, GrpY: Integer;
L: TLabel;
function MakeFlt(AParent: TWinControl; AX, AY, AW: Integer;
AMin, AMax, AStep: Double; ADec: Integer): TFlatFloatSpinEdit;
begin
Result := TFlatFloatSpinEdit.Create(Self);
Result.Parent := AParent;
Result.SetBounds(AX, AY, AW, BTN_H);
Result.MinValue := AMin;
Result.MaxValue := AMax;
Result.Increment := AStep;
Result.DecimalPlaces := ADec;
Result.Color := CLR_INPUT;
Result.Font.Color := CLR_INPUT_TEXT;
Result.Font.Name := UI_FONT;
Result.Font.Size := 9;
Result.OnChange := OnXvtrAnyChange;
end;
function MakeInt(AParent: TWinControl; AX, AY, AW: Integer;
AMin, AMax: Integer): TFlatSpinEdit;
begin
Result := TFlatSpinEdit.Create(Self);
Result.Parent := AParent;
Result.SetBounds(AX, AY, AW, BTN_H);
Result.MinValue := AMin;
Result.MaxValue := AMax;
Result.Color := CLR_INPUT;
Result.Font.Color := CLR_INPUT_TEXT;
Result.Font.Name := UI_FONT;
Result.Font.Size := 9;
Result.OnChange := OnXvtrAnyChange;
end;
// Чекбокс без подписи (только квадрат). Подпись пишем отдельно над колонкой.
function MakeChk(AParent: TWinControl; AX, AY: Integer): TFlatCheckBox;
begin
Result := TFlatCheckBox.Create(Self);
Result.Parent := AParent;
Result.Caption := '';
Result.SetBounds(AX, AY, 22, 20);
Result.Font.Color := CLR_TEXT;
Result.Font.Name := UI_FONT;
Result.Font.Size := 9;
Result.OnChange := OnXvtrAnyChange;
end;
function MakeEdt(AParent: TWinControl; AX, AY, AW: Integer): TFlatEdit;
begin
Result := TFlatEdit.Create(Self);
Result.Parent := AParent;
Result.SetBounds(AX, AY, AW, BTN_H);
Result.Color := CLR_INPUT;
Result.Font.Color := CLR_INPUT_TEXT;
Result.Font.Name := UI_FONT;
Result.Font.Size := 9;
Result.MaxLength := 15;
Result.OnChange := OnXvtrAnyChange;
end;
function MakeCmb(AParent: TWinControl; AX, AY, AW: Integer): TFlatComboBox;
begin
Result := TFlatComboBox.Create(Self);
Result.Parent := AParent;
Result.SetBounds(AX, AY, AW, BTN_H);
Result.Color := CLR_INPUT;
Result.Font.Color := CLR_INPUT_TEXT;
Result.Font.Name := UI_FONT;
Result.Font.Size := 9;
Result.OnChange := OnXvtrAnyChange;
end;
// Заголовок столбца, центрирован по ширине AW от AX.
function MakeHdr(AParent: TWinControl; const Cap: string;
AX, AY, AW: Integer): TLabel;
begin
Result := TLabel.Create(Self);
Result.Parent := AParent;
Result.Caption := Cap;
Result.AutoSize := False;
Result.SetBounds(AX, AY, AW, 28);
Result.Alignment := taCenter;
Result.Layout := tlCenter;
Result.WordWrap := True;
Result.Font.Color := CLR_TEXTDIM;
Result.Font.Name := UI_FONT;
Result.Font.Size := 9;
end;
begin
FLoading := True;
try
// --- Заголовок страницы ---
L := TLabel.Create(Self);
L.Parent := FPageXvtr;
L.Caption := 'Transverters';
L.SetBounds(MARGIN, 18, 500, 28);
L.Font.Name := UI_FONT; L.Font.Size := 16;
L.Font.Style := [fsBold]; L.Font.Color := CLR_TEXT;
L := TLabel.Create(Self);
L.Parent := FPageXvtr;
L.Caption := 'Configure up to 8 transverter (XVTR) bands. When a band is selected on the main form, ' +
'the VFO shows the displayed (RF) frequency.' + LineEnding +
'The radio is internally tuned to the IF: f_IF = f_displayed - LO + LO Error.';
L.SetBounds(MARGIN, 50, GRP_W, 34);
L.Font.Name := UI_FONT; L.Font.Size := 9; L.Font.Color := CLR_TEXTDIM;
L.WordWrap := True;
// --- Group "Slots" --- (последний слот зарезервирован под QO-100 — не строим)
GrpY := 100;
Grp := MakeGroupPanel(FPageXvtr, 'Slots',
MARGIN, GrpY, GRP_W, GRP_TITLE_H + HDR_H + QO100_SLOT * ROW_H + 14);
// Заголовки колонок (под GRP_TITLE_H + небольшой отступ)
Y := GRP_TITLE_H + 6;
MakeHdr(Grp, 'EN', EN_X + 2, Y, 28);
MakeHdr(Grp, 'Label', NAME_X, Y, NAME_W);
MakeHdr(Grp, 'Begin' + LineEnding + '(MHz)', BEG_X, Y, BEG_W);
MakeHdr(Grp, 'End' + LineEnding + '(MHz)', END_X, Y, END_W);
MakeHdr(Grp, 'LO' + LineEnding + '(MHz)', LO_X, Y, LO_W);
MakeHdr(Grp, 'LO Error' + LineEnding + 'Hz', ERR_X, Y, ERR_W);
MakeHdr(Grp, 'RX Gain' + LineEnding + 'dB', GAIN_X, Y, GAIN_W);
MakeHdr(Grp, 'RX' + LineEnding + 'Only', RXO_X, Y, RXO_W);
MakeHdr(Grp, 'TX Pwr' + LineEnding + '%', PWR_X, Y, PWR_W);
MakeHdr(Grp, 'Disable' + LineEnding + 'PA', PA_X, Y, PA_W);
MakeHdr(Grp, 'RX' + LineEnding + 'Antenna', ANT_X, Y, ANT_W);
// Ряды слотов (QO100_SLOT исключён — у него отдельная страница QO-100)
for i := 0 to QO100_SLOT - 1 do
begin
Y := GRP_TITLE_H + HDR_H + i * ROW_H + 2;
// Чекбокс EN центрируем по 24px ширине EN-колонки
FXvtrEn[i] := MakeChk(Grp, EN_X + 5, Y + 2);
FXvtrName[i] := MakeEdt(Grp, NAME_X, Y, NAME_W);
FXvtrBegin[i] := MakeFlt(Grp, BEG_X, Y, BEG_W, 0.0, 50000.0, 0.001, 3);
FXvtrEnd[i] := MakeFlt(Grp, END_X, Y, END_W, 0.0, 50000.0, 0.001, 3);
FXvtrLOOff[i] := MakeFlt(Grp, LO_X, Y, LO_W, -50000.0, 50000.0, 0.001, 6);
// диапазон ±10 МГц: QO-100 LNB без опоры легко уходит на сотни кГц, и
// beacon-lock складывает свою коррекцию сюда (живая калибровка LNB).
FXvtrLOErr[i] := MakeFlt(Grp, ERR_X, Y, ERR_W, -9999999.0, 9999999.0, 1.0, 0);
FXvtrRXGain[i]:= MakeFlt(Grp, GAIN_X, Y, GAIN_W, -60.0, 60.0, 0.1, 1);
// Чекбокс RX Only центрируем
FXvtrRXOnly[i]:= MakeChk(Grp, RXO_X + (RXO_W - 22) div 2, Y + 2);
FXvtrPower[i] := MakeInt(Grp, PWR_X, Y, PWR_W, 0, 100);
FXvtrDisPA[i] := MakeChk(Grp, PA_X + (PA_W - 22) div 2, Y + 2);
FXvtrRXAnt[i] := MakeCmb(Grp, ANT_X, Y, ANT_W);
FXvtrRXAnt[i].Items.Add('default');
FXvtrRXAnt[i].Items.Add('ANT1');
FXvtrRXAnt[i].Items.Add('ANT2');
FXvtrRXAnt[i].Items.Add('ANT3');
FXvtrRXAnt[i].ItemIndex := 0;
FXvtrEn[i].Tag := i;
FXvtrEn[i].OnClick := OnXvtrAnyChange;
end;
// --- Group "Options" ---
GrpY := GrpY + GRP_TITLE_H + HDR_H + QO100_SLOT * ROW_H + 14 + 18;
GrpOpts := MakeGroupPanel(FPageXvtr, 'Options',
MARGIN, GrpY, GRP_W, GRP_TITLE_H + 36);
FChkXvtrTunPwr := TFlatCheckBox.Create(Self);
FChkXvtrTunPwr.Parent := GrpOpts;
FChkXvtrTunPwr.Caption := 'Use XVTR power for TUN '
+ '(TUN button uses the per-slot TX Power instead of the global TUN Level)';
FChkXvtrTunPwr.SetBounds(14, GRP_TITLE_H + 8, GRP_W - 28, 20);
FChkXvtrTunPwr.Font.Color := CLR_TEXT;
FChkXvtrTunPwr.Font.Name := UI_FONT;
FChkXvtrTunPwr.Font.Size := 9;
FChkXvtrTunPwr.OnChange := OnXvtrAnyChange;
// --- Подсказка про колонки внизу ---
L := TLabel.Create(Self);
L.Parent := FPageXvtr;
L.Caption :=
'EN - enable the slot. Label - band button caption. Begin/End - displayed VFO range. ' +
'LO - local oscillator. LO Error - fine correction in Hz.' + LineEnding +
'RX Gain - transverter RX gain offset. RX Only - block TX. TX Pwr - drive limit. ' +
'XVTR RF - use the main HF RF path.' + LineEnding +
'Disable PA - keep the internal PA in RX position during TX. ' +
'RX Antenna - override the per-band antenna for this slot.';
L.SetBounds(MARGIN, GrpY + GRP_TITLE_H + 36 + 16, GRP_W, 52);
L.Font.Name := UI_FONT; L.Font.Size := 9; L.Font.Color := CLR_TEXTDIM;
L.WordWrap := True;
// Применяем начальное состояние disabled-строк
for i := 0 to CFG_XVTR_COUNT - 1 do
UpdateXvtrRowEnabled(i);
finally
FLoading := False;
end;
end;
procedure TSettingsForm.UpdateXvtrRowEnabled(Idx: Integer);
var En: Boolean;
begin
if (Idx < 0) or (Idx >= CFG_XVTR_COUNT) then Exit;
if FXvtrEn[Idx] = nil then Exit;
En := FXvtrEn[Idx].Checked;
if FXvtrName[Idx] <> nil then FXvtrName[Idx].Enabled := En;
if FXvtrBegin[Idx] <> nil then FXvtrBegin[Idx].Enabled := En;
if FXvtrEnd[Idx] <> nil then FXvtrEnd[Idx].Enabled := En;
if FXvtrLOOff[Idx] <> nil then FXvtrLOOff[Idx].Enabled := En;
if FXvtrLOErr[Idx] <> nil then FXvtrLOErr[Idx].Enabled := En;
if FXvtrRXGain[Idx]<> nil then FXvtrRXGain[Idx].Enabled:= En;
if FXvtrRXOnly[Idx]<> nil then FXvtrRXOnly[Idx].Enabled:= En;
if FXvtrPower[Idx] <> nil then FXvtrPower[Idx].Enabled := En;
if FXvtrDisPA[Idx] <> nil then FXvtrDisPA[Idx].Enabled := En;
if FXvtrRXAnt[Idx] <> nil then FXvtrRXAnt[Idx].Enabled := En;
end;
procedure TSettingsForm.CollectXvtrFromUI;
var i: Integer;
begin
for i := 0 to CFG_XVTR_COUNT - 1 do
begin
if FXvtrEn[i] <> nil then FXvtrConfig.Entries[i].Enabled := FXvtrEn[i].Checked;
if FXvtrName[i] <> nil then FXvtrConfig.Entries[i].ButtonText := Copy(FXvtrName[i].Text, 1, 15);
if FXvtrBegin[i] <> nil then FXvtrConfig.Entries[i].FreqBegin := FXvtrBegin[i].Value * 1.0e6;
if FXvtrEnd[i] <> nil then FXvtrConfig.Entries[i].FreqEnd := FXvtrEnd[i].Value * 1.0e6;
if FXvtrLOOff[i] <> nil then FXvtrConfig.Entries[i].LOOffset := FXvtrLOOff[i].Value * 1.0e6;
if FXvtrLOErr[i] <> nil then FXvtrConfig.Entries[i].LOError := FXvtrLOErr[i].Value;
if FXvtrRXGain[i]<> nil then FXvtrConfig.Entries[i].RXGain := FXvtrRXGain[i].Value;
if FXvtrRXOnly[i]<> nil then FXvtrConfig.Entries[i].RXOnly := FXvtrRXOnly[i].Checked;
if FXvtrPower[i] <> nil then FXvtrConfig.Entries[i].TXPower := FXvtrPower[i].Value;
if FXvtrDisPA[i] <> nil then FXvtrConfig.Entries[i].DisablePA := FXvtrDisPA[i].Checked;
if (FXvtrRXAnt[i]<> nil) and (FXvtrRXAnt[i].ItemIndex >= 0) then
FXvtrConfig.Entries[i].RXAntenna := Byte(FXvtrRXAnt[i].ItemIndex);
end;
if FChkXvtrTunPwr <> nil then
FXvtrConfig.UseXVTRTunePower := FChkXvtrTunPwr.Checked;
end;
procedure TSettingsForm.FireXvtrChange;
var i: Integer;
begin
if FLoading then Exit;
CollectXvtrFromUI; // слоты 0..QO100_SLOT-1 (виджеты QO-слота не созданы → skip)
CollectQOFromUI; // QO100_SLOT — из QO-страницы
// Перерисовать enabled-состояние строк (если изменилась галка En)
for i := 0 to CFG_XVTR_COUNT - 1 do
UpdateXvtrRowEnabled(i);
if Assigned(FOnXvtrChange) then
FOnXvtrChange(FXvtrConfig);
end;
procedure TSettingsForm.OnXvtrAnyChange(Sender: TObject);
begin
FireXvtrChange;
end;
procedure TSettingsForm.LoadXvtrSettings(const X: TXvtrSettings);
var i: Integer;
begin
FLoading := True;
try
FXvtrConfig := X;
for i := 0 to CFG_XVTR_COUNT - 1 do
begin
if FXvtrEn[i] <> nil then FXvtrEn[i].Checked := X.Entries[i].Enabled;
if FXvtrName[i] <> nil then FXvtrName[i].Text := X.Entries[i].ButtonText;
if FXvtrBegin[i] <> nil then FXvtrBegin[i].Value := X.Entries[i].FreqBegin / 1.0e6;
if FXvtrEnd[i] <> nil then FXvtrEnd[i].Value := X.Entries[i].FreqEnd / 1.0e6;
if FXvtrLOOff[i] <> nil then FXvtrLOOff[i].Value := X.Entries[i].LOOffset / 1.0e6;
if FXvtrLOErr[i] <> nil then FXvtrLOErr[i].Value := X.Entries[i].LOError;
if FXvtrRXGain[i]<> nil then FXvtrRXGain[i].Value := X.Entries[i].RXGain;
if FXvtrRXOnly[i]<> nil then FXvtrRXOnly[i].Checked:= X.Entries[i].RXOnly;
if FXvtrPower[i] <> nil then FXvtrPower[i].Value := X.Entries[i].TXPower;
if FXvtrDisPA[i] <> nil then FXvtrDisPA[i].Checked := X.Entries[i].DisablePA;
if FXvtrRXAnt[i] <> nil then
FXvtrRXAnt[i].ItemIndex := EnsureRange(X.Entries[i].RXAntenna, 0, 3);
UpdateXvtrRowEnabled(i);
// Sync VHF calibration label in PA tab
if FVHFCalLabels[i] <> nil then
begin
if X.Entries[i].ButtonText <> '' then
FVHFCalLabels[i].Caption := 'VHF' + IntToStr(i) + ' (' + X.Entries[i].ButtonText + '):'
else
FVHFCalLabels[i].Caption := 'VHF' + IntToStr(i) + ':';
FVHFCalLabels[i].Hint := FVHFCalLabels[i].Caption;
end;
end;
if FChkXvtrTunPwr <> nil then FChkXvtrTunPwr.Checked := X.UseXVTRTunePower;
LoadQOFromConfig; // QO100_SLOT → отдельная страница QO-100
finally
FLoading := False;
end;
end;
// ── QO-100 page ─────────────────────────────────────────────────────────────
procedure TSettingsForm.BuildQOTab;
const
MARGIN = 22;
LBL_X = 14; LBL_W = 230;
ED_X = 250; ED_W = 130;
var
Grp: TPanel;
L: TLabel;
Y: Integer;
function QoFloat(AY: Integer; AMin, AMax, AStep: Double; ADec: Integer): TFlatFloatSpinEdit;
begin
Result := TFlatFloatSpinEdit.Create(Self);
Result.Parent := Grp;
Result.SetBounds(ED_X, AY, ED_W, BTN_H);
Result.MinValue := AMin; Result.MaxValue := AMax;
Result.Increment := AStep; Result.DecimalPlaces := ADec;
Result.Color := CLR_INPUT; Result.Font.Color := CLR_INPUT_TEXT;
Result.Font.Name := UI_FONT; Result.Font.Size := 9;
Result.OnChange := OnQOAnyChange;
end;
procedure QoLabel(const Cap: string; AY: Integer);
begin
L := TLabel.Create(Self);
L.Parent := Grp; L.Caption := Cap;
L.SetBounds(LBL_X, AY + 4, LBL_W, 18);
L.Font.Color := CLR_TEXTDIM; L.Font.Name := UI_FONT; L.Font.Size := 9;
end;
begin
FLoading := True;
try
L := TLabel.Create(Self);
L.Parent := FPageQO; L.Caption := 'QO-100 (Es''hail-2 NB transponder)';
L.SetBounds(MARGIN, 18, 600, 28);
L.Font.Name := UI_FONT; L.Font.Size := 16;
L.Font.Style := [fsBold]; L.Font.Color := CLR_TEXT;
L := TLabel.Create(Self);
L.Parent := FPageQO;
L.Caption := 'Geostationary transponder: RX downlink (~10489.5 MHz) via LNB, ' +
'TX uplink (~2400 MHz) direct. VFO is displayed in downlink terms; ' +
'RX and TX use separate LOs. Drift is corrected by the beacon lock.';
L.SetBounds(MARGIN, 50, 700, 34);
L.Font.Name := UI_FONT; L.Font.Size := 9; L.Font.Color := CLR_TEXTDIM;
L.WordWrap := True;
Grp := MakeGroupPanel(FPageQO, 'Transponder', MARGIN, 100, 560, GRP_TITLE_H + 8 * 30 + 16);
Y := GRP_TITLE_H + 8;
FQoEn := TFlatCheckBox.Create(Self);
FQoEn.Parent := Grp; FQoEn.Caption := 'Enable QO-100 band';
FQoEn.SetBounds(LBL_X, Y, 300, 20);
FQoEn.Font.Color := CLR_TEXT; FQoEn.Font.Name := UI_FONT; FQoEn.Font.Size := 9;
FQoEn.OnChange := OnQOAnyChange;
Inc(Y, 32); QoLabel('LNB LO (MHz)', Y);
FQoLnbLO := QoFloat(Y, 0.0, 50000.0, 0.001, 6);
FQoLnbLO.Value := 9750.0;
// LO Error (Гц): живая калибровка дрейфа LNB; beacon-lock складывает сюда
// свою коррекцию. Диапазон ±10 МГц — LNB без опоры уходит на сотни кГц.
Inc(Y, 30); QoLabel('LO Error (Hz)', Y);
FQoLoErr := QoFloat(Y, -9999999.0, 9999999.0, 1.0, 0);
Inc(Y, 30); QoLabel('Transponder offset = downlink - uplink (MHz)', Y);
FQoTxLO := QoFloat(Y, 0.0, 50000.0, 0.001, 6);
FQoTxLO.Value := 8089.5;
Inc(Y, 30); QoLabel('Downlink begin (MHz)', Y);
FQoBegin := QoFloat(Y, 0.0, 50000.0, 0.001, 3);
FQoBegin.Value := 10489.5;
Inc(Y, 30); QoLabel('Downlink end (MHz)', Y);
FQoEnd := QoFloat(Y, 0.0, 50000.0, 0.001, 3);
FQoEnd.Value := 10490.0;
Inc(Y, 30); QoLabel('RX gain offset (dB)', Y);
FQoRXGain := QoFloat(Y, -60.0, 60.0, 0.1, 1);
Inc(Y, 30); QoLabel('TX drive ceiling (%)', Y);
FQoPower := TFlatSpinEdit.Create(Self);
FQoPower.Parent := Grp; FQoPower.SetBounds(ED_X, Y, ED_W, BTN_H);
FQoPower.MinValue := 0; FQoPower.MaxValue := 100; FQoPower.Value := 100;
FQoPower.Color := CLR_INPUT; FQoPower.Font.Color := CLR_INPUT_TEXT;
FQoPower.Font.Name := UI_FONT; FQoPower.Font.Size := 9;
FQoPower.OnChange := OnQOAnyChange;
FQoInfo := TLabel.Create(Self);
FQoInfo.Parent := FPageQO;
FQoInfo.SetBounds(MARGIN, 100 + GRP_TITLE_H + 8 * 30 + 28, 700, 52);
FQoInfo.Font.Color := CLR_TEXTDIM; FQoInfo.Font.Name := 'Courier New';
FQoInfo.Font.Size := 9; FQoInfo.WordWrap := True;
finally
FLoading := False;
end;
end;
procedure TSettingsForm.UpdateQOInfo;
var dlMid, rxIf, txF: Double;
begin
if FQoInfo = nil then Exit;
dlMid := (FQoBegin.Value + FQoEnd.Value) / 2.0; // МГц
rxIf := dlMid - FQoLnbLO.Value; // RX IF в SDR
txF := dlMid - FQoTxLO.Value; // TX uplink
FQoInfo.Caption := Format(
'At downlink %.3f MHz: RX IF = %.3f MHz TX = %.3f MHz',
[dlMid, rxIf, txF]);
end;
procedure TSettingsForm.CollectQOFromUI;
begin
if FQoEn = nil then Exit;
FXvtrConfig.Entries[QO100_SLOT].Enabled := FQoEn.Checked;
FXvtrConfig.Entries[QO100_SLOT].FullDuplexTx := True; // слот всегда QO-100
FXvtrConfig.Entries[QO100_SLOT].ButtonText := 'QO-100';
FXvtrConfig.Entries[QO100_SLOT].LOOffset := FQoLnbLO.Value * 1.0e6;
FXvtrConfig.Entries[QO100_SLOT].LOError := FQoLoErr.Value;
FXvtrConfig.Entries[QO100_SLOT].TxLOOffset := FQoTxLO.Value * 1.0e6;
FXvtrConfig.Entries[QO100_SLOT].FreqBegin := FQoBegin.Value * 1.0e6;
FXvtrConfig.Entries[QO100_SLOT].FreqEnd := FQoEnd.Value * 1.0e6;
FXvtrConfig.Entries[QO100_SLOT].RXGain := FQoRXGain.Value;
FXvtrConfig.Entries[QO100_SLOT].TXPower := FQoPower.Value;
end;
procedure TSettingsForm.LoadQOFromConfig;
var E: TXvtrEntry;
begin
if FQoEn = nil then Exit;
E := FXvtrConfig.Entries[QO100_SLOT];
FQoEn.Checked := E.Enabled;
FQoLnbLO.Value := E.LOOffset / 1.0e6;
FQoLoErr.Value := E.LOError;
FQoTxLO.Value := E.TxLOOffset / 1.0e6;
FQoBegin.Value := E.FreqBegin / 1.0e6;
FQoEnd.Value := E.FreqEnd / 1.0e6;
FQoRXGain.Value := E.RXGain;
FQoPower.Value := EnsureRange(E.TXPower, 0, 100);
UpdateQOInfo;
end;
procedure TSettingsForm.OnQOAnyChange(Sender: TObject);
begin
if FLoading then Exit;
UpdateQOInfo;
FireXvtrChange;
end;
end.