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.