mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 17:27:32 +00:00
«Default» строился через TXProfileCapture(живые настройки), поэтому сброс, сделанный на профиле ESSB 5k, оставлял в нём High cut 5000 — то есть Factory не сбрасывал ровно то, ради чего его жмут. Правило «заводское значит заводское» было применено только к EQ (FlatEQ), теперь ко всему тембру. База заводских профилей — DefaultTX, поверх копируется только живая обвязка микрофона: разъём и режим (Mic In/Line In, boost, bias, PTT, Tip/Ring), Line In gain и Mic gain. Их сброс сорвал бы настройку микшера. Мощность тоже остаётся живой — скачок drive ушёл бы в усилитель. Всё остальное (полоса, компрессор, leveler, ALC, phase rotator, EQ, AM carrier, TUN level) заводское. Текст подтверждения и подсказка обещали, что полоса и динамика останутся как есть, — переписаны под новое поведение. Проверено прогоном: с живыми ESSB 5k + компрессор 12 дБ + Line In + mic gain 22.5 заводской Default выходит 200/3100, comp off, EQ ровный, а Line In, mic gain и drive сохраняются. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
5432 lines
214 KiB
ObjectPascal
5432 lines
214 KiB
ObjectPascal
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, LCLType,
|
||
FlatButton, FlatCheckBox, FlatComboBox, FlatEdit, FlatSpinEdit, FlatFloatSpinEdit,
|
||
FlatRadioButton,
|
||
EqualizerControl, AudioOutput, AudioInput, AppTheme, Settings, BoardUtils, DpiUtils;
|
||
|
||
const
|
||
// Подвкладки страницы Transmit: Profile / EQ / Hardware / Display.
|
||
TXTAB_COUNT = 4;
|
||
// Tag контейнера подвкладки: ApplyTheme красит такие панели фоном страницы,
|
||
// а не цветом группы (иначе подложка выглядела бы как одна гигантская группа).
|
||
TAG_PAGE_BG = 909;
|
||
|
||
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;
|
||
// Калибровки (вкладка Calibration): детектор мощности/КСВ, напряжение/ток, S-метр
|
||
TOnCalibrationChange = procedure(const C: TCalibration) of object;
|
||
// TX-настройки целиком (любая правка → один callback с актуальной структурой)
|
||
TOnTXSettingsChange = procedure(const T: TTXSettings) of object;
|
||
// TX-профили: выбор/переименование/удаление по индексу, создание — по имени.
|
||
TOnTXProfileIdx = procedure(Idx: Integer) of object;
|
||
TOnTXProfileName = procedure(const AName: string) of object;
|
||
TOnTXProfileIdxName = procedure(Idx: Integer; const AName: string) 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; SpecPixels: Integer) 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;
|
||
// CAT дополнительных слайсов: индекс = слот (0 = слайс B).
|
||
TOnSliceCATChange = procedure(
|
||
const Enabled: array of Boolean;
|
||
const Port: array of Integer;
|
||
const AutoTX: array of Boolean;
|
||
const OutDev: array of string;
|
||
const InDev: array of string) of object;
|
||
TOnAlexChange = procedure(const A: TAlexSettings) of object;
|
||
TOnXvtrChange = procedure(const X: TXvtrSettings) of object;
|
||
TOnOCChange = procedure(const O: TOCSettings) 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;
|
||
FPageSlices: TScrollBox;
|
||
FPageTransmit: TScrollBox;
|
||
FNavAudio: TFlatButton;
|
||
FNavDisplay: TFlatButton;
|
||
FNavSpectrum: TFlatButton;
|
||
FNavWaterfall: TFlatButton;
|
||
FNavPA: TFlatButton;
|
||
FNavAdvanced: TFlatButton;
|
||
FNavCAT: TFlatButton;
|
||
FNavSlices: 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;
|
||
|
||
// ---- Calibration tab controls ----
|
||
// Таблицы «на диапазон» строятся на 12 слотов (максимум плана); подписи,
|
||
// видимость и раскладка задаются железом в RelayoutCalTab: у openHPSDR
|
||
// виден HF-план, у Pluto — VHF/UHF-план, слоты трансвертеров — только
|
||
// включённые. Значения пишутся в per-device секцию JSON (ключ = MAC),
|
||
// поэтому таблицы разных аппаратов не пересекаются.
|
||
FPageCalib: TScrollBox;
|
||
FNavCalib: TFlatButton;
|
||
FCalGrpDet: TPanel; // Power / SWR detector
|
||
FCalGrpVA: TPanel; // Supply voltage / current
|
||
FCalGrpSM: TPanel; // S-meter
|
||
FCmbPwrDetModel: TFlatComboBox; // модель детектора Alex-моста
|
||
FEdPADetCal: array[0..CFG_BAND_COUNT-1] of TFlatFloatSpinEdit; // множитель FWD/REV на банд
|
||
FLblPADetCal: array[0..CFG_BAND_COUNT-1] of TLabel;
|
||
FEdSupplyVoltCal: TFlatFloatSpinEdit; // множитель напряжения питания
|
||
FEdAmpVoff: TFlatFloatSpinEdit; // датчик тока: сдвиг нуля, мВ
|
||
FEdAmpSens: TFlatFloatSpinEdit; // датчик тока: мВ/А
|
||
FEdSMBand: array[0..CFG_BAND_COUNT-1] of TFlatFloatSpinEdit; // офсет S-метра на диапазон
|
||
FLblSMBand: array[0..CFG_BAND_COUNT-1] of TLabel;
|
||
FEdSMXvtr: array[0..CFG_XVTR_COUNT-1] of TFlatFloatSpinEdit; // офсет S-метра на трансвертер
|
||
FLblSMXvtr: array[0..CFG_XVTR_COUNT-1] of TLabel;
|
||
FLblSMBandHdr: TLabel;
|
||
FLblSMXvtrHdr: TLabel;
|
||
FCalGrpDisp: TPanel; // Spectrum / waterfall
|
||
FEdDispBand: array[0..CFG_BAND_COUNT-1] of TFlatFloatSpinEdit; // офсет спектра на диапазон
|
||
FLblDispBand: array[0..CFG_BAND_COUNT-1] of TLabel;
|
||
FEdDispXvtr: array[0..CFG_XVTR_COUNT-1] of TFlatFloatSpinEdit; // офсет спектра на трансвертер
|
||
FLblDispXvtr: array[0..CFG_XVTR_COUNT-1] of TLabel;
|
||
FLblDispBandHdr: TLabel;
|
||
FLblDispXvtrHdr: TLabel;
|
||
FBtnDispFromSM: TFlatButton; // «= S-meter» — копия таблицы
|
||
FCalGrpFreq: TPanel; // Reference frequency
|
||
FEdFreqCalPPM: TFlatFloatSpinEdit; // калибровка опорника, ppm
|
||
FCalIsPluto: Boolean; // активное железо — AD936x
|
||
FCalXvtrOn: array[0..CFG_XVTR_COUNT-1] of Boolean; // слот трансвертера включён
|
||
FCalXvtrName: array[0..CFG_XVTR_COUNT-1] of string; // подпись слота
|
||
FCalConfig: TCalibration;
|
||
FOnCalibrationChange: TOnCalibrationChange;
|
||
|
||
// ---- Transmit tab controls ----
|
||
// Текущее состояние, отдаётся в OnTXChange целиком при любой правке.
|
||
FTX: TTXSettings;
|
||
// Подвкладки страницы Transmit: Profile / EQ / Hardware / Display.
|
||
// Первые две — то, что хранится в TX-профиле, вторые две — свойства
|
||
// устройства и экрана (от профиля не зависят). См. TTXProfile в Settings.
|
||
FTXSubPage: array[0..TXTAB_COUNT-1] of TPanel;
|
||
FTXSubBtn: array[0..TXTAB_COUNT-1] of TFlatButton;
|
||
FTXSubIdx: Integer;
|
||
// Полоса профиля наверху страницы
|
||
FCmbTXProfile: TFlatComboBox;
|
||
FBtnTXProfNew: TFlatButton;
|
||
FBtnTXProfRen: TFlatButton;
|
||
FBtnTXProfDel: TFlatButton;
|
||
FBtnTXProfFactory: TFlatButton;
|
||
FLblTXProfDrive: TLabel; // «Drive профиля: N %» (ползунок — в главном окне)
|
||
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;
|
||
|
||
// ---- OC Control tab (Open Collector выходы Penny/Alex, openHPSDR-only) ----
|
||
FPageOC: TScrollBox;
|
||
FNavOC: TFlatButton;
|
||
FOCSubtitle: TLabel;
|
||
FOCEnabled: TFlatCheckBox;
|
||
// HF-группа: 11 диапазонов (0=160m..10=6m) × 7 пинов, RX/TX-маски
|
||
FOCHFRx: array[0..10, 0..OC_PIN_COUNT-1] of TFlatCheckBox;
|
||
FOCHFTx: array[0..10, 0..OC_PIN_COUNT-1] of TFlatCheckBox;
|
||
FOCHFAction: array[0..OC_PIN_COUNT-1] of TFlatComboBox;
|
||
FOCHFGrp: TPanel;
|
||
FOCHFActGrp: TPanel;
|
||
// VHF-группа = 8 слотов XVTR × 7 пинов, RX/TX-маски
|
||
FOCXvtrRx: array[0..CFG_XVTR_COUNT-1, 0..OC_PIN_COUNT-1] of TFlatCheckBox;
|
||
FOCXvtrTx: array[0..CFG_XVTR_COUNT-1, 0..OC_PIN_COUNT-1] of TFlatCheckBox;
|
||
FOCXvtrAction: array[0..OC_PIN_COUNT-1] of TFlatComboBox;
|
||
FOCXvtrGrp: TPanel;
|
||
FOCXvtrActGrp: TPanel;
|
||
FOCConfig: TOCSettings;
|
||
FOnOCChange: TOnOCChange;
|
||
|
||
// ---- 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)
|
||
FQoTxLOErr: TFlatFloatSpinEdit; // Гц — поправка offset'а (TO Error)
|
||
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;
|
||
// ---- Slices tab controls (CAT доп. слайсов; индекс = слот, 0 = слайс B) ----
|
||
FSliceCATEn: array[0..CFG_CAT_SLICE_COUNT-1] of TFlatCheckBox;
|
||
FSliceCATPort: array[0..CFG_CAT_SLICE_COUNT-1] of TFlatSpinEdit;
|
||
FSliceCATAutoTx: array[0..CFG_CAT_SLICE_COUNT-1] of TFlatCheckBox;
|
||
// Аудио слота: списки — общие с главными комбо (RefreshAudioDevices),
|
||
// первый пункт «Default»/«None» = общий выход/вход приложения.
|
||
FSliceOutDev: array[0..CFG_CAT_SLICE_COUNT-1] of TFlatComboBox;
|
||
FSliceInDev: array[0..CFG_CAT_SLICE_COUNT-1] of TFlatComboBox;
|
||
|
||
// ---- Advanced tab controls ----
|
||
FChkDither: TFlatCheckBox;
|
||
FChkRandom: TFlatCheckBox;
|
||
FChkWebEnabled: TFlatCheckBox;
|
||
FEdWebPort: TFlatSpinEdit;
|
||
FEdWebBind: TFlatEdit;
|
||
FEdWebUser: TFlatEdit;
|
||
FEdWebPass: TFlatEdit;
|
||
FCmbWebRes: TFlatComboBox;
|
||
|
||
// ---- Close button ----
|
||
FBtnClose: TFlatButton;
|
||
|
||
// ---- Callbacks ----
|
||
FOnCATChange: TOnCATChange;
|
||
FOnSliceCATChange: TOnSliceCATChange;
|
||
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;
|
||
FOnTXProfileSelect: TOnTXProfileIdx;
|
||
FOnTXProfileAdd: TOnTXProfileName;
|
||
FOnTXProfileRename: TOnTXProfileIdxName;
|
||
FOnTXProfileDelete: TOnTXProfileIdx;
|
||
FOnTXProfileFactory: TNotifyEvent;
|
||
|
||
FLoading: Boolean;
|
||
|
||
procedure BuildUI;
|
||
procedure BuildAudioTab;
|
||
procedure BuildDisplayTab;
|
||
procedure BuildRX1Tab;
|
||
procedure BuildPATab;
|
||
procedure BuildCalibrationTab;
|
||
procedure RelayoutCalTab;
|
||
// Раскладка блока офсетов «на диапазон + на трансвертер» внутри группы
|
||
// (общая для S-метра и спектра). Возвращает нужную высоту группы.
|
||
function LayoutCalOffsets(const BandLbl: array of TLabel;
|
||
const BandEd: array of TFlatFloatSpinEdit;
|
||
XvtrHdr: TLabel;
|
||
const XvtrLbl: array of TLabel;
|
||
const XvtrEd: array of TFlatFloatSpinEdit;
|
||
Y0: Integer): Integer;
|
||
procedure OnCalDispFromSMClick(Sender: TObject);
|
||
procedure CollectCalFromUI;
|
||
procedure OnCalAnyChange(Sender: TObject);
|
||
procedure BuildCATTab;
|
||
procedure BuildSlicesTab;
|
||
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 BuildOCTab;
|
||
procedure CollectOCFromUI;
|
||
procedure FireOCChange;
|
||
procedure OnOCAnyChange(Sender: TObject);
|
||
procedure CollectTXFromUI;
|
||
procedure FireTXChange;
|
||
procedure OnTXAnyChange(Sender: TObject);
|
||
procedure OnMicJackChange(Sender: TObject);
|
||
procedure UpdateMicJackVisibility;
|
||
// Подвкладки и полоса профиля на странице Transmit
|
||
procedure SelectTXSubPage(Idx: Integer);
|
||
procedure TXSubTabClick(Sender: TObject);
|
||
procedure OnTXProfileComboChange(Sender: TObject);
|
||
procedure BtnTXProfNewClick(Sender: TObject);
|
||
procedure BtnTXProfRenClick(Sender: TObject);
|
||
procedure BtnTXProfDelClick(Sender: TObject);
|
||
procedure BtnTXProfFactoryClick(Sender: TObject);
|
||
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 OnSliceCATCtrlChange(Sender: TObject);
|
||
procedure SelectDevInCombo(Cmb: TFlatComboBox; const DevName: string);
|
||
procedure FillSliceDevCombos(AudioOut: TAudioOutput; AudioIn: TAudioInput);
|
||
procedure FireSliceCATChange;
|
||
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);
|
||
procedure LoadCalibration(const C: TCalibration);
|
||
// Железо вкладки Calibration: band-план и состав групп (Pluto/openHPSDR).
|
||
procedure SetCalibrationBackend(IsPluto: Boolean);
|
||
// Вкладка Antenna: Alex-таблица (openHPSDR) либо таблица разъёмов AD936x.
|
||
procedure SetAntennaBackend(IsAD936x, HasAntSwitch: Boolean);
|
||
// Таблица разъёмов + имена/видимость слотов трансвертеров.
|
||
procedure LoadAnt936x(const A: TAD936xAnt; const X: TXvtrSettings);
|
||
procedure LoadVHFCalSettings(const VHFCal: array of Double);
|
||
// Слайс-CAT: enable/порт/AutoTX по слотам (0 = слайс B).
|
||
procedure LoadSliceCATSettings(
|
||
const Enabled: array of Boolean;
|
||
const Port: array of Integer;
|
||
const AutoTX: array of Boolean;
|
||
const OutDev: array of string;
|
||
const InDev: array of string);
|
||
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);
|
||
// Список TX-профилей + активный + drive активного (ползунок DRV живёт в
|
||
// главном окне, здесь показываем его значение как часть профиля).
|
||
procedure LoadTXProfiles(const Names: array of string;
|
||
ActiveIdx, DrivePct: Integer);
|
||
// Живое обновление подписи «Drive профиля» (ползунок DRV в главном окне).
|
||
procedure SetTXProfileDrive(DrivePct: Integer);
|
||
// Открыть диалог сразу на странице Transmit (ПКМ по кнопке профиля).
|
||
// SubIdx: 0=Profile, 1=EQ, 2=Hardware, 3=Display.
|
||
procedure ShowTransmitPage(SubIdx: Integer = 0);
|
||
procedure LoadAlexSettings(const A: TAlexSettings; BoardType: Integer);
|
||
procedure LoadXvtrSettings(const X: TXvtrSettings);
|
||
procedure LoadOCSettings(const O: TOCSettings);
|
||
// Вкладка OC Control — только openHPSDR (у Pluto/AD936x нет такого выхода).
|
||
procedure SetOCBackend(IsPluto: Boolean);
|
||
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; SpecPixels: Integer);
|
||
|
||
property OnCATChange: TOnCATChange read FOnCATChange write FOnCATChange;
|
||
property OnSliceCATChange: TOnSliceCATChange read FOnSliceCATChange write FOnSliceCATChange;
|
||
property OnAlexChange: TOnAlexChange read FOnAlexChange write FOnAlexChange;
|
||
property OnXvtrChange: TOnXvtrChange read FOnXvtrChange write FOnXvtrChange;
|
||
property OnOCChange: TOnOCChange read FOnOCChange write FOnOCChange;
|
||
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 OnCalibrationChange: TOnCalibrationChange read FOnCalibrationChange write FOnCalibrationChange;
|
||
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 OnTXProfileSelect: TOnTXProfileIdx read FOnTXProfileSelect write FOnTXProfileSelect;
|
||
property OnTXProfileAdd: TOnTXProfileName read FOnTXProfileAdd write FOnTXProfileAdd;
|
||
property OnTXProfileRename: TOnTXProfileIdxName read FOnTXProfileRename write FOnTXProfileRename;
|
||
property OnTXProfileDelete: TOnTXProfileIdx read FOnTXProfileDelete write FOnTXProfileDelete;
|
||
property OnTXProfileFactory: TNotifyEvent read FOnTXProfileFactory write FOnTXProfileFactory;
|
||
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)
|
||
|
||
// Первый пункт комбо аудио слайса = «как у приложения» (пустое имя в конфиге).
|
||
SLICE_DEV_DEFAULT = 'Default (app output)';
|
||
SLICE_DEV_NONE = 'None (app input)';
|
||
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');
|
||
|
||
// TX pin action (OC Control) — имена в UI, значения = OCA_* из Settings.pas.
|
||
OC_ACTION_NAMES: array[0..6] of string = (
|
||
'MOX', 'TUNE', '2TON', 'MOX+TUNE', 'MOX+2TON', 'TUNE+2TON', 'MOX+TUNE+2TON');
|
||
|
||
// Раскладка вкладки Calibration — общая для построения (BuildCalibrationTab)
|
||
// и перекладки под активное железо (RelayoutCalTab).
|
||
CAL_MARGIN = 22;
|
||
CAL_PAD = 18;
|
||
CAL_ROW_H = 32;
|
||
CAL_R1 = 42;
|
||
CAL_ED_W = 72;
|
||
CAL_LBL_W = 54;
|
||
CAL_SM_LBL_W = 68; // подписи трансвертеров длиннее ('70cm', 'QO-100')
|
||
CAL_COLS = 4;
|
||
CAL_GRP_W = 700;
|
||
CAL_GRP1_H = 236; // Power / SWR detector
|
||
CAL_GRP2_H = 156; // Supply voltage / current
|
||
CAL_VA_LBL_W = 220;
|
||
|
||
{ TSettingsForm }
|
||
|
||
constructor TSettingsForm.Create(AOwner: TComponent);
|
||
begin
|
||
inherited CreateNew(AOwner);
|
||
Caption := 'Settings';
|
||
Width := DpiScale(1180);
|
||
Height := DpiScale(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 := DpiScale(ALeft);
|
||
Result.Top := DpiScale(ATop);
|
||
if AW > 0 then Result.Width := DpiScale(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 := DpiScale(ALeft);
|
||
Result.Top := DpiScale(ATop);
|
||
Result.Width := DpiScale(AW);
|
||
Result.Height := DpiScale(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;
|
||
SAW: Integer; // AW уже пересчитан в реальные пиксели — используем везде ниже
|
||
begin
|
||
SAW := DpiScale(AW);
|
||
Result := TPanel.Create(Self);
|
||
Result.Parent := AParent;
|
||
Result.SetBounds(DpiScale(ALeft), DpiScale(ATop), SAW, DpiScale(AH));
|
||
Result.BevelOuter := bvNone;
|
||
Result.Color := CLR_PANEL;
|
||
|
||
TitleLbl := TLabel.Create(Self);
|
||
TitleLbl.Parent := Result;
|
||
TitleLbl.Caption := Cap;
|
||
TitleLbl.SetBounds(DpiScale(8), DpiScale(5), SAW - DpiScale(16), DpiScale(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, DpiScale(GRP_TITLE_H), SAW + DpiScale(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(DpiScale(10), DpiScale(ATop), DpiScale(166), DpiScale(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 := DpiScale(188);
|
||
FNavPanel.BevelOuter := bvNone;
|
||
FNavPanel.Color := TColor($00151515);
|
||
|
||
with TLabel.Create(Self) do
|
||
begin
|
||
Parent := FNavPanel;
|
||
Caption := 'Settings';
|
||
SetBounds(DpiScale(14), DpiScale(14), DpiScale(160), DpiScale(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(DpiScale(14), DpiScale(39), DpiScale(160), DpiScale(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);
|
||
FNavCalib := MakeNavButton('Calibration', 290);
|
||
FNavAdvanced := MakeNavButton('Advanced', 326);
|
||
FNavCAT := MakeNavButton('CAT', 362);
|
||
FNavSlices := MakeNavButton('Slices', 398);
|
||
FNavAlex := MakeNavButton('Antenna / Alex', 434);
|
||
FNavXvtr := MakeNavButton('Transverter', 470);
|
||
FNavOC := MakeNavButton('OC Control', 506);
|
||
FNavQO := MakeNavButton('QO-100', 542);
|
||
|
||
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;
|
||
FPageCalib := MakeScrollPage;
|
||
FPageAdvanced := MakeScrollPage;
|
||
FPageCAT := MakeScrollPage;
|
||
FPageSlices := MakeScrollPage;
|
||
FPageAlex := MakeScrollPage;
|
||
FPageXvtr := MakeScrollPage;
|
||
FPageOC := MakeScrollPage;
|
||
FPageQO := MakeScrollPage;
|
||
FAlexBoardType := 0;
|
||
TSettingsManager.DefaultAlex(FAlexConfig);
|
||
TSettingsManager.DefaultXvtr(FXvtrConfig);
|
||
TSettingsManager.DefaultCalibration(FCalConfig);
|
||
TSettingsManager.DefaultOC(FOCConfig);
|
||
|
||
BuildAudioTab;
|
||
BuildDisplayTab;
|
||
BuildRX1Tab;
|
||
BuildTransmitTab;
|
||
BuildPATab;
|
||
BuildCalibrationTab;
|
||
BuildAdvancedTab;
|
||
BuildCATTab;
|
||
BuildSlicesTab;
|
||
BuildAlexTab;
|
||
BuildXvtrTab;
|
||
BuildOCTab;
|
||
BuildQOTab;
|
||
|
||
FBtnClose := TFlatButton.Create(Self);
|
||
FBtnClose.Parent := Self;
|
||
FBtnClose.Caption := 'Close';
|
||
FBtnClose.SetBounds(Width - DpiScale(106), Height - DpiScale(40), DpiScale(92), DpiScale(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 = FNavCalib then SelectPage(FPageCalib, FNavCalib)
|
||
else if Sender = FNavAdvanced then SelectPage(FPageAdvanced, FNavAdvanced)
|
||
else if Sender = FNavCAT then SelectPage(FPageCAT, FNavCAT)
|
||
else if Sender = FNavSlices then SelectPage(FPageSlices, FNavSlices)
|
||
else if Sender = FNavAlex then SelectPage(FPageAlex, FNavAlex)
|
||
else if Sender = FNavXvtr then SelectPage(FPageXvtr, FNavXvtr)
|
||
else if Sender = FNavOC then SelectPage(FPageOC, FNavOC)
|
||
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;
|
||
FPageCalib.Visible := APage = FPageCalib;
|
||
FPageAdvanced.Visible := APage = FPageAdvanced;
|
||
FPageCAT.Visible := APage = FPageCAT;
|
||
FPageSlices.Visible := APage = FPageSlices;
|
||
FPageAlex.Visible := APage = FPageAlex;
|
||
FPageXvtr.Visible := APage = FPageXvtr;
|
||
FPageOC.Visible := APage = FPageOC;
|
||
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;
|
||
FNavCalib.Active := ANav = FNavCalib;
|
||
FNavAdvanced.Active := ANav = FNavAdvanced;
|
||
FNavCAT.Active := ANav = FNavCAT;
|
||
FNavSlices.Active := ANav = FNavSlices;
|
||
FNavAlex.Active := ANav = FNavAlex;
|
||
FNavXvtr.Active := ANav = FNavXvtr;
|
||
FNavOC.Active := ANav = FNavOC;
|
||
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(DpiScale(MARGIN), DpiScale(18), DpiScale(400), DpiScale(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(DpiScale(MARGIN), DpiScale(48), DpiScale(560), DpiScale(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(DpiScale(PAD), DpiScale(R1), DpiScale(420), DpiScale(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(DpiScale(MARGIN), DpiScale(18), DpiScale(400), DpiScale(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(DpiScale(MARGIN), DpiScale(48), DpiScale(680), DpiScale(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(DpiScale(PAD), DpiScale(R1), DpiScale(CHKW), DpiScale(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(DpiScale(PAD + CHKW + 20), DpiScale(R1), DpiScale(CHKW), DpiScale(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(DpiScale(PAD + (CHKW + 20) * 2), DpiScale(R1), DpiScale(CHKW), DpiScale(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(DpiScale(PAD + (CHKW + 20) * 2), DpiScale(R1 + 28), DpiScale(CHKW), DpiScale(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(DpiScale(PAD), DpiScale(R1 + 28), DpiScale(CHKW), DpiScale(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(DpiScale(PAD), DpiScale(R1), DpiScale(CHKW), DpiScale(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(DpiScale(AX), DpiScale(AY), DpiScale(AW), DpiScale(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(DpiScale(AX), DpiScale(AY), DpiScale(AW), DpiScale(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(DpiScale(MARGIN), DpiScale(18), DpiScale(400), DpiScale(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(DpiScale(MARGIN), DpiScale(48), DpiScale(620), DpiScale(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(DpiScale(MARGIN), DpiScale(18), DpiScale(400), DpiScale(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(DpiScale(MARGIN), DpiScale(48), DpiScale(620), DpiScale(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(DpiScale(PAD), DpiScale(R1), DpiScale(200), DpiScale(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(DpiScale(PAD), DpiScale(R1 + STEP), DpiScale(200), DpiScale(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;
|
||
SubTop: Integer; // общий (немасштабированный) Top всех четырёх подвкладок
|
||
|
||
// Подложка подвкладки: страница Transmit — четыре независимых экрана,
|
||
// переключаемых сегментами (свиток на 2000 px не листают, в нём ищут).
|
||
function SubPage(Idx: Integer; ATop: Integer): TPanel;
|
||
begin
|
||
Result := TPanel.Create(Self);
|
||
Result.Parent := FPageTransmit;
|
||
Result.BevelOuter := bvNone;
|
||
Result.Color := CLR_BG;
|
||
Result.Tag := TAG_PAGE_BG; // ApplyTheme: фон страницы, не группа
|
||
Result.SetBounds(0, DpiScale(ATop),
|
||
DpiScale(GRP_W + 2 * MARGIN), DpiScale(400));
|
||
Result.Visible := False;
|
||
FTXSubPage[Idx] := Result;
|
||
end;
|
||
|
||
function MkBtn(AParent: TWinControl; AX, AY, AW: Integer;
|
||
const ACap: string; AClick: TNotifyEvent): TFlatButton;
|
||
begin
|
||
Result := TFlatButton.Create(Self);
|
||
Result.Parent := AParent;
|
||
Result.Caption := ACap;
|
||
Result.SetBounds(DpiScale(AX), DpiScale(AY), DpiScale(AW), DpiScale(BTN_H));
|
||
Result.ClrNorm := CLR_PANEL;
|
||
Result.ClrBorder := CLR_BORDER;
|
||
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.OnClick := AClick;
|
||
end;
|
||
|
||
function MkSpin(AParent: TWinControl; AX, AY: Integer; AMin, AMax, AVal: Integer): TFlatSpinEdit;
|
||
begin
|
||
Result := TFlatSpinEdit.Create(Self);
|
||
Result.Parent := AParent;
|
||
Result.SetBounds(DpiScale(AX), DpiScale(AY), DpiScale(SPW), DpiScale(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(DpiScale(AX), DpiScale(AY), DpiScale(SPW), DpiScale(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(DpiScale(AX), DpiScale(AY), DpiScale(AW), DpiScale(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(DpiScale(MARGIN), DpiScale(18), DpiScale(400), DpiScale(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 := 'Profile and EQ hold the active TX profile; '
|
||
+ 'Hardware and Display are radio properties and do not follow it.';
|
||
SetBounds(DpiScale(MARGIN), DpiScale(48), DpiScale(GRP_W), DpiScale(20));
|
||
Font.Name := UI_FONT;
|
||
Font.Size := 9;
|
||
Font.Color := CLR_TEXTDIM;
|
||
end;
|
||
|
||
// ---- Полоса профиля (над подвкладками, видна всегда) --------------------
|
||
Grp := MakeGroupPanel(FPageTransmit, 'TX Profile', MARGIN, 78, GRP_W, 124);
|
||
|
||
MakeLbl(Grp, 'Active', PAD, R1 + 5, 60);
|
||
FCmbTXProfile := MakeCombo(Grp, PAD + 60, R1, 230, OnTXProfileComboChange);
|
||
FBtnTXProfNew := MkBtn(Grp, PAD + 302, R1, 100, 'New...', BtnTXProfNewClick);
|
||
FBtnTXProfRen := MkBtn(Grp, PAD + 408, R1, 100, 'Rename...', BtnTXProfRenClick);
|
||
FBtnTXProfDel := MkBtn(Grp, PAD + 514, R1, 100, 'Delete', BtnTXProfDelClick);
|
||
FBtnTXProfFactory := MkBtn(Grp, PAD + 620, R1, 100, 'Factory', BtnTXProfFactoryClick);
|
||
|
||
MakeLbl(Grp, 'Edits on the Profile and EQ tabs go into the active profile '
|
||
+ 'immediately — there is no separate Save button.',
|
||
PAD, R1 + 30, GRP_W - 2 * PAD);
|
||
MakeLbl(Grp, 'New... forks the current sound under a new name. Factory rebuilds '
|
||
+ 'the stock set; it keeps the microphone wiring and drive, not the sound.',
|
||
PAD, R1 + 48, GRP_W - 2 * PAD);
|
||
|
||
// ---- Сегменты подвкладок ------------------------------------------------
|
||
Y := 78 + 124 + 12;
|
||
FTXSubBtn[0] := MkBtn(FPageTransmit, MARGIN, Y, 132, 'Profile', TXSubTabClick);
|
||
FTXSubBtn[1] := MkBtn(FPageTransmit, MARGIN + 136, Y, 132, 'EQ', TXSubTabClick);
|
||
FTXSubBtn[2] := MkBtn(FPageTransmit, MARGIN + 272, Y, 132, 'Hardware', TXSubTabClick);
|
||
FTXSubBtn[3] := MkBtn(FPageTransmit, MARGIN + 408, Y, 132, 'Display', TXSubTabClick);
|
||
for i := 0 to TXTAB_COUNT - 1 do FTXSubBtn[i].Tag := i;
|
||
|
||
Inc(Y, BTN_H + 14);
|
||
SubTop := Y; // ★ немасштабированный: FTXSubPage[].Top уже в пикселях
|
||
|
||
// =========================================================================
|
||
// Подвкладка 0 — Profile: всё, что определяет звук в эфире
|
||
// =========================================================================
|
||
SubPage(0, SubTop);
|
||
|
||
// ---- Микрофонный вход (часть профиля: гарнитура vs микшер-ESSB) ----
|
||
// Две строки, обе по общей сетке страницы: подпись в колонке PAD (ширина LW),
|
||
// поле в колонке CX, вторая пара — от PAD+320. Тумблеры справа с равным
|
||
// шагом. Connector скрыт на не-Orion и потому делит строку с Mic gain —
|
||
// спрятанная пара не оставляет дыры.
|
||
Grp := MakeGroupPanel(FTXSubPage[0], 'Microphone', MARGIN, 0, GRP_W, 134);
|
||
|
||
// Row 0: Mic jack | Boost (Mic In) либо Line In gain (Line In) | Bias | PTT
|
||
MakeLbl(Grp, 'Mic jack', PAD, R1 + 5, LW);
|
||
FCmbTXMicJack := MkCmb(Grp, CX, R1, 110);
|
||
FCmbTXMicJack.Items.Add('Mic In');
|
||
FCmbTXMicJack.Items.Add('Line In');
|
||
FCmbTXMicJack.ItemIndex := 0;
|
||
FCmbTXMicJack.OnChange := OnMicJackChange;
|
||
|
||
// Boost и пара «Line In gain» занимают одно и то же место (видима одна из них).
|
||
FChkTXMicBoost := MkChk(Grp, PAD + 302, R1, 170, 'Mic boost +20 dB');
|
||
FLblLineInGain := MakeLbl(Grp, 'Line In gain', PAD + 302, R1 + 5, 76);
|
||
FEdLineInGain := MkFloat(Grp, PAD + 382, R1, 0.0, -34.5, 12.0, 1.5);
|
||
FEdLineInGain.DecimalPlaces := 1;
|
||
FChkTXMicBias := MkChk(Grp, PAD + 490, R1, 100, 'Mic bias');
|
||
FChkTXMicPTT := MkChk(Grp, PAD + 596, R1, 130, 'Mic-jack PTT');
|
||
|
||
// Row 1: Mic gain | Connector Tip/Ring (только Orion boards 4 и 5)
|
||
MakeLbl(Grp, 'Mic gain (dB)', PAD, R1 + STEP + 5, LW);
|
||
FEdTXMicGain := MkFloat(Grp, CX, R1 + STEP, 10.0, -20.0, 60.0, 0.5);
|
||
|
||
FLblTipRing := MakeLbl(Grp, 'Connector', PAD + 302, R1 + STEP + 5, 76);
|
||
FCmbTXMicTR := MkCmb(Grp, PAD + 382, R1 + STEP, 96);
|
||
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;
|
||
|
||
// ---- Кромки TX-фильтра (FIR/фаза/окно — на вкладке Hardware) ----
|
||
Y := 134 + 12;
|
||
Grp := MakeGroupPanel(FTXSubPage[0], 'TX Filter (bandpass)', MARGIN, Y, GRP_W, 108);
|
||
|
||
MakeLbl(Grp, 'Low cut (Hz)', PAD, R1 + 5, LW);
|
||
FEdTXFiltLow := MkSpin(Grp, CX, R1, 0, 8000, 200);
|
||
MakeLbl(Grp, 'High cut (Hz)', PAD + 320, R1 + 5, 90);
|
||
FEdTXFiltHigh := MkSpin(Grp, PAD + 320 + 100, R1, 100, 12000, 3100);
|
||
MakeLbl(Grp, 'Voice bandwidth. ESSB is simply a high cut of 3500..5000 Hz; '
|
||
+ 'sideband sign is applied by the engine, these are magnitudes.',
|
||
PAD, R1 + 32, GRP_W - 2 * PAD);
|
||
|
||
// ---- Динамическая обработка ----
|
||
Inc(Y, 108 + 12);
|
||
Grp := MakeGroupPanel(FTXSubPage[0], '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);
|
||
|
||
// ---- Мощность и несущая AM (тоже часть профиля) ----
|
||
Inc(Y, 220 + 12);
|
||
Grp := MakeGroupPanel(FTXSubPage[0], 'Power / AM carrier', MARGIN, Y, GRP_W, 148);
|
||
|
||
MakeLbl(Grp, 'Tune level (%)', PAD, R1 + 5, LW);
|
||
FEdTUNLevel := MkSpin(Grp, CX, R1, 0, 100, 10);
|
||
MakeLbl(Grp, 'AM carrier level', PAD + 320, R1 + 5, 110);
|
||
FEdTXAMCarrier := MkFloat(Grp, PAD + 320 + 120, R1, 0.5, 0.0, 1.0, 0.05);
|
||
|
||
FLblTXProfDrive := MakeLbl(Grp, 'Profile drive: —', PAD, R1 + STEP + 5, 300);
|
||
MakeLbl(Grp, 'Drive belongs to the profile, but you set it with the DRV slider '
|
||
+ 'in the main window — the active profile follows it.',
|
||
PAD, R1 + STEP + 28, GRP_W - 2 * PAD);
|
||
|
||
FTXSubPage[0].Height := DpiScale(Y + 148 + 12);
|
||
|
||
// =========================================================================
|
||
// Подвкладка 1 — EQ (тоже профильный, но занимает целый экран)
|
||
// =========================================================================
|
||
SubPage(1, SubTop);
|
||
|
||
Grp := MakeGroupPanel(FTXSubPage[1], 'TX Equalizer', MARGIN, 0, GRP_W, 434);
|
||
|
||
FTXEQControl := TEqualizerControl.Create(Self);
|
||
FTXEQControl.Parent := Grp;
|
||
FTXEQControl.SetBounds(DpiScale(PAD), DpiScale(R1), DpiScale(GRP_W - 2 * PAD), DpiScale(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;
|
||
|
||
// Главное, что надо знать про этот EQ: точки — не «полосы», а узлы кривой,
|
||
// и за крайними узлами WDSP обрывает АЧХ (см. WDSPEngine.PushTXEQProfile).
|
||
MakeLbl(Grp, 'Points are curve nodes: the response is interpolated in dB '
|
||
+ 'between them and cut off beyond the outer ones.',
|
||
PAD, R1 + 332, GRP_W - 2 * PAD);
|
||
MakeLbl(Grp, 'Keep the lowest node at or below the TX filter low cut and the '
|
||
+ 'highest at or above the high cut, or the EQ will clip the passband.',
|
||
PAD, R1 + 352, GRP_W - 2 * PAD);
|
||
MakeLbl(Grp, 'In 3-band mode the frequencies are fixed by WDSP '
|
||
+ '(150 / 1500 / 6000 Hz) — only the gains move.',
|
||
PAD, R1 + 372, GRP_W - 2 * PAD);
|
||
|
||
FTXSubPage[1].Height := DpiScale(434 + 12);
|
||
|
||
// =========================================================================
|
||
// Подвкладка 2 — Hardware: свойства устройства, профилем не переключаются
|
||
// =========================================================================
|
||
SubPage(2, SubTop);
|
||
|
||
Grp := MakeGroupPanel(FTXSubPage[2], 'Radio', MARGIN, 0, GRP_W, 108);
|
||
|
||
MakeLbl(Grp, 'ATT on TX (dB)', PAD, R1 + 5, LW);
|
||
FEdTXAttOnTX := MkSpin(Grp, CX, R1, 0, 31, 31);
|
||
MakeLbl(Grp, 'Tune tone (Hz)', PAD + 320, R1 + 5, 110);
|
||
FEdTUNFreq := MkSpin(Grp, PAD + 320 + 120, R1, 100, 5000, 750);
|
||
MakeLbl(Grp, 'ATT on TX attenuates the RX ADC while transmitting (input '
|
||
+ 'protection) — not to be confused with output power.',
|
||
PAD, R1 + 32, GRP_W - 2 * PAD);
|
||
|
||
Y := 108 + 12;
|
||
Grp := MakeGroupPanel(FTXSubPage[2], 'TX Filter (DSP)', MARGIN, Y, GRP_W, 190);
|
||
|
||
MakeLbl(Grp, 'FIR length', PAD, R1 + 5, LW);
|
||
FCmbTXFiltNC := MkCmb(Grp, CX, R1, 140);
|
||
FCmbTXFiltNC.Items.AddStrings(['256','512','1024','2048','4096']);
|
||
FCmbTXFiltNC.ItemIndex := 2;
|
||
|
||
MakeLbl(Grp, 'Phase mode', PAD, R1 + STEP + 5, LW);
|
||
FCmbTXFiltMP := MkCmb(Grp, CX, R1 + STEP, 200);
|
||
FCmbTXFiltMP.Items.AddStrings(['Linear Phase','Low Latency']);
|
||
FCmbTXFiltMP.ItemIndex := 1;
|
||
|
||
MakeLbl(Grp, 'Window', PAD, R1 + 2 * STEP + 5, LW);
|
||
FCmbTXFiltWindow := MkCmb(Grp, CX, R1 + 2 * STEP, 240);
|
||
for i := 0 to High(WINDOW_NAMES) do
|
||
FCmbTXFiltWindow.Items.Add(WINDOW_NAMES[i]);
|
||
FCmbTXFiltWindow.ItemIndex := 1;
|
||
|
||
MakeLbl(Grp, 'Skirt steepness and path latency, not timbre — hence outside '
|
||
+ 'the profile.', PAD, R1 + 3 * STEP + 5, GRP_W - 2 * PAD);
|
||
|
||
Inc(Y, 190 + 12);
|
||
Grp := MakeGroupPanel(FTXSubPage[2], 'FM / CTCSS', MARGIN, Y, GRP_W, 250);
|
||
|
||
MakeLbl(Grp, 'FM deviation (Hz)', PAD, R1 + 5, LW);
|
||
FEdTXFMDev := MkFloat(Grp, CX, R1, 5000.0, 1000.0, 25000.0, 100.0);
|
||
|
||
MakeLbl(Grp, 'FM AF low cut', PAD, R1 + STEP + 5, LW);
|
||
FEdTXFMLow := MkSpin(Grp, CX, R1 + STEP, 0, 5000, 300);
|
||
MakeLbl(Grp, 'FM AF high cut', PAD + 320, R1 + STEP + 5, 100);
|
||
FEdTXFMHigh := MkSpin(Grp, PAD + 320 + 110, R1 + STEP, 100, 8000, 3000);
|
||
|
||
MakeLbl(Grp, 'FM emphasis', PAD, R1 + 2 * STEP + 5, LW);
|
||
FCmbTXFMEmph := MkCmb(Grp, CX, R1 + 2 * STEP, 180);
|
||
FCmbTXFMEmph.Items.AddStrings(['Pre-EQ','Post-EQ']);
|
||
FCmbTXFMEmph.ItemIndex := 1;
|
||
|
||
FChkTXCTCSSOn := MkChk(Grp, PAD, R1 + 3 * STEP, 220, 'CTCSS tone');
|
||
MakeLbl(Grp, 'Freq (Hz)', PAD + 230, R1 + 3 * STEP + 5, 80);
|
||
FEdTXCTCSSFreq := MkFloat(Grp, PAD + 320, R1 + 3 * STEP, 100.0, 50.0, 300.0, 0.1);
|
||
|
||
MakeLbl(Grp, 'A property of the channel or repeater — a voice profile does '
|
||
+ 'not switch it.', PAD, R1 + 4 * STEP + 5, GRP_W - 2 * PAD);
|
||
|
||
FTXSubPage[2].Height := DpiScale(Y + 250 + 12);
|
||
|
||
// =========================================================================
|
||
// Подвкладка 3 — Display: анализатор и сетка TX-спектра
|
||
// =========================================================================
|
||
SubPage(3, SubTop);
|
||
|
||
Grp := MakeGroupPanel(FTXSubPage[3], 'TX Display (Analyzer)', MARGIN, 0, 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) ----
|
||
Y := 318 + 12;
|
||
Grp := MakeGroupPanel(FTXSubPage[3], 'TX Grid', MARGIN, Y, GRP_W, 170);
|
||
|
||
MakeLbl(Grp, 'Reference level', PAD, R1 + 5, LW);
|
||
FEdTXRefLevel := TFlatEdit.Create(Self);
|
||
FEdTXRefLevel.Parent := Grp;
|
||
FEdTXRefLevel.SetBounds(DpiScale(CX), DpiScale(R1), DpiScale(96), DpiScale(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(DpiScale(CX), DpiScale(R1 + STEP), DpiScale(96), DpiScale(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;
|
||
|
||
FTXSubPage[3].Height := DpiScale(Y + 170 + 12);
|
||
|
||
FTXSubIdx := -1;
|
||
SelectTXSubPage(0);
|
||
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;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Transmit: подвкладки и полоса TX-профиля
|
||
// ---------------------------------------------------------------------------
|
||
|
||
procedure TSettingsForm.SelectTXSubPage(Idx: Integer);
|
||
var i: Integer;
|
||
begin
|
||
if (Idx < 0) or (Idx >= TXTAB_COUNT) then Exit;
|
||
FTXSubIdx := Idx;
|
||
for i := 0 to TXTAB_COUNT - 1 do
|
||
begin
|
||
if FTXSubPage[i] <> nil then FTXSubPage[i].Visible := (i = Idx);
|
||
if FTXSubBtn[i] <> nil then FTXSubBtn[i].Active := (i = Idx);
|
||
end;
|
||
end;
|
||
|
||
procedure TSettingsForm.TXSubTabClick(Sender: TObject);
|
||
begin
|
||
if Sender is TFlatButton then SelectTXSubPage(TFlatButton(Sender).Tag);
|
||
end;
|
||
|
||
procedure TSettingsForm.OnTXProfileComboChange(Sender: TObject);
|
||
begin
|
||
if FLoading then Exit;
|
||
if (FCmbTXProfile = nil) or (FCmbTXProfile.ItemIndex < 0) then Exit;
|
||
// Применение снимка в эфир — снаружи (контроллер); назад придёт LoadTXSettings
|
||
// с уже новыми значениями, форма перерисуется целиком.
|
||
if Assigned(FOnTXProfileSelect) then FOnTXProfileSelect(FCmbTXProfile.ItemIndex);
|
||
end;
|
||
|
||
procedure TSettingsForm.BtnTXProfNewClick(Sender: TObject);
|
||
var S: string;
|
||
begin
|
||
if FCmbTXProfile = nil then Exit;
|
||
if FCmbTXProfile.Items.Count >= TXPROF_MAX then
|
||
begin
|
||
MessageDlg('TX Profile',
|
||
Format('No more than %d profiles per radio.', [TXPROF_MAX]),
|
||
mtInformation, [mbOK], 0);
|
||
Exit;
|
||
end;
|
||
S := 'Profile ' + IntToStr(FCmbTXProfile.Items.Count + 1);
|
||
if not InputQuery('TX Profile', 'Name for the new profile (copy of the current sound):', S) then Exit;
|
||
if Trim(S) = '' then Exit;
|
||
if Assigned(FOnTXProfileAdd) then FOnTXProfileAdd(Trim(S));
|
||
end;
|
||
|
||
procedure TSettingsForm.BtnTXProfRenClick(Sender: TObject);
|
||
var S: string; Idx: Integer;
|
||
begin
|
||
if FCmbTXProfile = nil then Exit;
|
||
Idx := FCmbTXProfile.ItemIndex;
|
||
if (Idx < 0) or (Idx >= FCmbTXProfile.Items.Count) then Exit;
|
||
S := FCmbTXProfile.Items[Idx];
|
||
if not InputQuery('TX Profile', 'New profile name:', S) then Exit;
|
||
if Trim(S) = '' then Exit;
|
||
if Assigned(FOnTXProfileRename) then FOnTXProfileRename(Idx, Trim(S));
|
||
end;
|
||
|
||
procedure TSettingsForm.BtnTXProfDelClick(Sender: TObject);
|
||
var Idx: Integer;
|
||
begin
|
||
if FCmbTXProfile = nil then Exit;
|
||
Idx := FCmbTXProfile.ItemIndex;
|
||
// Последний профиль не удаляем — активному профилю всегда есть куда писать.
|
||
if (Idx < 0) or (FCmbTXProfile.Items.Count <= 1) then Exit;
|
||
if MessageDlg('TX Profile',
|
||
'Delete profile "' + FCmbTXProfile.Items[Idx] + '"?',
|
||
mtConfirmation, [mbYes, mbNo], 0) <> mrYes then Exit;
|
||
if Assigned(FOnTXProfileDelete) then FOnTXProfileDelete(Idx);
|
||
end;
|
||
|
||
procedure TSettingsForm.BtnTXProfFactoryClick(Sender: TObject);
|
||
// Пересоздать заводской набор. Нужен не только «на случай чего»: заводские
|
||
// профили пишутся в конфиг один раз при первом подключении устройства, и без
|
||
// этой кнопки обновлённые заводские настройки (например кривые EQ у ESSB)
|
||
// не доехали бы до тех, у кого секция tx_profiles уже записана.
|
||
begin
|
||
if MessageDlg('TX Profile',
|
||
'Replace the whole profile list with the factory set?'#13#10
|
||
+ 'Your own profiles and edits to the stock ones will be lost.'#13#10
|
||
+ '"Default" becomes active and restores the stock sound: '
|
||
+ 'bandwidth, dynamics and a flat equalizer. Microphone wiring, '
|
||
+ 'its gain and drive are kept as they are.',
|
||
mtConfirmation, [mbYes, mbNo], 0) <> mrYes then Exit;
|
||
if Assigned(FOnTXProfileFactory) then FOnTXProfileFactory(Self);
|
||
end;
|
||
|
||
procedure TSettingsForm.LoadTXProfiles(const Names: array of string;
|
||
ActiveIdx, DrivePct: Integer);
|
||
var
|
||
i: Integer;
|
||
WasLoading: Boolean;
|
||
begin
|
||
if FCmbTXProfile = nil then Exit;
|
||
WasLoading := FLoading;
|
||
FLoading := True; // заполнение списка не должно шибать OnChange
|
||
try
|
||
FCmbTXProfile.Items.Clear;
|
||
for i := 0 to High(Names) do FCmbTXProfile.Items.Add(Names[i]);
|
||
if (ActiveIdx >= 0) and (ActiveIdx < FCmbTXProfile.Items.Count) then
|
||
FCmbTXProfile.ItemIndex := ActiveIdx;
|
||
if FBtnTXProfDel <> nil then
|
||
FBtnTXProfDel.Enabled := FCmbTXProfile.Items.Count > 1;
|
||
if FLblTXProfDrive <> nil then
|
||
FLblTXProfDrive.Caption := Format('Profile drive: %d %%', [DrivePct]);
|
||
finally
|
||
FLoading := WasLoading;
|
||
end;
|
||
end;
|
||
|
||
procedure TSettingsForm.SetTXProfileDrive(DrivePct: Integer);
|
||
begin
|
||
if FLblTXProfDrive <> nil then
|
||
FLblTXProfDrive.Caption := Format('Profile drive: %d %%', [DrivePct]);
|
||
end;
|
||
|
||
procedure TSettingsForm.ShowTransmitPage(SubIdx: Integer = 0);
|
||
begin
|
||
SelectPage(FPageTransmit, FNavTransmit);
|
||
SelectTXSubPage(SubIdx);
|
||
Show;
|
||
BringToFront;
|
||
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(DpiScale(ALeft), DpiScale(ATop), DpiScale(ED_W), DpiScale(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(DpiScale(MARGIN), DpiScale(18), DpiScale(400), DpiScale(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(DpiScale(MARGIN), DpiScale(48), DpiScale(620), DpiScale(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(DpiScale(GRP_PAD), DpiScale(R1 + 6), DpiScale(132), DpiScale(18));
|
||
Lbl.Font.Color := CLR_TEXTDIM;
|
||
Lbl.Font.Name := UI_FONT;
|
||
Lbl.Font.Size := 9;
|
||
|
||
Spin := TFlatSpinEdit.Create(Self);
|
||
Spin.Parent := Grp;
|
||
Spin.SetBounds(DpiScale(GRP_PAD + 150), DpiScale(R1), DpiScale(ED_W), DpiScale(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(DpiScale(GRP_PAD + 320), DpiScale(R1 + 6), DpiScale(180), DpiScale(18));
|
||
Lbl.Font.Color := CLR_TEXTDIM;
|
||
Lbl.Font.Name := UI_FONT;
|
||
Lbl.Font.Size := 9;
|
||
|
||
FEdPlutoTxMaxAtt := TFlatFloatSpinEdit.Create(Self);
|
||
FEdPlutoTxMaxAtt.Parent := Grp;
|
||
FEdPlutoTxMaxAtt.SetBounds(DpiScale(GRP_PAD + 510), DpiScale(R1), DpiScale(ED_W), DpiScale(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(DpiScale(GRP_PAD), DpiScale(R1 + ROW_H + 12), DpiScale(320), DpiScale(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(DpiScale(X), DpiScale(Y + 5), DpiScale(LBL_W), DpiScale(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(DpiScale(GRP_PAD), DpiScale(R1), DpiScale(420), DpiScale(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(DpiScale(X), DpiScale(Y + 5), DpiScale(VHF_LBL_W), DpiScale(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.BuildCalibrationTab;
|
||
// Вкладка Calibration (паритет с Thetis-Enhanced): модель детектора мощности
|
||
// Alex-моста + множитель FWD/REV на банд (Det. Cal.), калибровка напряжения/
|
||
// тока питания (PAVoltCal/AmpVoff/AmpSens) и офсет S-метра на диапазон и на
|
||
// слот трансвертера. Позиции/подписи/видимость band-строк ставит RelayoutCalTab
|
||
// под активное железо — здесь только создание контролов.
|
||
const
|
||
GRP3_H0 = 200; // стартовая высота S-метра, уточняется в RelayoutCalTab
|
||
|
||
// Длинные подсказки не влезают в ширину группы — переводим на несколько
|
||
// строк (TLabel по умолчанию AutoSize=True и режется по краю).
|
||
procedure WrapLbl(L: TLabel; AW, AH: Integer);
|
||
begin
|
||
if L = nil then Exit;
|
||
L.AutoSize := False;
|
||
L.WordWrap := True;
|
||
L.Width := DpiScale(AW);
|
||
L.Height := DpiScale(AH);
|
||
end;
|
||
|
||
function MakeCalSpin(AParent: TWinControl; ALeft, ATop: Integer;
|
||
AMin, AMax, AInc: Double; ADec: Integer;
|
||
ADefault: Double): TFlatFloatSpinEdit;
|
||
var E: TFlatFloatSpinEdit;
|
||
begin
|
||
E := TFlatFloatSpinEdit.Create(Self);
|
||
E.Parent := AParent;
|
||
E.SetBounds(DpiScale(ALeft), DpiScale(ATop), DpiScale(CAL_ED_W), DpiScale(BTN_H + 2));
|
||
E.Color := CLR_INPUT;
|
||
E.Font.Color := CLR_INPUT_TEXT;
|
||
E.Font.Name := UI_FONT;
|
||
E.Font.Size := 9;
|
||
E.MinValue := AMin;
|
||
E.MaxValue := AMax;
|
||
E.Increment := AInc;
|
||
E.DecimalPlaces := ADec;
|
||
E.Value := ADefault;
|
||
E.OnChange := OnCalAnyChange;
|
||
Result := E;
|
||
end;
|
||
|
||
var
|
||
Grp: TPanel;
|
||
Lbl: TLabel;
|
||
i, GTop: Integer;
|
||
begin
|
||
// Гард: SetItemIndex у TFlatComboBox стреляет OnChange и при программном
|
||
// присвоении — без гарда OnCalAnyChange дёрнется из «ItemIndex := 0» ниже,
|
||
// когда остальные контролы вкладки ещё nil → AV в CollectCalFromUI.
|
||
FLoading := True;
|
||
FCalIsPluto := False;
|
||
for i := 0 to CFG_XVTR_COUNT - 1 do
|
||
begin
|
||
FCalXvtrOn[i] := False;
|
||
FCalXvtrName[i] := '';
|
||
end;
|
||
|
||
with TLabel.Create(Self) do
|
||
begin
|
||
Parent := FPageCalib;
|
||
Caption := 'Calibration';
|
||
SetBounds(DpiScale(CAL_MARGIN), DpiScale(18), DpiScale(400), DpiScale(28));
|
||
Font.Name := UI_FONT;
|
||
Font.Size := 16;
|
||
Font.Style := [fsBold];
|
||
Font.Color := CLR_TEXT;
|
||
end;
|
||
|
||
with TLabel.Create(Self) do
|
||
begin
|
||
Parent := FPageCalib;
|
||
Caption := 'Power/SWR detector, supply voltage/current and S-meter calibration.';
|
||
SetBounds(DpiScale(CAL_MARGIN), DpiScale(48), DpiScale(620), DpiScale(20));
|
||
Font.Name := UI_FONT;
|
||
Font.Size := 9;
|
||
Font.Color := CLR_TEXTDIM;
|
||
end;
|
||
|
||
// ── Power / SWR detector ────────────────────────────────────────────────
|
||
GTop := 86;
|
||
FCalGrpDet := MakeGroupPanel(FPageCalib, 'Power / SWR detector',
|
||
CAL_MARGIN, GTop, CAL_GRP_W, CAL_GRP1_H);
|
||
Grp := FCalGrpDet;
|
||
|
||
Lbl := MakeLbl(Grp, 'Detector model', CAL_PAD, CAL_R1 + 6, 132);
|
||
FCmbPwrDetModel := MakeCombo(Grp, CAL_PAD + 150, CAL_R1, 280, OnCalAnyChange);
|
||
FCmbPwrDetModel.Items.Add('ANAN-10/100/100B/100D (3.3V)');
|
||
FCmbPwrDetModel.Items.Add('ANAN-200D / Orion');
|
||
FCmbPwrDetModel.Items.Add('ANAN-7000DLE / Anvelina Pro 3 / G2');
|
||
FCmbPwrDetModel.Items.Add('ANAN-8000DLE / Orion MkII');
|
||
FCmbPwrDetModel.ItemIndex := 0;
|
||
|
||
Lbl := MakeLbl(Grp,
|
||
'FWD/REV multiplier per band: 1.00 = no correction, >1 if meter under-reads '
|
||
+ '(e.g. 88 W shown at real 100 W -> 1.14). SWR is not affected.',
|
||
CAL_PAD, CAL_R1 + CAL_ROW_H + 8, 650);
|
||
WrapLbl(Lbl, 650, 34);
|
||
|
||
for i := 0 to CFG_BAND_COUNT - 1 do
|
||
begin
|
||
FLblPADetCal[i] := MakeLbl(Grp, '', 0, 0, CAL_LBL_W);
|
||
FEdPADetCal[i] := MakeCalSpin(Grp, 0, 0, 0.50, 2.00, 0.01, 2, 1.00);
|
||
FEdPADetCal[i].Tag := i;
|
||
end;
|
||
|
||
// ── Supply voltage / current ────────────────────────────────────────────
|
||
GTop := GTop + CAL_GRP1_H + 14;
|
||
FCalGrpVA := MakeGroupPanel(FPageCalib, 'Supply voltage / current',
|
||
CAL_MARGIN, GTop, CAL_GRP_W, CAL_GRP2_H);
|
||
Grp := FCalGrpVA;
|
||
|
||
Lbl := MakeLbl(Grp, 'Voltage multiplier (1.000 = no correction)',
|
||
CAL_PAD, CAL_R1 + 6, CAL_VA_LBL_W);
|
||
FEdSupplyVoltCal := MakeCalSpin(Grp, CAL_PAD + CAL_VA_LBL_W + 10, CAL_R1,
|
||
0.100, 5.000, 0.001, 3, 1.000);
|
||
|
||
Lbl := MakeLbl(Grp, 'Current sensor zero offset (mV)',
|
||
CAL_PAD, CAL_R1 + CAL_ROW_H + 6, CAL_VA_LBL_W);
|
||
FEdAmpVoff := MakeCalSpin(Grp, CAL_PAD + CAL_VA_LBL_W + 10, CAL_R1 + CAL_ROW_H,
|
||
0.0, 2000.0, 1.0, 0, 360.0);
|
||
|
||
Lbl := MakeLbl(Grp, 'Current sensor sensitivity (mV/A)',
|
||
CAL_PAD, CAL_R1 + 2 * CAL_ROW_H + 6, CAL_VA_LBL_W);
|
||
FEdAmpSens := MakeCalSpin(Grp, CAL_PAD + CAL_VA_LBL_W + 10, CAL_R1 + 2 * CAL_ROW_H,
|
||
1.0, 1000.0, 0.1, 1, 120.0);
|
||
|
||
Lbl := MakeLbl(Grp, 'Defaults 360 mV / 120 mV/A (ACS712-style, Orion MkII)',
|
||
CAL_PAD + CAL_VA_LBL_W + 10 + CAL_ED_W + 14,
|
||
CAL_R1 + CAL_ROW_H + 6, 320);
|
||
WrapLbl(Lbl, 320, 34);
|
||
|
||
// ── S-meter ─────────────────────────────────────────────────────────────
|
||
GTop := GTop + CAL_GRP2_H + 14;
|
||
FCalGrpSM := MakeGroupPanel(FPageCalib, 'S-meter',
|
||
CAL_MARGIN, GTop, CAL_GRP_W, GRP3_H0);
|
||
Grp := FCalGrpSM;
|
||
|
||
FLblSMBandHdr := MakeLbl(Grp,
|
||
'Offset (dB) added to S-meter readings — per band (+ = meter reads higher).',
|
||
CAL_PAD, 30, 540);
|
||
WrapLbl(FLblSMBandHdr, 540, 18);
|
||
|
||
for i := 0 to CFG_BAND_COUNT - 1 do
|
||
begin
|
||
FLblSMBand[i] := MakeLbl(Grp, '', 0, 0, CAL_SM_LBL_W);
|
||
FEdSMBand[i] := MakeCalSpin(Grp, 0, 0, -30.0, 30.0, 0.1, 1, 0.0);
|
||
FEdSMBand[i].Tag := i;
|
||
end;
|
||
|
||
FLblSMXvtrHdr := MakeLbl(Grp, 'Per transverter slot', CAL_PAD, 0, 300);
|
||
|
||
for i := 0 to CFG_XVTR_COUNT - 1 do
|
||
begin
|
||
FLblSMXvtr[i] := MakeLbl(Grp, '', 0, 0, CAL_SM_LBL_W);
|
||
FLblSMXvtr[i].AutoSize := False;
|
||
FLblSMXvtr[i].ShowHint := True;
|
||
FEdSMXvtr[i] := MakeCalSpin(Grp, 0, 0, -30.0, 30.0, 0.1, 1, 0.0);
|
||
FEdSMXvtr[i].Tag := i;
|
||
end;
|
||
|
||
// ── Spectrum / waterfall ────────────────────────────────────────────────
|
||
// Отдельная от S-метра калибровка (Display Cal Offset в Thetis): у display-
|
||
// тракта своё окно/детектор, поэтому уровень трассы и показания стрелки
|
||
// расходятся на пару дБ. Сдвиг двигает ВСЮ трассу (и шумовую полку, и
|
||
// сигналы) — это калибровка уровня, а не подавление шума.
|
||
FCalGrpDisp := MakeGroupPanel(FPageCalib, 'Spectrum / waterfall',
|
||
CAL_MARGIN, GTop, CAL_GRP_W, GRP3_H0);
|
||
Grp := FCalGrpDisp;
|
||
|
||
FLblDispBandHdr := MakeLbl(Grp,
|
||
'Offset (dB) added to spectrum/waterfall — per band (moves the whole trace).',
|
||
CAL_PAD, 30, 540);
|
||
WrapLbl(FLblDispBandHdr, 540, 18);
|
||
|
||
FBtnDispFromSM := TFlatButton.Create(Self);
|
||
FBtnDispFromSM.Parent := Grp;
|
||
FBtnDispFromSM.Caption := '= S-meter';
|
||
FBtnDispFromSM.SetBounds(DpiScale(CAL_GRP_W - 130), DpiScale(24), DpiScale(112), DpiScale(BTN_H));
|
||
FBtnDispFromSM.OnClick := OnCalDispFromSMClick;
|
||
FBtnDispFromSM.ClrNorm := CLR_PANEL;
|
||
FBtnDispFromSM.ClrBorder := TColor($00404040);
|
||
FBtnDispFromSM.ClrHot := TColor($00303030);
|
||
FBtnDispFromSM.ClrActive := TColor($00003300);
|
||
FBtnDispFromSM.ClrText := CLR_TEXT;
|
||
FBtnDispFromSM.ClrTextAct := CLR_ACCENT;
|
||
FBtnDispFromSM.Font.Name := UI_FONT;
|
||
FBtnDispFromSM.Font.Size := 9;
|
||
FBtnDispFromSM.Hint := 'Copy the S-meter offsets into this table';
|
||
FBtnDispFromSM.ShowHint := True;
|
||
|
||
for i := 0 to CFG_BAND_COUNT - 1 do
|
||
begin
|
||
FLblDispBand[i] := MakeLbl(Grp, '', 0, 0, CAL_SM_LBL_W);
|
||
FEdDispBand[i] := MakeCalSpin(Grp, 0, 0, -30.0, 30.0, 0.1, 1, 0.0);
|
||
FEdDispBand[i].Tag := i;
|
||
end;
|
||
|
||
FLblDispXvtrHdr := MakeLbl(Grp, 'Per transverter slot', CAL_PAD, 0, 300);
|
||
|
||
for i := 0 to CFG_XVTR_COUNT - 1 do
|
||
begin
|
||
FLblDispXvtr[i] := MakeLbl(Grp, '', 0, 0, CAL_SM_LBL_W);
|
||
FLblDispXvtr[i].AutoSize := False;
|
||
FLblDispXvtr[i].ShowHint := True;
|
||
FEdDispXvtr[i] := MakeCalSpin(Grp, 0, 0, -30.0, 30.0, 0.1, 1, 0.0);
|
||
FEdDispXvtr[i].Tag := i;
|
||
end;
|
||
|
||
// ── Reference frequency ─────────────────────────────────────────────────
|
||
// Одна поправка опорника на всё железо (Frequency Calibration в Thetis):
|
||
// у openHPSDR правится частотное слово DDC/DUC, у AD936x — xo_correction.
|
||
// Ручные LO Error / TO Error трансвертеров остаются сверху: тракт тракту
|
||
// рознь, и где-то уход больше, где-то меньше.
|
||
FCalGrpFreq := MakeGroupPanel(FPageCalib, 'Reference frequency',
|
||
CAL_MARGIN, GTop, CAL_GRP_W, CAL_GRP2_H);
|
||
Grp := FCalGrpFreq;
|
||
|
||
Lbl := MakeLbl(Grp, 'Oscillator error (ppm)', CAL_PAD, CAL_R1 + 6, CAL_VA_LBL_W);
|
||
FEdFreqCalPPM := MakeCalSpin(Grp, CAL_PAD + CAL_VA_LBL_W + 10, CAL_R1,
|
||
-100.0, 100.0, 0.01, 3, 0.0);
|
||
|
||
Lbl := MakeLbl(Grp,
|
||
'Positive value raises displayed frequencies. Measure against a known '
|
||
+ 'reference (beacon, WWV, GPSDO): if a 10 MHz signal reads 10 Hz low, '
|
||
+ 'that is +1.0 ppm. Applies to RX and TX on every band; per-transverter '
|
||
+ 'LO Error / TO Error still apply on top.',
|
||
CAL_PAD, CAL_R1 + CAL_ROW_H + 6, 650);
|
||
WrapLbl(Lbl, 650, 52);
|
||
|
||
RelayoutCalTab; // стартовая раскладка (HF-план, трансвертеры скрыты)
|
||
|
||
FLoading := False;
|
||
end;
|
||
|
||
procedure TSettingsForm.RelayoutCalTab;
|
||
// Показывает только то, что есть у активного железа: band-план (HF у openHPSDR,
|
||
// VHF/UHF у Pluto), из трансвертеров — включённые слоты. Детектор мощности и
|
||
// датчики питания — свойства Alex-моста openHPSDR, у AD936x такой телеметрии
|
||
// нет, поэтому обе группы прячутся, а S-метр поднимается наверх.
|
||
var
|
||
i, r, c, X, Y, GTop, HSM, HDisp: Integer;
|
||
Nm: string;
|
||
begin
|
||
// Гард: вкладка ещё строится (или LoadXvtrSettings пришёл раньше неё).
|
||
if (FCalGrpDet = nil) or (FCalGrpVA = nil) or (FCalGrpSM = nil)
|
||
or (FCalGrpDisp = nil) or (FLblDispXvtrHdr = nil)
|
||
or (FCalGrpFreq = nil) then Exit;
|
||
|
||
// TScrollBox двигает детей при прокрутке, и SetBounds ниже задаёт координаты
|
||
// ОТНОСИТЕЛЬНО прокрученного клиента. Если страницу успели прокрутить (у
|
||
// openHPSDR она длиннее на две группы), после перекладки прокрутка
|
||
// схлопывается — и группы остаются смещёнными вниз ровно на прежний скролл.
|
||
// Поэтому сначала возвращаем страницу в ноль.
|
||
FPageCalib.VertScrollBar.Position := 0;
|
||
FPageCalib.HorzScrollBar.Position := 0;
|
||
|
||
// ---- подписи/видимость строк Det. Cal. (своя сетка, уже с подсказкой) ----
|
||
for i := 0 to CFG_BAND_COUNT - 1 do
|
||
begin
|
||
Nm := BandName(i, FCalIsPluto);
|
||
r := i div CAL_COLS;
|
||
c := i mod CAL_COLS;
|
||
X := CAL_PAD + c * (CAL_LBL_W + CAL_ED_W + 18);
|
||
Y := CAL_R1 + CAL_ROW_H + 46 + r * CAL_ROW_H;
|
||
if FLblPADetCal[i] <> nil then
|
||
begin
|
||
FLblPADetCal[i].Caption := Nm + ':';
|
||
FLblPADetCal[i].SetBounds(DpiScale(X), DpiScale(Y + 5), DpiScale(CAL_LBL_W), DpiScale(18));
|
||
FLblPADetCal[i].Visible := Nm <> '';
|
||
end;
|
||
if FEdPADetCal[i] <> nil then
|
||
begin
|
||
FEdPADetCal[i].SetBounds(DpiScale(X + CAL_LBL_W), DpiScale(Y), DpiScale(CAL_ED_W), DpiScale(BTN_H + 2));
|
||
FEdPADetCal[i].Visible := Nm <> '';
|
||
end;
|
||
end;
|
||
|
||
// ---- таблицы офсетов (S-метр и спектр) — одинаковая геометрия ----
|
||
HSM := LayoutCalOffsets(FLblSMBand, FEdSMBand, FLblSMXvtrHdr,
|
||
FLblSMXvtr, FEdSMXvtr, 52);
|
||
HDisp := LayoutCalOffsets(FLblDispBand, FEdDispBand, FLblDispXvtrHdr,
|
||
FLblDispXvtr, FEdDispXvtr, 52);
|
||
|
||
// ---- вертикальная укладка групп ----
|
||
GTop := 86;
|
||
FCalGrpDet.Visible := not FCalIsPluto;
|
||
FCalGrpVA.Visible := not FCalIsPluto;
|
||
if not FCalIsPluto then
|
||
begin
|
||
FCalGrpDet.SetBounds(DpiScale(CAL_MARGIN), DpiScale(GTop), DpiScale(CAL_GRP_W), DpiScale(CAL_GRP1_H));
|
||
GTop := GTop + CAL_GRP1_H + 14;
|
||
FCalGrpVA.SetBounds(DpiScale(CAL_MARGIN), DpiScale(GTop), DpiScale(CAL_GRP_W), DpiScale(CAL_GRP2_H));
|
||
GTop := GTop + CAL_GRP2_H + 14;
|
||
end;
|
||
FCalGrpSM.SetBounds(DpiScale(CAL_MARGIN), DpiScale(GTop), DpiScale(CAL_GRP_W), DpiScale(HSM));
|
||
GTop := GTop + HSM + 14;
|
||
FCalGrpDisp.SetBounds(DpiScale(CAL_MARGIN), DpiScale(GTop), DpiScale(CAL_GRP_W), DpiScale(HDisp));
|
||
GTop := GTop + HDisp + 14;
|
||
FCalGrpFreq.SetBounds(DpiScale(CAL_MARGIN), DpiScale(GTop), DpiScale(CAL_GRP_W), DpiScale(CAL_GRP2_H));
|
||
end;
|
||
|
||
function TSettingsForm.LayoutCalOffsets(const BandLbl: array of TLabel;
|
||
const BandEd: array of TFlatFloatSpinEdit; XvtrHdr: TLabel;
|
||
const XvtrLbl: array of TLabel; const XvtrEd: array of TFlatFloatSpinEdit;
|
||
Y0: Integer): Integer;
|
||
// Строки диапазонов стоят по индексу плана (пустой слот просто скрыт),
|
||
// слоты трансвертеров — только включённые и без дырок.
|
||
var
|
||
i, r, c, X, Y, Slot, BandRows, XvtrRows: Integer;
|
||
Nm: string;
|
||
begin
|
||
BandRows := 0;
|
||
for i := 0 to CFG_BAND_COUNT - 1 do
|
||
begin
|
||
Nm := BandName(i, FCalIsPluto);
|
||
r := i div CAL_COLS;
|
||
c := i mod CAL_COLS;
|
||
X := CAL_PAD + c * (CAL_SM_LBL_W + CAL_ED_W + 14);
|
||
Y := Y0 + r * CAL_ROW_H;
|
||
if BandLbl[i] <> nil then
|
||
begin
|
||
BandLbl[i].Caption := Nm + ':';
|
||
BandLbl[i].SetBounds(DpiScale(X), DpiScale(Y + 5), DpiScale(CAL_SM_LBL_W), DpiScale(18));
|
||
BandLbl[i].Visible := Nm <> '';
|
||
end;
|
||
if BandEd[i] <> nil then
|
||
begin
|
||
BandEd[i].SetBounds(DpiScale(X + CAL_SM_LBL_W), DpiScale(Y), DpiScale(CAL_ED_W), DpiScale(BTN_H + 2));
|
||
BandEd[i].Visible := Nm <> '';
|
||
end;
|
||
if Nm <> '' then BandRows := r + 1;
|
||
end;
|
||
|
||
Y := Y0 + BandRows * CAL_ROW_H + 6;
|
||
XvtrHdr.SetBounds(DpiScale(CAL_PAD), DpiScale(Y), DpiScale(300), DpiScale(18));
|
||
Slot := 0;
|
||
for i := 0 to CFG_XVTR_COUNT - 1 do
|
||
begin
|
||
if FCalXvtrOn[i] then
|
||
begin
|
||
r := Slot div CAL_COLS;
|
||
c := Slot mod CAL_COLS;
|
||
X := CAL_PAD + c * (CAL_SM_LBL_W + CAL_ED_W + 14);
|
||
if FCalXvtrName[i] <> '' then Nm := FCalXvtrName[i]
|
||
else Nm := 'XVTR' + IntToStr(i);
|
||
XvtrLbl[i].Caption := Nm + ':';
|
||
XvtrLbl[i].Hint := 'XVTR' + IntToStr(i) + ' (' + Nm + ')';
|
||
XvtrLbl[i].SetBounds(DpiScale(X), DpiScale(Y + 24 + r * CAL_ROW_H + 5), DpiScale(CAL_SM_LBL_W), DpiScale(18));
|
||
XvtrEd[i].SetBounds(DpiScale(X + CAL_SM_LBL_W), DpiScale(Y + 24 + r * CAL_ROW_H), DpiScale(CAL_ED_W), DpiScale(BTN_H + 2));
|
||
Inc(Slot);
|
||
end;
|
||
XvtrLbl[i].Visible := FCalXvtrOn[i];
|
||
XvtrEd[i].Visible := FCalXvtrOn[i];
|
||
end;
|
||
XvtrRows := (Slot + CAL_COLS - 1) div CAL_COLS;
|
||
XvtrHdr.Visible := Slot > 0;
|
||
|
||
if Slot > 0 then Result := Y + 24 + XvtrRows * CAL_ROW_H + 12
|
||
else Result := Y + 12;
|
||
end;
|
||
|
||
procedure TSettingsForm.OnCalDispFromSMClick(Sender: TObject);
|
||
// Стартовая точка калибровки дисплея: те же цифры, что у стрелки,
|
||
// дальше доводятся руками по шумовой полке/эталонному генератору.
|
||
var i: Integer;
|
||
begin
|
||
FLoading := True;
|
||
try
|
||
for i := 0 to CFG_BAND_COUNT - 1 do
|
||
if (FEdDispBand[i] <> nil) and (FEdSMBand[i] <> nil) then
|
||
FEdDispBand[i].Value := FEdSMBand[i].Value;
|
||
for i := 0 to CFG_XVTR_COUNT - 1 do
|
||
if (FEdDispXvtr[i] <> nil) and (FEdSMXvtr[i] <> nil) then
|
||
FEdDispXvtr[i].Value := FEdSMXvtr[i].Value;
|
||
finally
|
||
FLoading := False;
|
||
end;
|
||
OnCalAnyChange(FBtnDispFromSM);
|
||
end;
|
||
|
||
procedure TSettingsForm.SetCalibrationBackend(IsPluto: Boolean);
|
||
begin
|
||
FCalIsPluto := IsPluto;
|
||
RelayoutCalTab;
|
||
end;
|
||
|
||
procedure TSettingsForm.CollectCalFromUI;
|
||
var i: Integer;
|
||
begin
|
||
FCalConfig.PwrDetModel := FCmbPwrDetModel.ItemIndex;
|
||
for i := 0 to CFG_BAND_COUNT - 1 do
|
||
begin
|
||
if FEdPADetCal[i] <> nil then FCalConfig.PADetCal[i] := FEdPADetCal[i].Value;
|
||
if FEdSMBand[i] <> nil then FCalConfig.SMeterBandDB[i] := FEdSMBand[i].Value;
|
||
if FEdDispBand[i] <> nil then FCalConfig.DispBandDB[i] := FEdDispBand[i].Value;
|
||
end;
|
||
for i := 0 to CFG_XVTR_COUNT - 1 do
|
||
begin
|
||
if FEdSMXvtr[i] <> nil then FCalConfig.SMeterXvtrDB[i] := FEdSMXvtr[i].Value;
|
||
if FEdDispXvtr[i] <> nil then FCalConfig.DispXvtrDB[i] := FEdDispXvtr[i].Value;
|
||
end;
|
||
FCalConfig.FreqCalPPM := FEdFreqCalPPM.Value;
|
||
FCalConfig.SupplyVoltCal := FEdSupplyVoltCal.Value;
|
||
FCalConfig.AmpVoffMV := FEdAmpVoff.Value;
|
||
FCalConfig.AmpSensMVA := FEdAmpSens.Value;
|
||
end;
|
||
|
||
procedure TSettingsForm.OnCalAnyChange(Sender: TObject);
|
||
begin
|
||
if FLoading then Exit;
|
||
// Смена модели детектора подставляет модельные дефолты датчика тока
|
||
// (Thetis GetDefaultVoltCalibration): ANAN-7000DLE/Anvelina Pro 3 — 340/88,
|
||
// остальные — 360/120. Правится руками после, если датчик нестандартный.
|
||
if Sender = FCmbPwrDetModel then
|
||
begin
|
||
FLoading := True;
|
||
try
|
||
if FCmbPwrDetModel.ItemIndex = 2 then
|
||
begin
|
||
FEdAmpVoff.Value := 340.0;
|
||
FEdAmpSens.Value := 88.0;
|
||
end else
|
||
begin
|
||
FEdAmpVoff.Value := 360.0;
|
||
FEdAmpSens.Value := 120.0;
|
||
end;
|
||
finally
|
||
FLoading := False;
|
||
end;
|
||
end;
|
||
CollectCalFromUI;
|
||
if Assigned(FOnCalibrationChange) then
|
||
FOnCalibrationChange(FCalConfig);
|
||
end;
|
||
|
||
procedure TSettingsForm.LoadCalibration(const C: TCalibration);
|
||
var i: Integer;
|
||
begin
|
||
FLoading := True;
|
||
try
|
||
FCalConfig := C;
|
||
FCmbPwrDetModel.ItemIndex := EnsureRange(C.PwrDetModel, 0, 3);
|
||
for i := 0 to CFG_BAND_COUNT - 1 do
|
||
begin
|
||
if FEdPADetCal[i] <> nil then FEdPADetCal[i].Value := C.PADetCal[i];
|
||
if FEdSMBand[i] <> nil then FEdSMBand[i].Value := C.SMeterBandDB[i];
|
||
if FEdDispBand[i] <> nil then FEdDispBand[i].Value := C.DispBandDB[i];
|
||
end;
|
||
for i := 0 to CFG_XVTR_COUNT - 1 do
|
||
begin
|
||
if FEdSMXvtr[i] <> nil then FEdSMXvtr[i].Value := C.SMeterXvtrDB[i];
|
||
if FEdDispXvtr[i] <> nil then FEdDispXvtr[i].Value := C.DispXvtrDB[i];
|
||
end;
|
||
FEdFreqCalPPM.Value := EnsureRange(C.FreqCalPPM, -100.0, 100.0);
|
||
FEdSupplyVoltCal.Value := C.SupplyVoltCal;
|
||
FEdAmpVoff.Value := C.AmpVoffMV;
|
||
FEdAmpSens.Value := C.AmpSensMVA;
|
||
finally
|
||
FLoading := False;
|
||
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;
|
||
Lbl: TLabel;
|
||
Y, i: Integer;
|
||
begin
|
||
with TLabel.Create(Self) do
|
||
begin
|
||
Parent := FPageAdvanced;
|
||
Caption := 'Advanced';
|
||
SetBounds(DpiScale(MARGIN), DpiScale(18), DpiScale(400), DpiScale(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(DpiScale(GRP_PAD), DpiScale(R1), DpiScale(260), DpiScale(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(DpiScale(GRP_PAD), DpiScale(R1 + ROW_H), DpiScale(260), DpiScale(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, 318);
|
||
|
||
Y := R1;
|
||
Chk := TFlatCheckBox.Create(Self);
|
||
Chk.Parent := Grp;
|
||
Chk.Caption := 'Enabled';
|
||
Chk.SetBounds(DpiScale(GRP_PAD), DpiScale(Y), DpiScale(260), DpiScale(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(DpiScale(GRP_PAD + LBL_W + 12), DpiScale(Y), DpiScale(90), DpiScale(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(DpiScale(GRP_PAD + LBL_W + 12), DpiScale(Y), DpiScale(ED_W), DpiScale(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(DpiScale(GRP_PAD + LBL_W + 12), DpiScale(Y), DpiScale(ED_W), DpiScale(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(DpiScale(GRP_PAD + LBL_W + 12), DpiScale(Y), DpiScale(ED_W), DpiScale(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;
|
||
|
||
// Потолок точек в кадре спектра/водопада, уходящем веб-клиенту. Реально
|
||
// отправляется min(этот потолок, разрешение анализатора = ширина
|
||
// панадаптера); выше — децимация max-детектором.
|
||
Y := Y + ROW_H;
|
||
MakeLbl(Grp, 'Spectrum:', GRP_PAD, Y + 4, LBL_W);
|
||
FCmbWebRes := MakeCombo(Grp, GRP_PAD + LBL_W + 12, Y, 130, OnWebAnyChange);
|
||
for i := 0 to High(DISPLAY_RES_NAMES) do FCmbWebRes.Items.Add(DISPLAY_RES_NAMES[i]);
|
||
FCmbWebRes.ItemIndex := 0;
|
||
Lbl := MakeLbl(Grp, '1024 px ~160 KB/s, 4096 px ~650 KB/s per client',
|
||
GRP_PAD + LBL_W + 12, Y + ROW_H - 8, 320);
|
||
Lbl.Font.Size := 8;
|
||
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);
|
||
var Res: Integer;
|
||
begin
|
||
if FLoading then Exit;
|
||
if not Assigned(FOnWebSettingsChange) then Exit;
|
||
if (FCmbWebRes.ItemIndex >= 0) and (FCmbWebRes.ItemIndex <= High(DISPLAY_RES_VALUES)) then
|
||
Res := DISPLAY_RES_VALUES[FCmbWebRes.ItemIndex]
|
||
else
|
||
Res := DISPLAY_RES_VALUES[0];
|
||
FOnWebSettingsChange(
|
||
FChkWebEnabled.Checked,
|
||
FEdWebPort.Value,
|
||
FEdWebBind.Text,
|
||
FEdWebUser.Text,
|
||
FEdWebPass.Text,
|
||
Res);
|
||
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; SpecPixels: Integer);
|
||
var i: Integer;
|
||
begin
|
||
FLoading := True;
|
||
try
|
||
FChkWebEnabled.Checked := Enabled;
|
||
FEdWebPort.Value := EnsureRange(Port, 1, 65535);
|
||
FEdWebBind.Text := BindAddr;
|
||
FEdWebUser.Text := User;
|
||
FEdWebPass.Text := Pass;
|
||
FCmbWebRes.ItemIndex := 0;
|
||
for i := 0 to High(DISPLAY_RES_VALUES) do
|
||
if DISPLAY_RES_VALUES[i] = SpecPixels then
|
||
begin FCmbWebRes.ItemIndex := i; Break; end;
|
||
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 if TPanel(Ctrl).Tag = TAG_PAGE_BG then
|
||
TPanel(Ctrl).Color := T.BG // подложка подвкладки, не группа
|
||
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;
|
||
FillSliceDevCombos(AudioOut, AudioIn);
|
||
end;
|
||
|
||
procedure TSettingsForm.FillSliceDevCombos(AudioOut: TAudioOutput; AudioIn: TAudioInput);
|
||
// Списки устройств в комбо вкладки Slices — те же, что у главных комбо. Текущий
|
||
// выбор держим по ИМЕНИ: порядок перечисления PortAudio между запусками плавает.
|
||
var
|
||
Names: TStringList;
|
||
Idx: array[0..63] of Integer;
|
||
Cnt, i, k: Integer;
|
||
Cur: string;
|
||
begin
|
||
if FSliceOutDev[0] = nil then Exit; // вкладка ещё не построена
|
||
Names := TStringList.Create;
|
||
FLoading := True;
|
||
try
|
||
if AudioOut <> nil then
|
||
begin
|
||
Cnt := 0; Names.Clear;
|
||
AudioOut.EnumOutputDevices(Names, Idx, Cnt);
|
||
for i := 0 to CFG_CAT_SLICE_COUNT - 1 do
|
||
begin
|
||
Cur := FSliceOutDev[i].Text;
|
||
if FSliceOutDev[i].ItemIndex = 0 then Cur := '';
|
||
FSliceOutDev[i].Items.Clear;
|
||
FSliceOutDev[i].Items.Add(SLICE_DEV_DEFAULT);
|
||
for k := 0 to Cnt - 1 do FSliceOutDev[i].Items.Add(Names[k]);
|
||
SelectDevInCombo(FSliceOutDev[i], Cur);
|
||
end;
|
||
end;
|
||
if AudioIn <> nil then
|
||
begin
|
||
Cnt := 0; Names.Clear;
|
||
AudioIn.EnumInputDevices(Names, Idx, Cnt);
|
||
for i := 0 to CFG_CAT_SLICE_COUNT - 1 do
|
||
begin
|
||
Cur := FSliceInDev[i].Text;
|
||
if FSliceInDev[i].ItemIndex = 0 then Cur := '';
|
||
FSliceInDev[i].Items.Clear;
|
||
FSliceInDev[i].Items.Add(SLICE_DEV_NONE);
|
||
for k := 0 to Cnt - 1 do FSliceInDev[i].Items.Add(Names[k]);
|
||
SelectDevInCombo(FSliceInDev[i], Cur);
|
||
end;
|
||
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(DpiScale(MARGIN), DpiScale(18), DpiScale(400), DpiScale(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(DpiScale(MARGIN), DpiScale(48), DpiScale(680), DpiScale(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(DpiScale(PAD), DpiScale(R1), DpiScale(GRP_W - PAD * 2), DpiScale(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(DpiScale(CX), DpiScale(R1 + STEP), DpiScale(EDT_W), DpiScale(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(DpiScale(PAD), DpiScale(R1 + 6 * STEP), DpiScale(GRP_W - PAD * 2), DpiScale(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(DpiScale(PAD), DpiScale(R1), DpiScale(300), DpiScale(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(DpiScale(CX), DpiScale(R1 + STEP), DpiScale(100), DpiScale(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;
|
||
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// BuildSlicesTab — CAT дополнительных слайсов (B..): свой TCP-порт на слайс.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
procedure TSettingsForm.BuildSlicesTab;
|
||
const
|
||
MARGIN = 22;
|
||
PAD = 18;
|
||
LW = 120;
|
||
CX = PAD + LW + 8;
|
||
GRP_W = 380;
|
||
GRP_H = 240;
|
||
GRP_GAP = 16;
|
||
CMB_W = 210;
|
||
R1 = 42;
|
||
STEP = 32;
|
||
var
|
||
i, col, row, gx, gy: Integer;
|
||
Grp: TPanel;
|
||
Chk: TFlatCheckBox;
|
||
Spin: TFlatSpinEdit;
|
||
Cmb: TFlatComboBox;
|
||
begin
|
||
with TLabel.Create(Self) do
|
||
begin
|
||
Parent := FPageSlices;
|
||
Caption := 'Slices';
|
||
SetBounds(DpiScale(MARGIN), DpiScale(18), DpiScale(400), DpiScale(28));
|
||
Font.Name := UI_FONT; Font.Size := 16;
|
||
Font.Style := [fsBold]; Font.Color := CLR_TEXT;
|
||
end;
|
||
with TLabel.Create(Self) do
|
||
begin
|
||
Parent := FPageSlices;
|
||
Caption := 'A separate CAT TCP port per additional slice — external software '
|
||
+ 'controls its own slice as a standalone transceiver.';
|
||
SetBounds(DpiScale(MARGIN), DpiScale(48), DpiScale(760), DpiScale(20));
|
||
Font.Name := UI_FONT; Font.Size := 9; Font.Color := CLR_TEXTDIM;
|
||
end;
|
||
with TLabel.Create(Self) do
|
||
begin
|
||
Parent := FPageSlices;
|
||
Caption := 'Slice A is the main receiver — it is configured on the CAT tab '
|
||
+ '(serial ports and the main TCP port).';
|
||
SetBounds(DpiScale(MARGIN), DpiScale(66), DpiScale(760), DpiScale(20));
|
||
Font.Name := UI_FONT; Font.Size := 9; Font.Color := CLR_TEXTDIM;
|
||
end;
|
||
|
||
for i := 0 to CFG_CAT_SLICE_COUNT - 1 do
|
||
begin
|
||
row := i div 2;
|
||
col := i mod 2;
|
||
gx := MARGIN + col * (GRP_W + GRP_GAP);
|
||
gy := 96 + row * (GRP_H + GRP_GAP);
|
||
|
||
Grp := MakeGroupPanel(FPageSlices, 'Slice ' + Chr(Ord('B') + i), gx, gy, GRP_W, GRP_H);
|
||
|
||
Chk := TFlatCheckBox.Create(Self);
|
||
Chk.Parent := Grp;
|
||
Chk.Caption := 'Enable CAT TCP server';
|
||
Chk.SetBounds(DpiScale(PAD), DpiScale(R1), DpiScale(GRP_W - PAD * 2), DpiScale(22));
|
||
Chk.Font.Color := CLR_TEXT;
|
||
Chk.Font.Name := UI_FONT; Chk.Font.Size := 9;
|
||
Chk.Tag := i;
|
||
Chk.OnChange := OnSliceCATCtrlChange;
|
||
FSliceCATEn[i] := Chk;
|
||
|
||
MakeLbl(Grp, 'Port:', PAD, R1 + STEP + 6, LW);
|
||
Spin := TFlatSpinEdit.Create(Self);
|
||
Spin.Parent := Grp;
|
||
Spin.SetBounds(DpiScale(CX), DpiScale(R1 + STEP), DpiScale(100), DpiScale(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 := CFG_CAT_SLICE_PORT0 + i;
|
||
Spin.Tag := i;
|
||
Spin.OnChange := OnSliceCATCtrlChange;
|
||
FSliceCATPort[i] := Spin;
|
||
|
||
// Auto TX = «Auto Switch TX Slice» из SmartSDR CAT: команда TX по этому
|
||
// порту сама делает слайс источником передачи. Пока кто-то уже в эфире,
|
||
// заявка игнорируется — передаёт всегда только один слайс. Подпись короткая
|
||
// (в ширину группы), пояснение — строкой ниже.
|
||
Chk := TFlatCheckBox.Create(Self);
|
||
Chk.Parent := Grp;
|
||
Chk.Caption := 'Auto TX';
|
||
Chk.SetBounds(DpiScale(PAD), DpiScale(R1 + 2 * STEP), DpiScale(GRP_W - PAD * 2), DpiScale(22));
|
||
Chk.Font.Color := CLR_TEXT;
|
||
Chk.Font.Name := UI_FONT; Chk.Font.Size := 9;
|
||
Chk.Tag := i;
|
||
Chk.OnChange := OnSliceCATCtrlChange;
|
||
FSliceCATAutoTx[i] := Chk;
|
||
|
||
MakeLbl(Grp, 'CAT TX takes transmit to this slice',
|
||
PAD + 2, R1 + 2 * STEP + 24, GRP_W - PAD * 2 - 2);
|
||
|
||
// Аудио слота: не шаблон, а привязка буквы — применяется к слайсу, который
|
||
// стоит на слоте сейчас, и достаётся следующему.
|
||
MakeLbl(Grp, 'Audio out:', PAD, R1 + 3 * STEP + 26, LW);
|
||
Cmb := MakeCombo(Grp, CX, R1 + 3 * STEP + 20, CMB_W, OnSliceCATCtrlChange);
|
||
Cmb.Tag := i;
|
||
Cmb.Items.Add(SLICE_DEV_DEFAULT);
|
||
Cmb.ItemIndex := 0;
|
||
FSliceOutDev[i] := Cmb;
|
||
|
||
MakeLbl(Grp, 'Audio in:', PAD, R1 + 4 * STEP + 26, LW);
|
||
Cmb := MakeCombo(Grp, CX, R1 + 4 * STEP + 20, CMB_W, OnSliceCATCtrlChange);
|
||
Cmb.Tag := i;
|
||
Cmb.Items.Add(SLICE_DEV_NONE);
|
||
Cmb.ItemIndex := 0;
|
||
FSliceInDev[i] := Cmb;
|
||
end;
|
||
|
||
gy := 96 + ((CFG_CAT_SLICE_COUNT + 1) div 2) * (GRP_H + GRP_GAP);
|
||
with TLabel.Create(Self) do
|
||
begin
|
||
Parent := FPageSlices;
|
||
Caption := 'Audio out/in belong to the slice letter: the setting moves the '
|
||
+ 'slice sitting on that slot right away and the next one inherits it.';
|
||
SetBounds(DpiScale(MARGIN), DpiScale(gy), DpiScale(760), DpiScale(20));
|
||
Font.Name := UI_FONT; Font.Size := 9; Font.Color := CLR_TEXTDIM;
|
||
end;
|
||
Inc(gy, 18);
|
||
with TLabel.Create(Self) do
|
||
begin
|
||
Parent := FPageSlices;
|
||
Caption := 'Only one slice transmits at a time: while anything is already on '
|
||
+ 'the air the request is ignored — no take-over.';
|
||
SetBounds(DpiScale(MARGIN), DpiScale(gy), DpiScale(760), DpiScale(20));
|
||
Font.Name := UI_FONT; Font.Size := 9; Font.Color := CLR_TEXTDIM;
|
||
end;
|
||
with TLabel.Create(Self) do
|
||
begin
|
||
Parent := FPageSlices;
|
||
Caption := 'Without Auto TX a slice keys up over CAT only when it is already '
|
||
+ 'the transmit source (TX badge on the slice flag).';
|
||
SetBounds(DpiScale(MARGIN), DpiScale(gy + 18), DpiScale(760), DpiScale(20));
|
||
Font.Name := UI_FONT; Font.Size := 9; Font.Color := CLR_TEXTDIM;
|
||
end;
|
||
with TLabel.Create(Self) do
|
||
begin
|
||
Parent := FPageSlices;
|
||
Caption := 'The port is opened even when the slice does not exist yet — until '
|
||
+ 'it is created CAT answers "radio off" (PS0).';
|
||
SetBounds(DpiScale(MARGIN), DpiScale(gy + 36), DpiScale(760), DpiScale(20));
|
||
Font.Name := UI_FONT; Font.Size := 9; Font.Color := CLR_TEXTDIM;
|
||
end;
|
||
end;
|
||
|
||
procedure TSettingsForm.FireSliceCATChange;
|
||
var
|
||
En, Auto: array[0..CFG_CAT_SLICE_COUNT-1] of Boolean;
|
||
Prt: array[0..CFG_CAT_SLICE_COUNT-1] of Integer;
|
||
OutD, InD: array[0..CFG_CAT_SLICE_COUNT-1] of string;
|
||
i: Integer;
|
||
begin
|
||
if FLoading then Exit;
|
||
if not Assigned(FOnSliceCATChange) then Exit;
|
||
for i := 0 to CFG_CAT_SLICE_COUNT - 1 do
|
||
begin
|
||
En[i] := FSliceCATEn[i].Checked;
|
||
Prt[i] := FSliceCATPort[i].Value;
|
||
Auto[i] := FSliceCATAutoTx[i].Checked;
|
||
// Первый пункт списка — «общий выход/вход приложения» (пустое имя).
|
||
if FSliceOutDev[i].ItemIndex <= 0 then OutD[i] := ''
|
||
else OutD[i] := FSliceOutDev[i].Text;
|
||
if FSliceInDev[i].ItemIndex <= 0 then InD[i] := ''
|
||
else InD[i] := FSliceInDev[i].Text;
|
||
end;
|
||
FOnSliceCATChange(En, Prt, Auto, OutD, InD);
|
||
end;
|
||
|
||
procedure TSettingsForm.OnSliceCATCtrlChange(Sender: TObject);
|
||
begin FireSliceCATChange; end;
|
||
|
||
procedure TSettingsForm.LoadSliceCATSettings(
|
||
const Enabled: array of Boolean;
|
||
const Port: array of Integer;
|
||
const AutoTX: array of Boolean;
|
||
const OutDev: array of string;
|
||
const InDev: array of string);
|
||
var i: Integer;
|
||
begin
|
||
FLoading := True;
|
||
try
|
||
for i := 0 to CFG_CAT_SLICE_COUNT - 1 do
|
||
begin
|
||
if i <= High(Enabled) then FSliceCATEn[i].Checked := Enabled[i];
|
||
if (i <= High(Port)) and (Port[i] > 0) then
|
||
FSliceCATPort[i].Value := EnsureRange(Port[i], 1, 65535);
|
||
if i <= High(AutoTX) then FSliceCATAutoTx[i].Checked := AutoTX[i];
|
||
if i <= High(OutDev) then SelectDevInCombo(FSliceOutDev[i], OutDev[i]);
|
||
if i <= High(InDev) then SelectDevInCombo(FSliceInDev[i], InDev[i]);
|
||
end;
|
||
finally
|
||
FLoading := False;
|
||
end;
|
||
end;
|
||
|
||
procedure TSettingsForm.SelectDevInCombo(Cmb: TFlatComboBox; const DevName: string);
|
||
// Выбор устройства по ИМЕНИ. Сохранённого устройства может не быть в системе
|
||
// (кабель не поднят, гарнитура не воткнута) — тогда добавляем имя в список,
|
||
// чтобы привязка слота не потерялась молча при первом же открытии настроек.
|
||
var i: Integer;
|
||
begin
|
||
if Cmb = nil then Exit;
|
||
if DevName = '' then begin Cmb.ItemIndex := 0; Exit; end;
|
||
i := Cmb.Items.IndexOf(DevName);
|
||
if i < 0 then i := Cmb.Items.Add(DevName);
|
||
Cmb.ItemIndex := i;
|
||
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(DpiScale(AX), DpiScale(AY), DpiScale(AW), DpiScale(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(DpiScale(AX), DpiScale(0), DpiScale(22), DpiScale(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(DpiScale(AX), DpiScale(AY), DpiScale(AW), DpiScale(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(DpiScale(AX), DpiScale(0), DpiScale(22), DpiScale(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(DpiScale(AX), DpiScale(AY), DpiScale(84), DpiScale(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(DpiScale(AX), DpiScale(AY), DpiScale(30), DpiScale(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(DpiScale(MARGIN), DpiScale(18), DpiScale(500), DpiScale(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(DpiScale(MARGIN), DpiScale(48), DpiScale(680), DpiScale(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(DpiScale(0), DpiScale(Y), DpiScale(ANT_GRP_W), DpiScale(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 + DpiScale(ANT_ROW_H) - DpiScale(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, DpiScale(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 - DpiScale(PAD); // fallback, если заголовка нет
|
||
if (FAntXvtrHdr <> nil) and (Bands > 0) then Y := FAntXvtrHdr.Top - DpiScale(6);
|
||
end;
|
||
FPlutoAntGrp.Height := Y + DpiScale(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(DpiScale(AX), DpiScale(AY), DpiScale(AW), DpiScale(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(DpiScale(AX), DpiScale(AY), DpiScale(AW), DpiScale(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(DpiScale(AX), DpiScale(AY), DpiScale(22), DpiScale(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(DpiScale(AX), DpiScale(AY), DpiScale(AW), DpiScale(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(DpiScale(AX), DpiScale(AY), DpiScale(AW), DpiScale(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(DpiScale(AX), DpiScale(AY), DpiScale(AW), DpiScale(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(DpiScale(MARGIN), DpiScale(18), DpiScale(500), DpiScale(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(DpiScale(MARGIN), DpiScale(50), DpiScale(GRP_W), DpiScale(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(DpiScale(14), DpiScale(GRP_TITLE_H + 8), DpiScale(GRP_W - 28), DpiScale(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(DpiScale(MARGIN), DpiScale(GrpY + GRP_TITLE_H + 36 + 16), DpiScale(GRP_W), DpiScale(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;
|
||
// Офсеты S-метра на вкладке Calibration — только по включённым слотам.
|
||
FCalXvtrOn[i] := X.Entries[i].Enabled;
|
||
FCalXvtrName[i] := X.Entries[i].ButtonText;
|
||
end;
|
||
RelayoutCalTab;
|
||
if FChkXvtrTunPwr <> nil then FChkXvtrTunPwr.Checked := X.UseXVTRTunePower;
|
||
LoadQOFromConfig; // QO100_SLOT → отдельная страница QO-100
|
||
finally
|
||
FLoading := False;
|
||
end;
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// OC Control tab (Open Collector выходы Penny/Alex, byte 1401 HP-пакета).
|
||
// Портировано из Thetis (Console/setup.cs UpdateOCBits + Console/HPSDR/
|
||
// Penny.cs), объём сокращён — см. память project_oc_control: без PA-override/
|
||
// hot-switching внешнего PA и без split-pins VFO A/B. Только openHPSDR —
|
||
// вкладка целиком прячется на Pluto/AD936x (SetOCBackend).
|
||
// HF-группа = 11 диапазонов (как Alex/PA), VHF-группа = 8 слотов XVTR.
|
||
// ===========================================================================
|
||
|
||
procedure TSettingsForm.BuildOCTab;
|
||
const
|
||
MARGIN = 22;
|
||
PAD = 12;
|
||
HDR1_H = 22;
|
||
HDR2_H = 20;
|
||
ROW_H = 24;
|
||
PIN_W = 24;
|
||
BAND_X = PAD;
|
||
RX0_X = PAD + 50;
|
||
TX0_X = RX0_X + OC_PIN_COUNT * PIN_W + 20;
|
||
GRP_W = TX0_X + OC_PIN_COUNT * PIN_W + PAD;
|
||
DATA_Y0 = GRP_TITLE_H + HDR1_H + HDR2_H + 4;
|
||
HF_GRP_H = GRP_TITLE_H + HDR1_H + HDR2_H + 11 * ROW_H + PAD;
|
||
XVTR_GRP_H = GRP_TITLE_H + HDR1_H + HDR2_H + CFG_XVTR_COUNT * ROW_H + PAD;
|
||
ACT_ROW_H = 30;
|
||
ACT_GRP_W = 260;
|
||
ACT_GRP_H = GRP_TITLE_H + PAD + OC_PIN_COUNT * ACT_ROW_H;
|
||
|
||
HF_BAND_NAMES: array[0..10] of string =
|
||
('160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m');
|
||
|
||
var
|
||
Grp, ActGrp: TPanel;
|
||
b, p: Integer;
|
||
Y: Integer;
|
||
Cmb: TFlatComboBox;
|
||
|
||
function MkChk(AParent: TWinControl; AX, AY: Integer): TFlatCheckBox;
|
||
begin
|
||
Result := TFlatCheckBox.Create(Self);
|
||
Result.Parent := AParent;
|
||
Result.Caption := '';
|
||
Result.SetBounds(DpiScale(AX), DpiScale(AY), DpiScale(PIN_W), DpiScale(20));
|
||
Result.Font.Color := CLR_TEXT;
|
||
Result.Font.Name := UI_FONT;
|
||
Result.Font.Size := 9;
|
||
Result.OnChange := OnOCAnyChange;
|
||
end;
|
||
|
||
begin
|
||
FLoading := True;
|
||
try
|
||
with TLabel.Create(Self) do
|
||
begin
|
||
Parent := FPageOC;
|
||
Caption := 'OC Control';
|
||
SetBounds(DpiScale(MARGIN), DpiScale(18), DpiScale(500), DpiScale(28));
|
||
Font.Name := UI_FONT; Font.Size := 16;
|
||
Font.Style := [fsBold]; Font.Color := CLR_TEXT;
|
||
end;
|
||
FOCSubtitle := TLabel.Create(Self);
|
||
FOCSubtitle.Parent := FPageOC;
|
||
FOCSubtitle.Caption :=
|
||
'Open Collector outputs (Penny/Alex board), per-band and per-transverter. openHPSDR only.';
|
||
FOCSubtitle.SetBounds(DpiScale(MARGIN), DpiScale(48), DpiScale(680), DpiScale(20));
|
||
FOCSubtitle.Font.Name := UI_FONT;
|
||
FOCSubtitle.Font.Size := 9;
|
||
FOCSubtitle.Font.Color := CLR_TEXTDIM;
|
||
|
||
FOCEnabled := TFlatCheckBox.Create(Self);
|
||
FOCEnabled.Parent := FPageOC;
|
||
FOCEnabled.Caption := 'Enable OC Control';
|
||
FOCEnabled.SetBounds(DpiScale(MARGIN), DpiScale(76), DpiScale(220), DpiScale(22));
|
||
FOCEnabled.Font.Color := CLR_TEXT;
|
||
FOCEnabled.Font.Name := UI_FONT;
|
||
FOCEnabled.Font.Size := 9;
|
||
FOCEnabled.OnChange := OnOCAnyChange;
|
||
|
||
// ---- HF band grid ----
|
||
Grp := MakeGroupPanel(FPageOC, 'HF Bands', MARGIN, 106, GRP_W, HF_GRP_H);
|
||
FOCHFGrp := Grp;
|
||
MakeLbl(Grp, 'Band', BAND_X, GRP_TITLE_H + 2, 44);
|
||
MakeLbl(Grp, 'RX pins', RX0_X, GRP_TITLE_H + 2, OC_PIN_COUNT * PIN_W);
|
||
MakeLbl(Grp, 'TX pins', TX0_X, GRP_TITLE_H + 2, OC_PIN_COUNT * PIN_W);
|
||
for p := 0 to OC_PIN_COUNT - 1 do
|
||
begin
|
||
MakeLbl(Grp, IntToStr(p + 1), RX0_X + p * PIN_W + 5, GRP_TITLE_H + HDR1_H + 2, PIN_W);
|
||
MakeLbl(Grp, IntToStr(p + 1), TX0_X + p * PIN_W + 5, GRP_TITLE_H + HDR1_H + 2, PIN_W);
|
||
end;
|
||
for b := 0 to 10 do
|
||
begin
|
||
Y := DATA_Y0 + b * ROW_H;
|
||
MakeLbl(Grp, HF_BAND_NAMES[b], BAND_X, Y + 2, 44);
|
||
for p := 0 to OC_PIN_COUNT - 1 do
|
||
begin
|
||
FOCHFRx[b, p] := MkChk(Grp, RX0_X + p * PIN_W, Y);
|
||
FOCHFTx[b, p] := MkChk(Grp, TX0_X + p * PIN_W, Y);
|
||
end;
|
||
end;
|
||
|
||
// ---- HF TX pin action ----
|
||
ActGrp := MakeGroupPanel(FPageOC, 'HF TX Pin Action',
|
||
MARGIN + GRP_W + 14, 106, ACT_GRP_W, ACT_GRP_H);
|
||
FOCHFActGrp := ActGrp;
|
||
for p := 0 to OC_PIN_COUNT - 1 do
|
||
begin
|
||
MakeLbl(ActGrp, 'Pin ' + IntToStr(p + 1), PAD, GRP_TITLE_H + PAD + p * ACT_ROW_H + 5, 44);
|
||
Cmb := MakeCombo(ActGrp, PAD + 50, GRP_TITLE_H + PAD + p * ACT_ROW_H, 190, OnOCAnyChange);
|
||
for b := 0 to 6 do Cmb.Items.Add(OC_ACTION_NAMES[b]);
|
||
Cmb.ItemIndex := OCA_MOX_TUNE_TWOTONE;
|
||
FOCHFAction[p] := Cmb;
|
||
end;
|
||
|
||
// ---- XVTR (VHF) slot grid ----
|
||
Grp := MakeGroupPanel(FPageOC, 'Transverter Slots', MARGIN, 106 + HF_GRP_H + 14,
|
||
GRP_W, XVTR_GRP_H);
|
||
FOCXvtrGrp := Grp;
|
||
MakeLbl(Grp, 'Slot', BAND_X, GRP_TITLE_H + 2, 44);
|
||
MakeLbl(Grp, 'RX pins', RX0_X, GRP_TITLE_H + 2, OC_PIN_COUNT * PIN_W);
|
||
MakeLbl(Grp, 'TX pins', TX0_X, GRP_TITLE_H + 2, OC_PIN_COUNT * PIN_W);
|
||
for p := 0 to OC_PIN_COUNT - 1 do
|
||
begin
|
||
MakeLbl(Grp, IntToStr(p + 1), RX0_X + p * PIN_W + 5, GRP_TITLE_H + HDR1_H + 2, PIN_W);
|
||
MakeLbl(Grp, IntToStr(p + 1), TX0_X + p * PIN_W + 5, GRP_TITLE_H + HDR1_H + 2, PIN_W);
|
||
end;
|
||
for b := 0 to CFG_XVTR_COUNT - 1 do
|
||
begin
|
||
Y := DATA_Y0 + b * ROW_H;
|
||
MakeLbl(Grp, 'XVTR' + IntToStr(b + 1), BAND_X, Y + 2, 48);
|
||
for p := 0 to OC_PIN_COUNT - 1 do
|
||
begin
|
||
FOCXvtrRx[b, p] := MkChk(Grp, RX0_X + p * PIN_W, Y);
|
||
FOCXvtrTx[b, p] := MkChk(Grp, TX0_X + p * PIN_W, Y);
|
||
end;
|
||
end;
|
||
|
||
// ---- XVTR TX pin action ----
|
||
ActGrp := MakeGroupPanel(FPageOC, 'Transverter TX Pin Action',
|
||
MARGIN + GRP_W + 14, 106 + HF_GRP_H + 14, ACT_GRP_W, ACT_GRP_H);
|
||
FOCXvtrActGrp := ActGrp;
|
||
for p := 0 to OC_PIN_COUNT - 1 do
|
||
begin
|
||
MakeLbl(ActGrp, 'Pin ' + IntToStr(p + 1), PAD, GRP_TITLE_H + PAD + p * ACT_ROW_H + 5, 44);
|
||
Cmb := MakeCombo(ActGrp, PAD + 50, GRP_TITLE_H + PAD + p * ACT_ROW_H, 190, OnOCAnyChange);
|
||
for b := 0 to 6 do Cmb.Items.Add(OC_ACTION_NAMES[b]);
|
||
Cmb.ItemIndex := OCA_MOX_TUNE_TWOTONE;
|
||
FOCXvtrAction[p] := Cmb;
|
||
end;
|
||
finally
|
||
FLoading := False;
|
||
end;
|
||
end;
|
||
|
||
procedure TSettingsForm.CollectOCFromUI;
|
||
var b, p: Integer; Mask: Byte;
|
||
begin
|
||
FOCConfig.Enabled := (FOCEnabled <> nil) and FOCEnabled.Checked;
|
||
for b := 0 to 10 do
|
||
begin
|
||
Mask := 0;
|
||
for p := 0 to OC_PIN_COUNT - 1 do
|
||
if (FOCHFRx[b, p] <> nil) and FOCHFRx[b, p].Checked then Mask := Mask or (1 shl p);
|
||
FOCConfig.RxMaskHF[b] := Mask;
|
||
Mask := 0;
|
||
for p := 0 to OC_PIN_COUNT - 1 do
|
||
if (FOCHFTx[b, p] <> nil) and FOCHFTx[b, p].Checked then Mask := Mask or (1 shl p);
|
||
FOCConfig.TxMaskHF[b] := Mask;
|
||
end;
|
||
for b := 0 to CFG_XVTR_COUNT - 1 do
|
||
begin
|
||
Mask := 0;
|
||
for p := 0 to OC_PIN_COUNT - 1 do
|
||
if (FOCXvtrRx[b, p] <> nil) and FOCXvtrRx[b, p].Checked then Mask := Mask or (1 shl p);
|
||
FOCConfig.RxMaskXvtr[b] := Mask;
|
||
Mask := 0;
|
||
for p := 0 to OC_PIN_COUNT - 1 do
|
||
if (FOCXvtrTx[b, p] <> nil) and FOCXvtrTx[b, p].Checked then Mask := Mask or (1 shl p);
|
||
FOCConfig.TxMaskXvtr[b] := Mask;
|
||
end;
|
||
for p := 0 to OC_PIN_COUNT - 1 do
|
||
begin
|
||
if (FOCHFAction[p] <> nil) and (FOCHFAction[p].ItemIndex >= 0) then
|
||
FOCConfig.ActionHF[p] := Byte(FOCHFAction[p].ItemIndex);
|
||
if (FOCXvtrAction[p] <> nil) and (FOCXvtrAction[p].ItemIndex >= 0) then
|
||
FOCConfig.ActionXvtr[p] := Byte(FOCXvtrAction[p].ItemIndex);
|
||
end;
|
||
end;
|
||
|
||
procedure TSettingsForm.FireOCChange;
|
||
begin
|
||
if FLoading then Exit;
|
||
CollectOCFromUI;
|
||
if Assigned(FOnOCChange) then
|
||
FOnOCChange(FOCConfig);
|
||
end;
|
||
|
||
procedure TSettingsForm.OnOCAnyChange(Sender: TObject);
|
||
begin
|
||
FireOCChange;
|
||
end;
|
||
|
||
procedure TSettingsForm.LoadOCSettings(const O: TOCSettings);
|
||
var b, p: Integer;
|
||
begin
|
||
FLoading := True;
|
||
try
|
||
FOCConfig := O;
|
||
if FOCEnabled <> nil then FOCEnabled.Checked := O.Enabled;
|
||
for b := 0 to 10 do
|
||
for p := 0 to OC_PIN_COUNT - 1 do
|
||
begin
|
||
if FOCHFRx[b, p] <> nil then
|
||
FOCHFRx[b, p].Checked := (O.RxMaskHF[b] and (1 shl p)) <> 0;
|
||
if FOCHFTx[b, p] <> nil then
|
||
FOCHFTx[b, p].Checked := (O.TxMaskHF[b] and (1 shl p)) <> 0;
|
||
end;
|
||
for b := 0 to CFG_XVTR_COUNT - 1 do
|
||
for p := 0 to OC_PIN_COUNT - 1 do
|
||
begin
|
||
if FOCXvtrRx[b, p] <> nil then
|
||
FOCXvtrRx[b, p].Checked := (O.RxMaskXvtr[b] and (1 shl p)) <> 0;
|
||
if FOCXvtrTx[b, p] <> nil then
|
||
FOCXvtrTx[b, p].Checked := (O.TxMaskXvtr[b] and (1 shl p)) <> 0;
|
||
end;
|
||
for p := 0 to OC_PIN_COUNT - 1 do
|
||
begin
|
||
if FOCHFAction[p] <> nil then FOCHFAction[p].ItemIndex := EnsureRange(O.ActionHF[p], 0, 6);
|
||
if FOCXvtrAction[p] <> nil then FOCXvtrAction[p].ItemIndex := EnsureRange(O.ActionXvtr[p], 0, 6);
|
||
end;
|
||
finally
|
||
FLoading := False;
|
||
end;
|
||
end;
|
||
|
||
procedure TSettingsForm.SetOCBackend(IsPluto: Boolean);
|
||
// У Pluto/AD936x нет Open Collector выхода — вкладка целиком скрывается,
|
||
// а не показывает пустое содержимое (в отличие от Antenna, где Pluto имеет
|
||
// свою таблицу разъёмов).
|
||
begin
|
||
FNavOC.Visible := not IsPluto;
|
||
if IsPluto and (FPageOC.Visible) then
|
||
SelectPage(FPageAudio, FNavAudio);
|
||
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(DpiScale(ED_X), DpiScale(AY), DpiScale(ED_W), DpiScale(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(DpiScale(LBL_X), DpiScale(AY + 4), DpiScale(LBL_W), DpiScale(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(DpiScale(MARGIN), DpiScale(18), DpiScale(600), DpiScale(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(DpiScale(MARGIN), DpiScale(50), DpiScale(700), DpiScale(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 + 9 * 30 + 16);
|
||
|
||
Y := GRP_TITLE_H + 8;
|
||
FQoEn := TFlatCheckBox.Create(Self);
|
||
FQoEn.Parent := Grp; FQoEn.Caption := 'Enable QO-100 band';
|
||
FQoEn.SetBounds(DpiScale(LBL_X), DpiScale(Y), DpiScale(300), DpiScale(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;
|
||
|
||
// TO Error (Гц): сам транспондер-offset держим стандартным (8089.5 МГц),
|
||
// а уход конкретного тракта (TCXO uplink-SDR, наводка LNB) правим здесь.
|
||
Inc(Y, 30); QoLabel('TO Error (Hz)', Y);
|
||
FQoTxLOErr := QoFloat(Y, -999999.0, 999999.0, 1.0, 0);
|
||
|
||
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(DpiScale(ED_X), DpiScale(Y), DpiScale(ED_W), DpiScale(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(DpiScale(MARGIN), DpiScale(100 + GRP_TITLE_H + 9 * 30 + 28), DpiScale(700), DpiScale(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 + FQoTxLOErr.Value / 1.0e6; // TX uplink + TO Error
|
||
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].TxLOError := FQoTxLOErr.Value;
|
||
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;
|
||
FQoTxLOErr.Value := E.TxLOError;
|
||
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.
|