Files
ewsdr/Settings.pas
T
ew8bakandClaude Opus 4.8 366d4c9821 feature(calibration): офсет спектра/водопада на диапазон и на трансвертер
Вторая калибровочная таблица рядом с S-метром (паритет с Display Cal Offset
в Thetis): у display-тракта своё окно/детектор, и его уровень расходится с
показаниями стрелки на пару дБ, поэтому таблицы раздельные.

- Settings: DispBandDB[12] / DispXvtrDB[8] (JSON cal_disp_band_i /
  cal_disp_xvtr_i, дефолт 0 — картинка не меняется до первой правки).
- WDSPEngine: колбэк OnDispCal(PanId) — движок раз в кадр спрашивает офсет в
  UpdateSpectrum и прибавляет к пикселям (главный спектр/водопад в копирующем
  цикле, паны in-place при ненулевом офсете). На TX-дисплее не применяется.
  Так обошлись без хуков на каждое присваивание FCenterFreq.
- RadioController: общий CalLookup(Hz, BandTbl, XvtrTbl) — на нём SMeterCalFor
  и DispCalFor(PanId): главный дисплей по FCenterFreq, пан по FPanFreqHz[p],
  т.е. кросс-банд паны получают свой офсет. Колбэк вешается в CreateEngines,
  поэтому работает и в GUI, и в ewsdrd; web-зеркало получает готовые кадры.
- SettingsForm: группа «Spectrum / waterfall» с той же разбивкой (диапазоны +
  включённые трансвертеры активного железа) и кнопка «= S-meter» — копия
  таблицы стрелки как старт калибровки; общая раскладка сеток вынесена в
  LayoutCalOffsets.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 11:16:34 +03:00

2033 lines
94 KiB
ObjectPascal
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
unit Settings;
{
Settings.pas — JSON-конфигурация HPSDR SDR приложения.
Структура hpsdr_settings.json:
{ "AA:BB:CC:DD:EE:FF": { "global": {...}, "bands": {"5": {...}} } }
}
{$mode objfpc}{$H+}
interface
uses SysUtils, Classes, Math, fpJSON, jsonparser, jsonscanner, PlatformUtils,
RadioModes;
const
SETTINGS_FILE = 'hpsdr_settings.json';
CFG_BAND_COUNT = 12;
CFG_XVTR_COUNT = 8; // количество XVTR-слотов (трансверторов)
CFG_MODE_MIN = RadioModes.MODE_MIN;
CFG_MODE_MAX = RadioModes.MODE_MAX;
CFG_PANS_MAX = 4; // потолок панадаптеров (пан 0 + доп.) — = WDSPEngine.MAX_PANS
QO100_SLOT = CFG_XVTR_COUNT - 1; // последний слот зарезервирован под QO-100
// (split-LO транспондер; настраивается на
// отдельной странице, не в общей XVTR-таблице)
// PA gain defaults (dB) — match Thetis HERMES/HPSDR defaults
// order: 160m,80m,60m,40m,30m,20m,17m,15m,12m,10m,6m
// 12-й слот — пустой в HF-плане (используется только 12-местным VHF/UHF-планом
// Pluto, где частота/режим приходят из плана BoardUtils, а не отсюда).
CFG_PA_BAND_CAL_DEFAULT: array[0..CFG_BAND_COUNT-1] of Double = (
41.0, 41.2, 41.3, 41.3, 41.0, 40.5, 39.9, 38.8, 38.8, 38.8, 38.8, 38.8);
CFG_VHF_CAL_DEFAULT = 56.2; // dB, default for all XVTR slots
CFG_BAND_DEFAULT_FREQ: array[0..CFG_BAND_COUNT-1] of Double = (
1900000, 3750000, 5357000, 7100000, 10125000,
14200000, 18120000, 21200000, 24940000, 28500000, 50150000, 50150000);
CFG_BAND_DEFAULT_MODE: array[0..CFG_BAND_COUNT-1] of Integer = (
0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1);
// Полосы фильтров по типу модуляции (Гц) + дефолтные индексы. Нужны
// TRadioController для headless-выбора фильтра при смене режима; UI-подписи
// (FILT_*_NAMES) остаются в MainForm. NAMES-массивы там используют FILT_COUNT.
FILT_COUNT = 10;
FILT_SSB_BW: array[0..FILT_COUNT-1] of Integer =
(5000, 4400, 3800, 3300, 2900, 2700, 2400, 2100, 1800, 1000);
FILT_SSB_DEF = 5; // 2.7k
FILT_CW_BW: array[0..FILT_COUNT-1] of Integer =
(1000, 800, 600, 500, 400, 250, 150, 100, 50, 25);
FILT_CW_DEF = 3; // 500
FILT_DSB_BW: array[0..FILT_COUNT-1] of Integer =
(16000, 12000, 10000, 8000, 6600, 5200, 4000, 3100, 2900, 2400);
FILT_DSB_DEF = 3; // 8.0k
FILT_AM_BW: array[0..FILT_COUNT-1] of Integer =
(20000, 18000, 16000, 12000, 10000, 9000, 8000, 7000, 6000, 5000);
FILT_AM_DEF = 4; // 10k
// DIGI: полоса считается от нуля (кромки 0..BW), поэтому таблица своя —
// SSB-полосы отмерены от среза на 100 Гц и здесь не подходят.
FILT_DIGI_BW: array[0..FILT_COUNT-1] of Integer =
(3000, 2700, 2400, 2100, 1800, 1500, 1200, 900, 600, 300);
FILT_DIGI_DEF = 0; // 3.0k
FILT_FM_COUNT = 2;
FILT_FM_BW: array[0..1] of Integer = (11000, 16000);
FILT_FM_DEV: array[0..1] of Double = (2500.0, 5000.0);
FILT_FM_DEF = 0; // NFM
// WFM: вещательный канал. 200 кГц требует SampleRate 384k — при 192k
// WDSPEngine.SetFilter сам зажмёт кромки по Найквисту dsp_rate.
FILT_WFM_COUNT = 3;
FILT_WFM_BW: array[0..2] of Integer = (200000, 160000, 120000);
FILT_WFM_DEF = 1; // 160k — влезает и в 192k
// FM RAW: narrow, flat discriminator output for external digital decoders.
// These are RF channel widths; baseband stays flat at 20..7000 Hz.
FILT_RAW_COUNT = 5;
FILT_RAW_BW: array[0..FILT_RAW_COUNT-1] of Integer =
(6250, 10000, 12500, 20000, 25000);
FILT_RAW_DEF = 2; // 12.5 kHz
RAW_FM_DEV_DEFAULT = 2500;
RAW_FM_DEV_MIN = 500;
RAW_FM_DEV_MAX = 10000;
// DMR Tier II: 12.5 kHz RF channel, 4FSK nominal deviation +/-1.944 kHz.
FILT_DMR_BW = 12500;
DMR_FM_DEV = 2500;
type
TBandSettings = record
VfoA: Double;
VfoB: Double;
Mode: Integer;
FilterIdx: Integer;
FilterBW: Integer;
AGCMode: Integer;
AGCTop: Integer;
CTun: Boolean;
CenterHz: Double; // DDC-центр при CTun (восстановление offset VFO в полосе)
SpanHz: Double;
FMSQOn: Boolean;
FMSQLevel: Integer; // 0..100 → WDSP threshold 0.0..0.5
CTCSSOn: Boolean;
CTCSSToneIdx: Integer; // 0..37
FMStepOn: Boolean;
FMStepIdx: Integer; // 0..3 (6.25/12.5/20/25 kHz)
FMRptDir: Integer; // RPT_NONE/RPT_MINUS/RPT_PLUS
FMRptOffsetHz: Double; // Hz, 0 = use band default
ZoomFactor: Double; // зум спектра/водопада 0.0..1.0 (0.0 = без зума)
PanSlider: Double; // положение окна зума 0..1 (0.5 = центр)
// Аппаратный AGC AD936x (Pluto/LibreSDR) — на диапазон: усиление тракта
// сильно разное на 2м и на QO-100, поэтому режим и manual-гейн per-band.
RxGainMode: Integer; // 0=manual,1=fast,2=slow,3=hybrid
RxGainDb: Integer; // manual gain, дБ (0..73)
end;
// Персист панадаптеров и слайсов (секция "pans" под MAC). Восстанавливается
// один раз при выходе в Run. Слайс = приёмный WDSP-канал внутри пана; частоты
// абсолютные (видимые). См. [[project_slices_plan]].
TSliceCfg = record
TargetHz: Double; // абсолютная видимая частота слайса
Mode: Integer;
FilterLo: Integer;
FilterHi: Integer;
AGC: Integer; // Ord(TWDSPAGCMode)
Volume: Double; // 0..1
Muted: Boolean;
RxMuteOnTx: Boolean; // True = слайс молчит на передаче (self-monitor выкл)
FMSQOn: Boolean;
FMSQLevel: Integer;
DevName: string; // имя PortAudio-устройства ('' = default)
end;
TPanCfg = record
Active: Boolean; // пан 1..N существует (пан 0 всегда есть)
FreqHz: Double; // видимый центр DDC (только паны N)
RateKHz: Integer;
ADC: Integer;
Zoom: Double; // 0..1
PanSlider: Double; // 0..1
StackShare: Double; // доля высоты в стеке панов
ColShare: Double; // доля ширины в грид-раскладке
SplitRatio: Double; // доля высоты спектра (сплиттер спектр/водопад)
// Per-pan дисплей (3.6): override глобального рендера. HasDisplay=False ⇒
// пан наследует глобальный дефолт (поля игнорируются).
HasDisplay: Boolean;
DPalette: Integer; // 0=Classic/1=Inferno/2=Turbo
DGamma: Double;
DSpecRef: Double; // верх шкалы спектра, dBm
DSpecRange: Double; // диапазон шкалы, dB
DWfAGC: Boolean; // авто-уровень водопада
DWfOffset: Double; // яркость водопада при AGC, dB
DWfHigh: Double; // ручной верх водопада, dBm
DWfLow: Double; // ручной низ водопада, dBm
Slices: array of TSliceCfg;
end;
TPansConfig = record
GridMode: Boolean; // грид-раскладка (▦) vs вертикальный стек (▤)
GridRowShare: Double; // доля высоты пана 0 в гриде
Pans: array[0..CFG_PANS_MAX-1] of TPanCfg; // [0]=главный (только Slices)
end;
// TX-настройки per-device. Хранятся в JSON-секции "tx" под MAC,
// отдельно от global/bands. Применяются к WDSP через SetTX* методы.
TTXSettings = record
// Radio mic options (DUC Specific byte 50/51)
MicLineIn: Boolean; // bit0: 1=Line In, 0=Mic In
MicBoost: Boolean; // bit1: +20 dB preamp (только когда Mic In)
MicPTTEnabled: Boolean; // bit2: Mic PTT enable (Orion)
MicTipRing: Boolean; // bit3: False=Tip, True=Ring (Orion/OrionMkII)
MicBias: Boolean; // bit4: электретный +5V bias (Orion)
LineInGainDB: Double; // byte51: -34.5..+12 dB (шаг 1.5; только когда Line In)
// Mic gain (dB, применяется в WDSP PanelGain1)
MicGainDB: Double; // -10..+30
// ATT on TX — ослабление RX-ADC во время передачи (защита от утечки TX).
// DUC Specific bytes 57/58/59 (StepAtten 0/1/2). Thetis default: 31 dB.
AttOnTX: Integer; // 0..31 dB
// Tune (кнопка TUN) — генерация тонального сигнала через WDSP PostGen.
// Уровень — отдельный от слайдера Drive, проходит через ту же
// per-band калибровку. Thetis defaults: level=10 %, freq=750 Hz.
TUNLevel: Integer; // 0..100 (% от полной шкалы Drive)
TUNFreq: Integer; // Hz, обычно 600..1500 для SSB tune
// TX bandpass filter
FilterLow: Integer; // Hz, для SSB обычно 100
FilterHigh: Integer; // Hz, для SSB обычно 2800
FilterNC: Integer; // FIR length (1024/2048/4096)
FilterMP: Integer; // 0=Linear Phase, 1=Low Latency
FilterWindow: Integer; // 0=Rect..6=BH7
// Compressor
CompressorOn: Boolean;
CompressorGain: Double; // 0..20 dB
// Leveler (TX AGC)
LevelerOn: Boolean;
LevelerTop: Double; // 0..10 dB
LevelerDecay: Integer; // ms
// ALC (Auto Level Control)
ALCOn: Boolean;
ALCMaxGain: Double; // -10..+10 dB
ALCDecay: Integer; // ms
// Phase Rotator
PhaseRotOn: Boolean;
PhaseRotStages: Integer; // 4/6/8
PhaseRotFreq: Double; // Hz, обычно 338
// EQ TX (10-band: [0]=preamp, [1..10]=bands)
EQOn: Boolean;
EQNumBands: Integer; // 3 или 10
EQGains: array[0..10] of Double; // dB, ±15
EQFreqs: array[0..10] of Double; // Hz
// AM modulator
AMCarrierLevel: Double; // 0..1
// FM modulator
FMDeviation: Double; // Hz, 2500..5000
FMLowCut: Integer; // AF filter low
FMHighCut: Integer; // AF filter high
FMEmphPosition: Integer; // 0=PreEQ, 1=PostEQ
// CTCSS
CTCSSOn: Boolean;
CTCSSFreq: Double; // Hz
// TX Display (отдельный analyzer для TX-спектра/водопада)
TXFFTSize: Integer; // 4096..262144
TXWindowType: Integer; // 0..6
TXSpecDetector: Integer; // 0=Peak,1=Rosenfell,2=Average,3=Sample,4=RMS
TXSpecAvgMode: Integer; // 0=None,1=Recursive,2=TimeWindow,3=LogRecursive
TXSpecAvgTimeMS: Double;
TXWfDetector: Integer;
TXWfAvgMode: Integer;
TXWfAvgTimeMS: Double;
// TX Grid (отдельные от RX — Thetis отрисовывает спектр TX в своих границах)
TXSpecRefLevel: Double; // dBm reference (top of scale), e.g. 0
TXSpecRange: Double; // dB range, e.g. 80
TXSpecGridStep: Double; // grid step in dB, e.g. 10
// PureSignal (WDSP calcc; дефолты = Thetis PSForm, протокол 2)
PSMoxDelay: Double; // с — задержка после MOX до сбора feedback (0.2)
PSLoopDelay: Double; // с — пауза между итерациями калибровки (0)
PSTXDelayNs: Integer; // нс — компенсация задержки TX-тракта (150)
PSRelaxPtol: Boolean; // ослабленный допуск фазы (ptol 0.4 вместо 0.8)
PSAutoAtten: Boolean; // авто-подстройка AttOnTX по уровню feedback
PSPin: Boolean; // pin mode (True)
PSMap: Boolean; // map mode (True)
PSStabilize: Boolean; // stabilize (False)
PSIntsIdx: Integer; // 0=16/256, 1=8/512, 2=4/1024
PSHWPeak: Double; // SetPk (0.2899 для P2, 0.6121 Saturn)
PSTTFreq1: Integer; // Гц — тон 1 двухтональника (700)
PSTTFreq2: Integer; // Гц — тон 2 двухтональника (1900)
// Калибровка фидбек-тракта (Thetis-Enhanced eu2av): на Orion MkII цель 152
// перегружает входные каскады ADC → IMD-«юбка», оптимум FB≈22 при ATT≈10дБ.
PSFBTarget: Integer; // цель уровня feedback info[4]; 0 = авто по плате
// (Orion MkII → 22, иначе 152)
PSOutlierSigma: Double; // σ outlier-фильтра calcc; <0 = авто по плате
// (Orion MkII → 5.0, иначе выкл.), 0 = выкл.
end;
// Web-сервер — глобальные настройки (не привязаны к устройству, секция "web").
TWebSettings = record
Enabled: Boolean;
Port: Integer; // 1..65535
BindAddr: string; // e.g. '0.0.0.0' (все интерфейсы)
User: string;
Pass: string;
end;
// Transverter (XVTR) entry — один трансвертер.
// Видимая частота VFO в [FreqBegin..FreqEnd] транслируется в IF-частоту,
// которую слышит трансивер: f_IF = f_visible - LOOffset + LOError.
// Примеры: 2m XVTR с LO=116 MHz → visible 144.200 ⇒ IF 28.200 MHz.
TXvtrEntry = record
Enabled: Boolean;
ButtonText: string[15]; // подпись на band-кнопке (например, '2m', '70cm')
FreqBegin: Double; // Hz
FreqEnd: Double; // Hz
LOOffset: Double; // Hz, отнимается от видимой частоты для получения IF (RX)
LOError: Double; // Hz, корректировка LO (положительная — IF растёт) — RX/LNB дрейф
// QO-100 / split-LO транспондер: RX и TX идут на РАЗНЫХ LO. RX = visible-LOOffset
// (+LOError, дрейф LNB по маяку). TX = visible-TxLOOffset (без LOError — uplink-LO
// собственный у SDR). FullDuplexTx помечает слот как такой транспондер.
FullDuplexTx: Boolean; // True — раздельный TX-LO (QO-100); авто display-duplex
TxLOOffset: Double; // Hz, TX LO-offset (для QO-100 = транспондер-offset, ~8089.5 МГц)
RXGain: Double; // dB, компенсация усиления RX тракта
RXOnly: Boolean; // True — запрещает TX на этом трансверторе
TXPower: Integer; // 0..100 % (drive level специально для XVTR)
DisablePA: Boolean; // True — отключить внутренний PA при TX (XVTR имеет свой)
RXAntenna: Byte; // 0=по-умолчанию(основной band), 1=ANT1, 2=ANT2, 3=ANT3
LastFreq: Double; // Hz — последняя tuned visible частота VFO A (BandStack)
LastFreqB: Double; // Hz — последняя tuned visible частота VFO B
LastMode: Integer; // Mode (LSB=0,USB=1,…FM=5)
LastFilterIdx: Integer; // FM: 0=NFM, 1=WFM; иначе 0..FILT_COUNT-1
LastFMSQOn: Boolean;
LastFMSQLevel: Integer;
LastCTCSSOn: Boolean;
LastCTCSSToneIdx: Integer;
LastFMStepOn: Boolean;
LastFMStepIdx: Integer;
LastCTun: Boolean;
LastCenterHz: Double; // Hz — DDC-центр при CTun (восстановление offset VFO)
LastAGCMode: Integer;
LastAGCTop: Integer;
LastFMRptDir: Integer;
LastFMRptOffsetHz: Double;
LastCTCSSAutoActive: Boolean; // CTCSS был включён каналом (авто)
LastFMRptAutoActive: Boolean; // RPT был включён каналом (авто)
LastZoomFactor: Double; // зум спектра/водопада 0.0..1.0 (0.0 = без зума)
LastPanSlider: Double; // положение окна зума 0..1 (0.5 = центр)
// Аппаратный AGC AD936x на слот трансвертера (у QO-100 своё усиление тракта).
LastRxGainMode: Integer; // 0=manual,1=fast,2=slow,3=hybrid
LastRxGainDb: Integer; // manual gain, дБ
end;
TXvtrSettings = record
Entries: array[0..CFG_XVTR_COUNT-1] of TXvtrEntry;
// Use XVTR TUN power — кнопка TUN использует XVTR-specific power.
UseXVTRTunePower: Boolean;
end;
// Alex (Antenna + Filter board) settings per-device.
// Хранятся в JSON-секции "alex" под MAC.
// Безопасные дефолты: ANT1 для всех диапазонов, вторичный вход выключен.
// Антенные разъёмы AD936x (Pluto/LibreSDR): какой физический вход/выход
// используется на каждом диапазоне. 0 = RX1/TX1, 1 = RX2/TX2.
// Диапазоны — VU-план BoardUtils (12 слотов), плюс слоты трансвертеров.
TAD936xAnt = record
RxAnt: array[0..CFG_BAND_COUNT-1] of Byte;
TxAnt: array[0..CFG_BAND_COUNT-1] of Byte;
XvtrRxAnt: array[0..CFG_XVTR_COUNT-1] of Byte;
XvtrTxAnt: array[0..CFG_XVTR_COUNT-1] of Byte;
end;
TAlexSettings = record
// Per-band (11 bands: 0=160m..10=6m)
RxAnt: array[0..CFG_BAND_COUNT-1] of Byte; // 1..3 (ANT1/2/3)
TxAnt: array[0..CFG_BAND_COUNT-1] of Byte; // 1..3 (ANT1/2/3)
// Вторичный RX вход: 0=выкл, 1=BYPS(Ext1+Bypass), 2=EXT1(Ext2+Bypass), 3=XVTR(XvtrDDC+Bypass)
RxOnly: array[0..CFG_BAND_COUNT-1] of Byte;
DoNotTx: array[0..CFG_BAND_COUNT-1] of Boolean; // программный запрет TX на диапазоне
// Глобальные флаги в режиме TX
RxBypassOnTx: Boolean; // бит 11 (Bypass relay) во время TX
Ext1OnTx: Boolean; // бит 9 (Ext1) + бит 11 во время TX
GndBpf2OnTx: Boolean; // заземление BPF2/RX2 во время TX (Alex1 бит 8, ANAN-8000DLE)
EnableXvtrHf: Boolean; // режим XVTR HF
end;
// Калибровки (вкладка Calibration): детектор мощности/КСВ Alex-моста,
// напряжение/ток питания, S-метр. Модель детектора задаёт константы моста
// (Vref/bridge_volt/ADC-offset, как в Thetis computeAlexFwdPower).
TCalibration = record
PwrDetModel: Integer; // 0=ANAN-10/100 (3.3V), 1=ANAN-200D, 2=ANAN-7000DLE/Anvelina Pro 3, 3=ANAN-8000DLE/Orion MkII
PADetCal: array[0..CFG_BAND_COUNT-1] of Double; // множитель FWD/REV на банд, 0.50..2.00 (1.0 = без коррекции)
SupplyVoltCal: Double; // множитель напряжения питания (1.0 = без коррекции)
AmpVoffMV: Double; // сдвиг нуля датчика тока, мВ (Thetis default 360)
AmpSensMVA: Double; // чувствительность датчика тока, мВ/А (Thetis default 120)
// Офсет S-метра, дБ — раздельно на диапазон и на слот трансвертера (усиление
// тракта разное на 160м и на 10м, а у XVTR своё — конвертер/LNB). Смысл
// индекса диапазона задаёт band-план железа (HF у openHPSDR, VHF/UHF у
// Pluto); таблицы не пересекаются, т.к. секция JSON ключуется по MAC.
SMeterBandDB: array[0..CFG_BAND_COUNT-1] of Double;
SMeterXvtrDB: array[0..CFG_XVTR_COUNT-1] of Double;
// Офсет спектра/водопада, дБ — та же разбивка. Отдельно от S-метра (как
// Display Cal Offset в Thetis): у display-тракта своё окно/детектор, и его
// уровень отличается от показаний стрелки на пару дБ.
DispBandDB: array[0..CFG_BAND_COUNT-1] of Double;
DispXvtrDB: array[0..CFG_XVTR_COUNT-1] of Double;
end;
TGlobalSettings = record
Volume: Integer;
TxMonVolume: Integer; // громкость self-monitor RX-аудио на TX (RX MUTE off)
DriveLevel: Integer;
ActiveVfo: Integer;
NRMode: Integer;
NBMode: Integer;
SNBEnabled: Boolean;
ANFEnabled: Boolean;
AGCSlope: Integer;
AGCHangThreshold: Integer;
WfAGCEnabled: Boolean;
WfNFEnabled: Boolean;
LastBand: Integer;
LastXvtr: Integer; // -1 = HF, 0..CFG_XVTR_COUNT-1 = активный трансвертер при выходе
SampleRate: Integer; // глобальный — один для всех диапазонов
RxGainMode: Integer; // Pluto: 0=manual,1=fast,2=slow,3=hybrid
RxGainDb: Integer; // Pluto: manual gain дБ
WindowLeft: Integer;
WindowTop: Integer;
WindowWidth: Integer;
WindowHeight: Integer;
// --- Display settings ---
FFTSize: Integer; // 4096..262144
WindowType: Integer; // 0=Rect,1=BH4,2=Hann,3=FlatTop,4=Hamming,5=Kaiser,6=BH7
SpecDetector: Integer; // 0=Peak,1=Rosenfell,2=Average,3=Sample,4=RMS
SpecAvgMode: Integer; // 0=None,1=Recursive,2=TimeWindow,3=LogRecursive
SpecAvgTimeMS: Double; // averaging time in milliseconds
WfDetector: Integer; // waterfall detector
WfAvgMode: Integer; // waterfall average mode
WfAvgTimeMS: Double; // waterfall averaging time in milliseconds
WfManualHigh: Double; // manual waterfall high threshold
WfManualLow: Double; // manual waterfall low threshold
WfAGCOffset: Double; // AGC offset in dB
WfPalette: Integer; // 0=Classic, 1=Inferno, 2=Turbo
WfGamma: Double; // контраст палитры (0.3..3.0, 1.0 = линейно)
DisplayPixels: Integer; // потолок точек анализатора (1024/2048/4096)
WfSpeedDiv: Integer; // делитель скорости водопада (1/2/4/8, max-hold)
SpecMSAA: Integer; // MSAA GL-спектра (1=off, 2, 4, 8)
SpecRefLevel: Double; // dBm reference (top of scale), e.g. -20
SpecRange: Double; // dB range, e.g. 110 (bottom = ref - range)
SpecGridStep: Double; // grid step in dB, e.g. 10
// --- Audio settings ---
AudioOutDevice: string; // PortAudio output device name ('' = default)
AudioSampleRate: Integer; // PortAudio stream sample rate (e.g. 48000)
AudioInDevice: string; // PortAudio input device name for TX mic ('' = none)
SendAudioToRadio: Boolean; // True = send RX audio stream to radio (built-in speaker)
RawFMDeviation: Integer; // Hz, flat RAW FM discriminator scale
// --- Visibility settings ---
ShowSpectrum: Boolean; // True = draw spectrum on main form
ShowWaterfall: Boolean; // True = draw waterfall on main form
ShowWideband: Boolean; // True = draw raw ADC wideband pane on main form
WidebandFill: Boolean; // True = fill wideband spectrum with gradient
SpectrumFill: Boolean; // True = fill main spectrum under the curve with gradient
DisplayDuplex: Boolean; // DUP: при TX показывать RX-водопад, TX-фильтр overlay'ем
RxMuteOnTx: Boolean; // QO-100 self-monitor: True=RX-аудио глушится на TX (умолч.)
// --- Display FPS ---
DisplayFPS: Integer; // spectrum/waterfall timer fps (1..100)
// --- Theme ---
LightTheme: Boolean; // False = dark (default), True = light
// --- VFO display digits ---
FreqMhzDigits: Integer; // minimum MHz digit count: 3=999MHz, 4=9.999GHz, 5=99.999GHz
// --- PA (Power Amplifier) settings ---
PAMaxPower: Double; // максимальная выходная мощность, Вт (5..200)
PlutoTxMaxAttDb: Double; // Pluto: TX-аттенюация на drive=100%, дБ<0 (дефолт -10)
PABandCal: array[0..CFG_BAND_COUNT-1] of Double; // калибровка на диапазон 38.8..100.0
VHFBandCal: array[0..CFG_XVTR_COUNT-1] of Double; // калибровка для XVTR слотов 38.8..100.0
// --- Calibration (детектор мощности/КСВ, напряжение/ток, S-метр) ---
Cal: TCalibration;
// --- ADC settings ---
DitherEnabled: Boolean; // DDC Specific byte 5 — уменьшает интермодуляцию
RandomEnabled: Boolean; // DDC Specific byte 6 — рандомизация ADC
// --- CAT (Computer Aided Transceiver) settings ---
// До 4 последовательных портов (Linux: /dev/ttyS0, Windows: COM1 и т.д.)
CATSerialEnabled: array[0..3] of Boolean;
CATSerialPort: array[0..3] of string; // имя порта ОС
CATSerialBaud: array[0..3] of Integer; // скорость: 1200..115200
CATSerialDataBits: array[0..3] of Integer; // 7 или 8
CATSerialStopBits: array[0..3] of Integer; // 1 или 2
CATSerialParity: array[0..3] of Integer; // 0=None, 1=Odd, 2=Even
CATSerialAndromeda:array[0..3] of Boolean; // порт — передняя панель Andromeda/G2
// TCP CAT сервер (один, многоклиентский)
CATTcpEnabled: Boolean;
CATTcpPort: Integer; // порт, по умолчанию 19090
end;
TSettingsManager = class
private
FFilePath: string;
FRoot: TJSONObject;
function EnsureObj(P: TJSONObject; const K: string): TJSONObject;
function GetDevObj(const M: string): TJSONObject;
function GetGlobalObj(D: TJSONObject): TJSONObject;
function GetBandObj(D: TJSONObject; Idx: Integer): TJSONObject;
function JI(O: TJSONObject; const K: string; Def: Integer): Integer;
function JD(O: TJSONObject; const K: string; Def: Double): Double;
function JB(O: TJSONObject; const K: string; Def: Boolean): Boolean;
function JS(O: TJSONObject; const K: string; const Def: string): string;
procedure JW(O: TJSONObject; const K: string; V: Integer); overload;
procedure JW(O: TJSONObject; const K: string; V: Double); overload;
procedure JW(O: TJSONObject; const K: string; V: Boolean); overload;
procedure JWS(O: TJSONObject; const K: string; const V: string);
procedure MigrateXvtrTemplate(var X: TXvtrSettings;
const AName: string; ABegin, AEnd, ALO: Double;
AFullDup: Boolean = False; ATxLO: Double = 0.0);
public
constructor Create(const FilePath: string = '');
destructor Destroy; override;
procedure Load;
procedure Save;
function LoadDevice(const MAC: array of Byte;
out G: TGlobalSettings;
var Bands: array of TBandSettings): Boolean;
procedure SaveGlobal(const MAC: array of Byte; const G: TGlobalSettings);
procedure SaveBand(const MAC: array of Byte; BandIdx: Integer;
const B: TBandSettings);
function LoadBand(const MAC: array of Byte; BandIdx: Integer;
out B: TBandSettings): Boolean;
// TX settings per-device (JSON-секция "tx" под MAC).
// LoadTX возвращает True если устройство уже зарегистрировано в JSON;
// в любом случае T заполняется (дефолтами или сохранёнными значениями).
function LoadTX(const MAC: array of Byte; out T: TTXSettings): Boolean;
procedure SaveTX(const MAC: array of Byte; const T: TTXSettings);
class function MacToStr(const MAC: array of Byte): string;
class procedure DefaultBand(BandIdx: Integer; out B: TBandSettings);
class procedure DefaultGlobal(out G: TGlobalSettings);
class procedure DefaultCalibration(out C: TCalibration);
class procedure DefaultTX(out T: TTXSettings);
// Размер окна — не привязан к MAC, хранится в корне JSON
procedure SaveWindowBounds(L, T, W, H, DPI: Integer; Maximized: Boolean);
procedure LoadWindowBounds(out L, T, W, H, DPI: Integer; out Maximized: Boolean);
procedure SaveStartupPreview(VfoA, VfoB: Double; SampleRate: Integer);
function LoadStartupPreview(out VfoA, VfoB: Double; out SampleRate: Integer): Boolean;
procedure SaveAudioBufferSize(BufferSize: Integer);
function LoadAudioBufferSize: Integer;
// CAT settings — не привязаны к MAC, хранятся в корне JSON (доступны до подключения)
procedure SaveCATSettings(const G: TGlobalSettings);
procedure LoadCATSettings(var G: TGlobalSettings);
// Alex (antenna) settings per-device (JSON-секция "alex" под MAC).
class procedure DefaultAnt936x(out A: TAD936xAnt);
function LoadAnt936x(const MAC: array of Byte; out A: TAD936xAnt): Boolean;
procedure SaveAnt936x(const MAC: array of Byte; const A: TAD936xAnt);
class procedure DefaultAlex(out A: TAlexSettings);
function LoadAlex(const MAC: array of Byte; out A: TAlexSettings): Boolean;
procedure SaveAlex(const MAC: array of Byte; const A: TAlexSettings);
// XVTR (Transverter) settings per-device (JSON-секция "xvtr" под MAC).
class procedure DefaultXvtr(out X: TXvtrSettings);
function LoadXvtr(const MAC: array of Byte; out X: TXvtrSettings): Boolean;
procedure SaveXvtr(const MAC: array of Byte; const X: TXvtrSettings);
// Панадаптеры + слайсы per-device (JSON-секция "pans" под MAC). Ctx —
// ключ контекста ('hf' или 'xvtr_N'): КВ и каждый трансвертер раздельно.
class procedure DefaultPans(out P: TPansConfig);
function LoadPans(const MAC: array of Byte; const Ctx: string; out P: TPansConfig): Boolean;
procedure SavePans(const MAC: array of Byte; const Ctx: string; const P: TPansConfig);
// Web-сервер — глобальные настройки (секция "web" в корне JSON).
class procedure DefaultWeb(out W: TWebSettings);
procedure LoadWebSettings(out W: TWebSettings);
procedure SaveWebSettings(const W: TWebSettings);
// Тема — глобальная настройка, не привязана к устройству
procedure SaveTheme(ALightTheme: Boolean);
function LoadTheme: Boolean;
end;
implementation
function LegacyAverageTimeMS(FPS, AvgMode, AvgCount: Integer;
Backmult, DefaultMS: Double): Double;
var
FPSv: Integer;
begin
if AvgMode <= 0 then
Exit(0.0);
FPSv := EnsureRange(FPS, 1, 100);
if (Backmult > 0.0) and (Backmult < 1.0) then
Result := -1000.0 / (FPSv * Ln(Backmult))
else
Result := (1000.0 * Max(1, AvgCount)) / FPSv;
if IsNan(Result) or IsInfinite(Result) or (Result <= 0.0) then
Result := DefaultMS;
Result := EnsureRange(Result, 10.0, 5000.0);
end;
class function TSettingsManager.MacToStr(const MAC: array of Byte): string;
begin
Result := Format('%02X:%02X:%02X:%02X:%02X:%02X',
[MAC[0],MAC[1],MAC[2],MAC[3],MAC[4],MAC[5]]);
end;
class procedure TSettingsManager.DefaultBand(BandIdx: Integer; out B: TBandSettings);
begin
FillChar(B, SizeOf(B), 0);
B.VfoA := CFG_BAND_DEFAULT_FREQ[BandIdx];
B.VfoB := CFG_BAND_DEFAULT_FREQ[BandIdx];
B.Mode := CFG_BAND_DEFAULT_MODE[BandIdx];
B.FilterIdx := 5;
B.FilterBW := 2700;
B.AGCMode := 1;
B.AGCTop := 90;
B.CTun := False;
B.CenterHz := 0; // 0 = нет сохранённого центра → центрировать на VFO
B.SpanHz := 192000;
B.RxGainMode := 2; // slow_attack (как глобальный дефолт Pluto)
B.RxGainDb := 40;
B.FMSQOn := False;
B.FMSQLevel := 30;
B.CTCSSOn := False;
B.CTCSSToneIdx := 0;
B.FMStepOn := True;
B.FMStepIdx := 3; // 25 kHz
B.FMRptDir := 0;
B.FMRptOffsetHz := 0.0;
B.ZoomFactor := 0.0;
B.PanSlider := 0.5;
end;
class procedure TSettingsManager.DefaultGlobal(out G: TGlobalSettings);
var i: Integer;
begin
FillChar(G, SizeOf(G), 0);
G.Volume := 70; G.TxMonVolume := 40; G.DriveLevel := 50; G.ActiveVfo := 0;
G.NRMode := 0; G.NBMode := 0; G.SNBEnabled := False; G.ANFEnabled := False;
G.AGCSlope := 0; G.AGCHangThreshold := 100; G.LastBand := 5;
G.LastXvtr := -1;
G.WfAGCEnabled := True;
G.WfNFEnabled := False;
G.SampleRate := 192000;
G.RxGainMode := 2; G.RxGainDb := 40;
G.PAMaxPower := 100.0;
G.PlutoTxMaxAttDb := -10.0;
for i := 0 to CFG_BAND_COUNT-1 do G.PABandCal[i] := CFG_PA_BAND_CAL_DEFAULT[i];
for i := 0 to CFG_XVTR_COUNT-1 do G.VHFBandCal[i] := CFG_VHF_CAL_DEFAULT;
// Display defaults
G.FFTSize := 131072;
G.WindowType := 2; // Hann
G.SpecDetector := 0; // Peak
G.SpecAvgMode := 3; // Log Recursive
G.SpecAvgTimeMS := 30.0;
G.WfDetector := 0; // Peak
G.WfAvgMode := 3; // Log Recursive
G.WfAvgTimeMS := 120.0;
G.WfManualHigh := -80.0;
G.WfManualLow := -130.0;
G.WfAGCOffset := 0.0;
G.WfPalette := 0; // Classic
G.WfGamma := 1.0;
G.DisplayPixels := 4096; // 1:1 до 4K-ширины
G.WfSpeedDiv := 1; // полная скорость
G.SpecMSAA := 4; // сглаживание кривой спектра
G.SpecRefLevel := -20.0;
G.SpecRange := 110.0;
G.SpecGridStep := 10.0;
G.AudioOutDevice := '';
G.AudioSampleRate := 48000;
G.AudioInDevice := '';
G.SendAudioToRadio := False;
G.RawFMDeviation := RAW_FM_DEV_DEFAULT;
G.ShowSpectrum := True;
G.ShowWaterfall := True;
G.ShowWideband := False;
G.WidebandFill := False;
G.SpectrumFill := True;
G.DisplayDuplex := False;
G.RxMuteOnTx := True;
G.DisplayFPS := 60;
G.FreqMhzDigits := 3;
// CAT defaults
for i := 0 to 3 do begin
G.CATSerialEnabled[i] := False;
{$IFDEF MSWINDOWS}
G.CATSerialPort[i] := 'COM' + IntToStr(i+1);
{$ELSE}
G.CATSerialPort[i] := '/dev/ttyS' + IntToStr(i);
{$ENDIF}
G.CATSerialBaud[i] := 9600;
G.CATSerialDataBits[i] := 8;
G.CATSerialStopBits[i] := 1;
G.CATSerialParity[i] := 0; // None
G.CATSerialAndromeda[i]:= False;
end;
G.CATTcpEnabled := False;
G.CATTcpPort := 19090;
// ADC — включены по умолчанию: поддерживаются на Hermes, Angelia, Orion
G.DitherEnabled := True;
G.RandomEnabled := True;
DefaultCalibration(G.Cal);
end;
class procedure TSettingsManager.DefaultCalibration(out C: TCalibration);
var i: Integer;
begin
FillChar(C, SizeOf(C), 0);
C.PwrDetModel := 0; // ANAN-10/100 — совпадает с прежней зашитой формулой
for i := 0 to CFG_BAND_COUNT-1 do C.PADetCal[i] := 1.0;
C.SupplyVoltCal := 1.0;
C.AmpVoffMV := 360.0;
C.AmpSensMVA := 120.0;
// SMeterBandDB/SMeterXvtrDB — нули после FillChar (без коррекции).
end;
constructor TSettingsManager.Create(const FilePath: string);
begin
inherited Create;
if FilePath = '' then
FFilePath := GetAppCfgDir + SETTINGS_FILE
else
FFilePath := FilePath;
FRoot := TJSONObject.Create;
end;
destructor TSettingsManager.Destroy;
begin
FRoot.Free;
inherited;
end;
procedure TSettingsManager.Load;
var F: TFileStream; P: TJSONParser; D: TJSONData;
begin
if not FileExists(FFilePath) then Exit;
try
F := TFileStream.Create(FFilePath, fmOpenRead or fmShareDenyNone);
try
P := TJSONParser.Create(F, [joUTF8]);
try
D := P.Parse;
if (D <> nil) and (D is TJSONObject) then
begin FRoot.Free; FRoot := TJSONObject(D); end
else
D.Free;
finally P.Free; end;
finally F.Free; end;
except
FreeAndNil(FRoot);
FRoot := TJSONObject.Create;
end;
end;
procedure TSettingsManager.Save;
var
S: string;
F: TFileStream;
Buf: TBytes;
begin
try
S := FRoot.FormatJSON([], 2);
Buf := TEncoding.UTF8.GetBytes(S);
F := TFileStream.Create(FFilePath, fmCreate);
try
if Length(Buf) > 0 then
F.WriteBuffer(Buf[0], Length(Buf));
finally F.Free; end;
except end;
end;
function TSettingsManager.EnsureObj(P: TJSONObject; const K: string): TJSONObject;
var D: TJSONData; Idx: Integer;
begin
D := P.Find(K);
if (D <> nil) and (D is TJSONObject) then
Result := TJSONObject(D)
else
begin
if D <> nil then begin Idx := P.IndexOfName(K); if Idx >= 0 then P.Delete(Idx); end;
Result := TJSONObject.Create;
P.Add(K, Result);
end;
end;
function TSettingsManager.GetDevObj(const M: string): TJSONObject;
begin Result := EnsureObj(FRoot, M); end;
function TSettingsManager.GetGlobalObj(D: TJSONObject): TJSONObject;
begin Result := EnsureObj(D, 'global'); end;
function TSettingsManager.GetBandObj(D: TJSONObject; Idx: Integer): TJSONObject;
begin Result := EnsureObj(EnsureObj(D, 'bands'), IntToStr(Idx)); end;
function TSettingsManager.JI(O: TJSONObject; const K: string; Def: Integer): Integer;
var D: TJSONData;
begin D := O.Find(K); if D<>nil then try Result:=D.AsInteger; except Result:=Def; end else Result:=Def; end;
function TSettingsManager.JD(O: TJSONObject; const K: string; Def: Double): Double;
var D: TJSONData;
begin D := O.Find(K); if D<>nil then try Result:=D.AsFloat; except Result:=Def; end else Result:=Def; end;
function TSettingsManager.JB(O: TJSONObject; const K: string; Def: Boolean): Boolean;
var D: TJSONData;
begin D := O.Find(K); if D<>nil then try Result:=D.AsBoolean; except Result:=Def; end else Result:=Def; end;
procedure TSettingsManager.JW(O: TJSONObject; const K: string; V: Integer);
var Idx: Integer;
begin
Idx := O.IndexOfName(K); if Idx >= 0 then O.Delete(Idx);
O.Add(K, V);
end;
procedure TSettingsManager.JW(O: TJSONObject; const K: string; V: Double);
var Idx: Integer;
begin
Idx := O.IndexOfName(K); if Idx >= 0 then O.Delete(Idx);
O.Add(K, V);
end;
procedure TSettingsManager.JW(O: TJSONObject; const K: string; V: Boolean);
var Idx: Integer;
begin
Idx := O.IndexOfName(K); if Idx >= 0 then O.Delete(Idx);
O.Add(K, V);
end;
function TSettingsManager.JS(O: TJSONObject; const K: string; const Def: string): string;
var D: TJSONData;
begin D := O.Find(K); if D<>nil then try Result:=D.AsString; except Result:=Def; end else Result:=Def; end;
procedure TSettingsManager.JWS(O: TJSONObject; const K: string; const V: string);
var Idx: Integer;
begin
Idx := O.IndexOfName(K); if Idx >= 0 then O.Delete(Idx);
O.Add(K, V);
end;
function TSettingsManager.LoadDevice(const MAC: array of Byte;
out G: TGlobalSettings;
var Bands: array of TBandSettings): Boolean;
var MacStr: string; DevObj,GObj,BObj: TJSONObject; i: Integer; SMLegacy: Double;
begin
MacStr := MacToStr(MAC);
Result := FRoot.Find(MacStr) <> nil;
DevObj := GetDevObj(MacStr);
GObj := GetGlobalObj(DevObj);
G.Volume := JI(GObj,'volume',70);
G.TxMonVolume := JI(GObj,'tx_mon_volume',40);
G.DriveLevel := JI(GObj,'drive_level',50);
G.ActiveVfo := JI(GObj,'active_vfo',0);
G.NRMode := JI(GObj,'nr_mode',Ord(JB(GObj,'nr_enabled',False)));
G.NBMode := JI(GObj,'nb_mode',Ord(JB(GObj,'nb_enabled',False)));
G.SNBEnabled := JB(GObj,'snb_enabled',False);
G.ANFEnabled := JB(GObj,'anf_enabled',False);
G.AGCSlope := JI(GObj,'agc_slope',0);
G.AGCHangThreshold := JI(GObj,'agc_hang_threshold',100);
G.WfAGCEnabled := JB(GObj,'wf_agc_enabled',True);
G.WfNFEnabled := JB(GObj,'wf_nf_enabled',False);
G.LastBand := JI(GObj,'last_band',5);
G.LastXvtr := JI(GObj,'last_xvtr',-1);
G.SampleRate := JI(GObj,'sample_rate',192000);
G.RxGainMode := JI(GObj,'rx_gain_mode',2);
G.RxGainDb := JI(GObj,'rx_gain_db',40);
// Display settings
G.FFTSize := JI(GObj,'fft_size',131072);
G.WindowType := JI(GObj,'win_type',2);
G.SpecDetector := JI(GObj,'spec_detector',0);
G.SpecAvgMode := JI(GObj,'spec_avg_mode',3);
if GObj.Find('spec_avg_time_ms') <> nil then
G.SpecAvgTimeMS := JD(GObj,'spec_avg_time_ms',30.0)
else if (GObj.Find('spec_avg_count') <> nil) or
(GObj.Find('spec_backmult') <> nil) then
begin
G.SpecAvgTimeMS := LegacyAverageTimeMS(
JI(GObj,'display_fps',60),
G.SpecAvgMode,
JI(GObj,'spec_avg_count',1),
JD(GObj,'spec_backmult',0.0),
30.0);
end
else
G.SpecAvgTimeMS := 30.0;
G.WfDetector := JI(GObj,'wf_detector',0);
G.WfAvgMode := JI(GObj,'wf_avg_mode',3);
if GObj.Find('wf_avg_time_ms') <> nil then
G.WfAvgTimeMS := JD(GObj,'wf_avg_time_ms',120.0)
else if (GObj.Find('wf_avg_count') <> nil) or
(GObj.Find('wf_backmult') <> nil) then
begin
G.WfAvgTimeMS := LegacyAverageTimeMS(
JI(GObj,'display_fps',60),
G.WfAvgMode,
JI(GObj,'wf_avg_count',2),
JD(GObj,'wf_backmult',0.25),
120.0);
end
else
G.WfAvgTimeMS := 120.0;
G.WfManualHigh := JD(GObj,'wf_manual_high',-80.0);
G.WfManualLow := JD(GObj,'wf_manual_low',-130.0);
G.WfAGCOffset := JD(GObj,'wf_agc_offset',0.0);
G.WfPalette := JI(GObj,'wf_palette',0);
G.WfGamma := JD(GObj,'wf_gamma',1.0);
G.DisplayPixels := JI(GObj,'display_pixels',4096);
G.WfSpeedDiv := JI(GObj,'wf_speed_div',1);
G.SpecMSAA := JI(GObj,'spec_msaa',4);
G.SpecRefLevel := JD(GObj,'spec_ref_level',-20.0);
G.SpecRange := JD(GObj,'spec_range',110.0);
G.SpecGridStep := JD(GObj,'spec_grid_step',10.0);
// Audio settings
G.AudioOutDevice := JS(GObj,'audio_out_device','');
G.AudioSampleRate := JI(GObj,'audio_sample_rate',48000);
G.AudioInDevice := JS(GObj,'audio_in_device','');
G.SendAudioToRadio := JB(GObj,'send_audio_to_radio',False);
G.RawFMDeviation := EnsureRange(
JI(GObj,'raw_fm_deviation',RAW_FM_DEV_DEFAULT),
RAW_FM_DEV_MIN, RAW_FM_DEV_MAX);
// Visibility settings
G.ShowSpectrum := JB(GObj,'show_spectrum',True);
G.ShowWaterfall := JB(GObj,'show_waterfall',True);
G.ShowWideband := JB(GObj,'show_wideband',False);
G.WidebandFill := JB(GObj,'wideband_fill',False);
G.SpectrumFill := JB(GObj,'spectrum_fill',True);
G.DisplayDuplex := JB(GObj,'display_duplex',False);
G.RxMuteOnTx := JB(GObj,'rx_mute_on_tx',True);
G.DisplayFPS := JI(GObj,'display_fps',60);
G.LightTheme := JB(GObj,'light_theme',False);
G.FreqMhzDigits := EnsureRange(JI(GObj,'freq_mhz_digits',3), 3, 5);
// PA settings
G.PAMaxPower := JD(GObj,'pa_max_power',100.0);
G.PlutoTxMaxAttDb := JD(GObj,'pluto_tx_max_att',-10.0);
for i := 0 to CFG_BAND_COUNT-1 do
G.PABandCal[i] := JD(GObj,'pa_band_cal_'+IntToStr(i), CFG_PA_BAND_CAL_DEFAULT[i]);
for i := 0 to CFG_XVTR_COUNT-1 do
G.VHFBandCal[i] := JD(GObj,'vhf_cal_'+IntToStr(i), CFG_VHF_CAL_DEFAULT);
// CAT serial settings
for i := 0 to 3 do begin
G.CATSerialEnabled[i] := JB(GObj,'cat_serial_en_'+IntToStr(i), False);
{$IFDEF MSWINDOWS}
G.CATSerialPort[i] := JS(GObj,'cat_serial_port_'+IntToStr(i), 'COM'+IntToStr(i+1));
{$ELSE}
G.CATSerialPort[i] := JS(GObj,'cat_serial_port_'+IntToStr(i), '/dev/ttyS'+IntToStr(i));
{$ENDIF}
G.CATSerialBaud[i] := JI(GObj,'cat_serial_baud_'+IntToStr(i), 9600);
G.CATSerialDataBits[i] := JI(GObj,'cat_serial_dbits_'+IntToStr(i), 8);
G.CATSerialStopBits[i] := JI(GObj,'cat_serial_sbits_'+IntToStr(i), 1);
G.CATSerialParity[i] := JI(GObj,'cat_serial_parity_'+IntToStr(i), 0);
G.CATSerialAndromeda[i]:= JB(GObj,'cat_serial_androm_'+IntToStr(i), False);
end;
G.CATTcpEnabled := JB(GObj,'cat_tcp_enabled', False);
G.CATTcpPort := JI(GObj,'cat_tcp_port', 19090);
// ADC settings
G.DitherEnabled := JB(GObj,'dither_enabled', True);
G.RandomEnabled := JB(GObj,'random_enabled', True);
// Calibration (детектор мощности/КСВ, напряжение/ток, S-метр)
G.Cal.PwrDetModel := EnsureRange(JI(GObj,'cal_pwr_det_model', 0), 0, 3);
for i := 0 to CFG_BAND_COUNT-1 do
G.Cal.PADetCal[i] := EnsureRange(JD(GObj,'cal_pa_det_'+IntToStr(i), 1.0), 0.5, 2.0);
G.Cal.SupplyVoltCal := EnsureRange(JD(GObj,'cal_supply_volt', 1.0), 0.1, 5.0);
G.Cal.AmpVoffMV := EnsureRange(JD(GObj,'cal_amp_voff_mv', 360.0), 0.0, 2000.0);
G.Cal.AmpSensMVA := EnsureRange(JD(GObj,'cal_amp_sens_mva', 120.0), 1.0, 1000.0);
// S-метр: офсет на диапазон/трансвертер. Легаси-ключ (одно значение на всё
// устройство) становится дефолтом всех слотов — показания не меняются.
SMLegacy := EnsureRange(JD(GObj,'cal_smeter_db', 0.0), -30.0, 30.0);
for i := 0 to CFG_BAND_COUNT-1 do
G.Cal.SMeterBandDB[i] :=
EnsureRange(JD(GObj,'cal_smeter_band_'+IntToStr(i), SMLegacy), -30.0, 30.0);
for i := 0 to CFG_XVTR_COUNT-1 do
G.Cal.SMeterXvtrDB[i] :=
EnsureRange(JD(GObj,'cal_smeter_xvtr_'+IntToStr(i), SMLegacy), -30.0, 30.0);
// Спектр/водопад: своя таблица офсетов (дефолт 0 — как было).
for i := 0 to CFG_BAND_COUNT-1 do
G.Cal.DispBandDB[i] :=
EnsureRange(JD(GObj,'cal_disp_band_'+IntToStr(i), 0.0), -30.0, 30.0);
for i := 0 to CFG_XVTR_COUNT-1 do
G.Cal.DispXvtrDB[i] :=
EnsureRange(JD(GObj,'cal_disp_xvtr_'+IntToStr(i), 0.0), -30.0, 30.0);
for i := 0 to CFG_BAND_COUNT-1 do
begin
DefaultBand(i, Bands[i]);
BObj := GetBandObj(DevObj, i);
Bands[i].VfoA := JD(BObj,'vfo_a', Bands[i].VfoA);
Bands[i].VfoB := JD(BObj,'vfo_b', Bands[i].VfoB);
Bands[i].Mode := EnsureRange(JI(BObj,'mode', Bands[i].Mode), CFG_MODE_MIN, CFG_MODE_MAX);
Bands[i].FilterIdx := JI(BObj,'filter_idx', Bands[i].FilterIdx);
Bands[i].FilterBW := JI(BObj,'filter_bw', Bands[i].FilterBW);
Bands[i].AGCMode := JI(BObj,'agc_mode', Bands[i].AGCMode);
Bands[i].AGCTop := JI(BObj,'agc_top', Bands[i].AGCTop);
Bands[i].CTun := JB(BObj,'ctun', Bands[i].CTun);
Bands[i].CenterHz := JD(BObj,'center_hz', Bands[i].CenterHz);
Bands[i].SpanHz := JD(BObj,'span_hz', Bands[i].SpanHz);
// FM squelch / CTCSS / step / repeater — как в LoadBand (раньше пропускались
// здесь → на старте сбрасывались в дефолт, хотя SaveBand их пишет).
Bands[i].FMSQOn := JB(BObj,'fmsq_on', Bands[i].FMSQOn);
Bands[i].FMSQLevel := JI(BObj,'fmsq_level', Bands[i].FMSQLevel);
Bands[i].CTCSSOn := JB(BObj,'ctcss_on', Bands[i].CTCSSOn);
Bands[i].CTCSSToneIdx := EnsureRange(JI(BObj,'ctcss_idx', Bands[i].CTCSSToneIdx), 0, 37);
Bands[i].FMStepOn := JB(BObj,'fmstep_on', Bands[i].FMStepOn);
Bands[i].FMStepIdx := EnsureRange(JI(BObj,'fmstep_idx', Bands[i].FMStepIdx), 0, 3);
Bands[i].FMRptDir := EnsureRange(JI(BObj,'fmrpt_dir', Bands[i].FMRptDir), 0, 2);
Bands[i].FMRptOffsetHz := JD(BObj,'fmrpt_offset', Bands[i].FMRptOffsetHz);
Bands[i].RxGainMode := EnsureRange(JI(BObj,'rx_gain_mode', Bands[i].RxGainMode), 0, 3);
Bands[i].RxGainDb := EnsureRange(JI(BObj,'rx_gain_db', Bands[i].RxGainDb), 0, 73);
end;
end;
procedure TSettingsManager.SaveGlobal(const MAC: array of Byte;
const G: TGlobalSettings);
var O: TJSONObject; i: Integer;
begin
O := GetGlobalObj(GetDevObj(MacToStr(MAC)));
JW(O,'volume',G.Volume); JW(O,'tx_mon_volume',G.TxMonVolume); JW(O,'drive_level',G.DriveLevel);
JW(O,'active_vfo',G.ActiveVfo);
JW(O,'nr_mode',G.NRMode); JW(O,'nb_mode',G.NBMode);
JW(O,'nr_enabled',G.NRMode <> 0); JW(O,'nb_enabled',G.NBMode <> 0);
JW(O,'snb_enabled',G.SNBEnabled);
JW(O,'anf_enabled',G.ANFEnabled);
JW(O,'agc_slope',G.AGCSlope);
JW(O,'agc_hang_threshold',G.AGCHangThreshold);
JW(O,'wf_agc_enabled',G.WfAGCEnabled);
JW(O,'wf_nf_enabled',G.WfNFEnabled);
JW(O,'last_band',G.LastBand);
JW(O,'last_xvtr',G.LastXvtr);
JW(O,'sample_rate',G.SampleRate);
JW(O,'rx_gain_mode',G.RxGainMode);
JW(O,'rx_gain_db',G.RxGainDb);
// Display settings
JW(O,'fft_size',G.FFTSize);
JW(O,'win_type',G.WindowType);
JW(O,'spec_detector',G.SpecDetector);
JW(O,'spec_avg_mode',G.SpecAvgMode);
JW(O,'spec_avg_time_ms',G.SpecAvgTimeMS);
JW(O,'wf_detector',G.WfDetector);
JW(O,'wf_avg_mode',G.WfAvgMode);
JW(O,'wf_avg_time_ms',G.WfAvgTimeMS);
JW(O,'wf_manual_high',G.WfManualHigh);
JW(O,'wf_manual_low',G.WfManualLow);
JW(O,'wf_agc_offset',G.WfAGCOffset);
JW(O,'wf_palette',G.WfPalette);
JW(O,'wf_gamma',G.WfGamma);
JW(O,'display_pixels',G.DisplayPixels);
JW(O,'wf_speed_div',G.WfSpeedDiv);
JW(O,'spec_msaa',G.SpecMSAA);
JW(O,'spec_ref_level',G.SpecRefLevel);
JW(O,'spec_range',G.SpecRange);
JW(O,'spec_grid_step',G.SpecGridStep);
// Audio settings
JWS(O,'audio_out_device',G.AudioOutDevice);
JW(O,'audio_sample_rate',G.AudioSampleRate);
JWS(O,'audio_in_device',G.AudioInDevice);
JW(O,'send_audio_to_radio',G.SendAudioToRadio);
JW(O,'raw_fm_deviation',G.RawFMDeviation);
// Visibility settings
JW(O,'show_spectrum',G.ShowSpectrum);
JW(O,'show_waterfall',G.ShowWaterfall);
JW(O,'show_wideband',G.ShowWideband);
JW(O,'wideband_fill',G.WidebandFill);
JW(O,'spectrum_fill',G.SpectrumFill);
JW(O,'display_duplex',G.DisplayDuplex);
JW(O,'rx_mute_on_tx',G.RxMuteOnTx);
JW(O,'display_fps',G.DisplayFPS);
JW(O,'light_theme',G.LightTheme);
JW(O,'freq_mhz_digits',G.FreqMhzDigits);
// PA settings
JW(O,'pa_max_power',G.PAMaxPower);
JW(O,'pluto_tx_max_att',G.PlutoTxMaxAttDb);
for i := 0 to CFG_BAND_COUNT-1 do
JW(O,'pa_band_cal_'+IntToStr(i),G.PABandCal[i]);
for i := 0 to CFG_XVTR_COUNT-1 do
JW(O,'vhf_cal_'+IntToStr(i),G.VHFBandCal[i]);
// CAT serial settings
for i := 0 to 3 do begin
JW(O,'cat_serial_en_'+IntToStr(i), G.CATSerialEnabled[i]);
JWS(O,'cat_serial_port_'+IntToStr(i),G.CATSerialPort[i]);
JW(O,'cat_serial_baud_'+IntToStr(i), G.CATSerialBaud[i]);
JW(O,'cat_serial_dbits_'+IntToStr(i),G.CATSerialDataBits[i]);
JW(O,'cat_serial_sbits_'+IntToStr(i),G.CATSerialStopBits[i]);
JW(O,'cat_serial_parity_'+IntToStr(i),G.CATSerialParity[i]);
JW(O,'cat_serial_androm_'+IntToStr(i),G.CATSerialAndromeda[i]);
end;
JW(O,'cat_tcp_enabled',G.CATTcpEnabled);
JW(O,'cat_tcp_port',G.CATTcpPort);
// ADC settings
JW(O,'dither_enabled',G.DitherEnabled);
JW(O,'random_enabled',G.RandomEnabled);
// Calibration
JW(O,'cal_pwr_det_model',G.Cal.PwrDetModel);
for i := 0 to CFG_BAND_COUNT-1 do
JW(O,'cal_pa_det_'+IntToStr(i),G.Cal.PADetCal[i]);
JW(O,'cal_supply_volt',G.Cal.SupplyVoltCal);
JW(O,'cal_amp_voff_mv',G.Cal.AmpVoffMV);
JW(O,'cal_amp_sens_mva',G.Cal.AmpSensMVA);
for i := 0 to CFG_BAND_COUNT-1 do
JW(O,'cal_smeter_band_'+IntToStr(i),G.Cal.SMeterBandDB[i]);
for i := 0 to CFG_XVTR_COUNT-1 do
JW(O,'cal_smeter_xvtr_'+IntToStr(i),G.Cal.SMeterXvtrDB[i]);
for i := 0 to CFG_BAND_COUNT-1 do
JW(O,'cal_disp_band_'+IntToStr(i),G.Cal.DispBandDB[i]);
for i := 0 to CFG_XVTR_COUNT-1 do
JW(O,'cal_disp_xvtr_'+IntToStr(i),G.Cal.DispXvtrDB[i]);
end;
procedure TSettingsManager.SaveBand(const MAC: array of Byte; BandIdx: Integer;
const B: TBandSettings);
var O: TJSONObject;
begin
if (BandIdx < 0) or (BandIdx >= CFG_BAND_COUNT) then Exit;
O := GetBandObj(GetDevObj(MacToStr(MAC)), BandIdx);
JW(O,'vfo_a',B.VfoA); JW(O,'vfo_b',B.VfoB);
JW(O,'mode',B.Mode);
JW(O,'filter_idx',B.FilterIdx); JW(O,'filter_bw',B.FilterBW);
JW(O,'agc_mode',B.AGCMode); JW(O,'agc_top',B.AGCTop);
JW(O,'ctun',B.CTun); JW(O,'center_hz',B.CenterHz); JW(O,'span_hz',B.SpanHz);
JW(O,'fmsq_on',B.FMSQOn); JW(O,'fmsq_level',B.FMSQLevel);
JW(O,'ctcss_on',B.CTCSSOn); JW(O,'ctcss_idx',B.CTCSSToneIdx);
JW(O,'fmstep_on',B.FMStepOn); JW(O,'fmstep_idx',B.FMStepIdx);
JW(O,'fmrpt_dir',B.FMRptDir); JW(O,'fmrpt_offset',B.FMRptOffsetHz);
JW(O,'zoom_factor',B.ZoomFactor); JW(O,'pan_slider',B.PanSlider);
JW(O,'rx_gain_mode',B.RxGainMode); JW(O,'rx_gain_db',B.RxGainDb);
end;
function TSettingsManager.LoadBand(const MAC: array of Byte; BandIdx: Integer;
out B: TBandSettings): Boolean;
var MacStr: string; O: TJSONObject;
begin
DefaultBand(BandIdx, B);
MacStr := MacToStr(MAC);
Result := FRoot.Find(MacStr) <> nil;
if not Result then Exit;
O := GetBandObj(GetDevObj(MacStr), BandIdx);
B.VfoA := JD(O,'vfo_a', B.VfoA);
B.VfoB := JD(O,'vfo_b', B.VfoB);
B.Mode := EnsureRange(JI(O,'mode', B.Mode), CFG_MODE_MIN, CFG_MODE_MAX);
B.FilterIdx := JI(O,'filter_idx', B.FilterIdx);
B.FilterBW := JI(O,'filter_bw', B.FilterBW);
B.AGCMode := JI(O,'agc_mode', B.AGCMode);
B.AGCTop := JI(O,'agc_top', B.AGCTop);
B.CTun := JB(O,'ctun', B.CTun);
B.CenterHz := JD(O,'center_hz', B.CenterHz);
B.SpanHz := JD(O,'span_hz', B.SpanHz);
B.FMSQOn := JB(O,'fmsq_on', B.FMSQOn);
B.FMSQLevel := JI(O,'fmsq_level', B.FMSQLevel);
B.CTCSSOn := JB(O,'ctcss_on', B.CTCSSOn);
B.CTCSSToneIdx := EnsureRange(JI(O,'ctcss_idx', B.CTCSSToneIdx), 0, 37);
B.FMStepOn := JB(O,'fmstep_on', B.FMStepOn);
B.FMStepIdx := EnsureRange(JI(O,'fmstep_idx', B.FMStepIdx), 0, 3);
B.FMRptDir := EnsureRange(JI(O,'fmrpt_dir', B.FMRptDir), 0, 2);
B.FMRptOffsetHz := JD(O,'fmrpt_offset', B.FMRptOffsetHz);
B.RxGainMode := EnsureRange(JI(O,'rx_gain_mode', B.RxGainMode), 0, 3);
B.RxGainDb := EnsureRange(JI(O,'rx_gain_db', B.RxGainDb), 0, 73);
B.ZoomFactor := EnsureRange(JD(O,'zoom_factor', B.ZoomFactor), 0.0, 1.0);
B.PanSlider := EnsureRange(JD(O,'pan_slider', B.PanSlider), 0.0, 1.0);
end;
procedure TSettingsManager.SaveWindowBounds(L, T, W, H, DPI: Integer; Maximized: Boolean);
var O: TJSONObject;
begin
O := EnsureObj(FRoot, 'window');
JW(O, 'left', L);
JW(O, 'top', T);
JW(O, 'width', W);
JW(O, 'height', H);
JW(O, 'dpi', DPI);
JW(O, 'maximized', Maximized);
end;
procedure TSettingsManager.LoadWindowBounds(out L, T, W, H, DPI: Integer; out Maximized: Boolean);
var O: TJSONObject;
begin
L := 80; T := 80; W := 1400; H := 900; DPI := 0; Maximized := False;
if FRoot.Find('window') = nil then Exit;
O := EnsureObj(FRoot, 'window');
L := JI(O, 'left', 80);
T := JI(O, 'top', 80);
W := JI(O, 'width', 1400);
H := JI(O, 'height', 900);
DPI := JI(O, 'dpi', 0);
Maximized := JB(O, 'maximized', False);
end;
procedure TSettingsManager.SaveStartupPreview(VfoA, VfoB: Double; SampleRate: Integer);
var O: TJSONObject;
begin
O := EnsureObj(FRoot, 'startup');
JW(O, 'vfo_a', VfoA);
JW(O, 'vfo_b', VfoB);
JW(O, 'sample_rate', SampleRate);
end;
function TSettingsManager.LoadStartupPreview(out VfoA, VfoB: Double; out SampleRate: Integer): Boolean;
var O: TJSONObject;
begin
Result := FRoot.Find('startup') <> nil;
if not Result then Exit;
O := EnsureObj(FRoot, 'startup');
VfoA := JD(O, 'vfo_a', 14200000);
VfoB := JD(O, 'vfo_b', 7100000);
SampleRate := JI(O, 'sample_rate', 192000);
end;
procedure TSettingsManager.SaveAudioBufferSize(BufferSize: Integer);
var O: TJSONObject;
begin
if BufferSize <= 128 then BufferSize := 128
else if BufferSize <= 256 then BufferSize := 256
else BufferSize := 512;
O := EnsureObj(FRoot, 'audio');
JW(O, 'output_buffer_size', BufferSize);
end;
function TSettingsManager.LoadAudioBufferSize: Integer;
var O: TJSONObject;
begin
if FRoot.Find('audio') = nil then Exit(128);
O := EnsureObj(FRoot, 'audio');
Result := JI(O, 'output_buffer_size', 128);
if Result <= 128 then Result := 128
else if Result <= 256 then Result := 256
else Result := 512;
end;
procedure TSettingsManager.SaveCATSettings(const G: TGlobalSettings);
var O: TJSONObject; i: Integer;
begin
O := EnsureObj(FRoot, 'cat');
JW(O, 'tcp_en', G.CATTcpEnabled);
JW(O, 'tcp_port', G.CATTcpPort);
for i := 0 to 3 do
begin
JW(O, 'ser_en_' +IntToStr(i), G.CATSerialEnabled[i]);
JWS(O, 'ser_port_' +IntToStr(i), G.CATSerialPort[i]);
JW(O, 'ser_baud_' +IntToStr(i), G.CATSerialBaud[i]);
JW(O, 'ser_dbits_' +IntToStr(i), G.CATSerialDataBits[i]);
JW(O, 'ser_sbits_' +IntToStr(i), G.CATSerialStopBits[i]);
JW(O, 'ser_parity_'+IntToStr(i), G.CATSerialParity[i]);
JW(O, 'ser_androm_'+IntToStr(i), G.CATSerialAndromeda[i]);
end;
end;
procedure TSettingsManager.LoadCATSettings(var G: TGlobalSettings);
var O: TJSONObject; i: Integer;
begin
if FRoot.Find('cat') = nil then Exit;
O := EnsureObj(FRoot, 'cat');
G.CATTcpEnabled := JB(O, 'tcp_en', False);
G.CATTcpPort := JI(O, 'tcp_port', 19090);
for i := 0 to 3 do
begin
G.CATSerialEnabled[i] := JB(O, 'ser_en_' +IntToStr(i), False);
{$IFDEF MSWINDOWS}
G.CATSerialPort[i] := JS(O, 'ser_port_' +IntToStr(i), 'COM'+IntToStr(i+1));
{$ELSE}
G.CATSerialPort[i] := JS(O, 'ser_port_' +IntToStr(i), '/dev/ttyS'+IntToStr(i));
{$ENDIF}
G.CATSerialBaud[i] := JI(O, 'ser_baud_' +IntToStr(i), 9600);
G.CATSerialDataBits[i] := JI(O, 'ser_dbits_' +IntToStr(i), 8);
G.CATSerialStopBits[i] := JI(O, 'ser_sbits_' +IntToStr(i), 1);
G.CATSerialParity[i] := JI(O, 'ser_parity_'+IntToStr(i), 0);
G.CATSerialAndromeda[i]:= JB(O, 'ser_androm_'+IntToStr(i), False);
end;
end;
// ---------------------------------------------------------------------------
// TX settings (per-device, JSON-секция "tx" под MAC)
// ---------------------------------------------------------------------------
class procedure TSettingsManager.DefaultTX(out T: TTXSettings);
var
i: Integer;
const
// 10-полосный голосовой EQ (Hz). [0]=preamp slot, [1..10]=bands.
// Подобрано под голос, а не под музыку: больше точек в 100..3500 Hz.
DefaultEQFreqs: array[0..10] of Double = (
0, // [0] preamp slot — частота не используется
100, 200, 400, 700, 1100, 1500, 2000, 2500, 3000, 3500);
begin
FillChar(T, SizeOf(T), 0);
T.MicLineIn := False;
T.MicBoost := False;
T.MicPTTEnabled := False;
T.MicTipRing := False; // Tip (mic at Tip, PTT at Ring)
T.MicBias := True;
T.LineInGainDB := 0.0; // 0 dB
T.MicGainDB := 10.0;
// ATT on TX — Thetis default 31 dB (полная защита RX-ADC во время передачи)
T.AttOnTX := 31;
// Tune — Thetis defaults
T.TUNLevel := 10;
T.TUNFreq := 750;
// SSB-фильтр по умолчанию
T.FilterLow := 100;
T.FilterHigh := 2800;
T.FilterNC := 1024;
T.FilterMP := 1; // Low Latency (как в piHPSDR)
T.FilterWindow := 1; // BH4 / Blackman-Harris
// Compressor — выкл по умолчанию
T.CompressorOn := False;
T.CompressorGain:= 5.0;
// Leveler — вкл, типичные значения piHPSDR/Thetis
T.LevelerOn := True;
T.LevelerTop := 5.0;
T.LevelerDecay := 500;
// ALC — вкл всегда (защита от перегруза DAC)
T.ALCOn := True;
T.ALCMaxGain := 0.0;
T.ALCDecay := 10;
// Phase Rotator — выкл
T.PhaseRotOn := False;
T.PhaseRotStages:= 8;
T.PhaseRotFreq := 338.0;
// EQ — выкл, нулевые гейны
T.EQOn := False;
T.EQNumBands := 10;
for i := 0 to 10 do
begin
T.EQGains[i] := 0.0;
T.EQFreqs[i] := DefaultEQFreqs[i];
end;
// AM
T.AMCarrierLevel := 0.5;
// FM
T.FMDeviation := 5000.0;
T.FMLowCut := 300;
T.FMHighCut := 3000;
T.FMEmphPosition := 1; // PostEQ
// CTCSS
T.CTCSSOn := False;
T.CTCSSFreq := 100.0;
// TX Display defaults — те же что у RX (см. DefaultGlobal)
T.TXFFTSize := 16384; // меньше чем RX — TX-полоса узкая, FFT не нужен большой
T.TXWindowType := 2; // Hann
T.TXSpecDetector := 0; // Peak
T.TXSpecAvgMode := 3; // Log Recursive
T.TXSpecAvgTimeMS := 30.0;
T.TXWfDetector := 0;
T.TXWfAvgMode := 3;
T.TXWfAvgTimeMS := 120.0;
// TX Grid — потолок 0 dBm (TX-уровни выше RX), 80 dB динамики, шаг 10 dB
T.TXSpecRefLevel := 0.0;
T.TXSpecRange := 80.0;
T.TXSpecGridStep := 10.0;
// PureSignal — дефолты Thetis PSForm (протокол 2)
T.PSMoxDelay := 0.2;
T.PSLoopDelay := 0.0;
T.PSTXDelayNs := 150;
T.PSRelaxPtol := False;
T.PSAutoAtten := True;
T.PSPin := True;
T.PSMap := True;
T.PSStabilize := False;
T.PSIntsIdx := 0; // 16 ints / 256 spi
T.PSHWPeak := 0.2899;
T.PSTTFreq1 := 700;
T.PSTTFreq2 := 1900;
T.PSFBTarget := 0; // авто по плате
T.PSOutlierSigma := -1.0; // авто по плате
end;
function TSettingsManager.LoadTX(const MAC: array of Byte;
out T: TTXSettings): Boolean;
var MacStr: string; DevObj, O: TJSONObject; i: Integer;
begin
DefaultTX(T);
MacStr := MacToStr(MAC);
Result := FRoot.Find(MacStr) <> nil;
if not Result then Exit;
DevObj := GetDevObj(MacStr);
// Если секции "tx" ещё нет — оставляем дефолты, не материализуем пустую секцию
if DevObj.Find('tx') = nil then Exit;
O := EnsureObj(DevObj, 'tx');
T.MicLineIn := JB(O,'mic_line_in', T.MicLineIn);
T.MicBoost := JB(O,'mic_boost', T.MicBoost);
T.MicPTTEnabled := JB(O,'mic_ptt_enabled', T.MicPTTEnabled);
T.MicTipRing := JB(O,'mic_tip_ring', T.MicTipRing);
T.MicBias := JB(O,'mic_bias', T.MicBias);
T.LineInGainDB := EnsureRange(JD(O,'line_in_gain_db', T.LineInGainDB), -34.5, 12.0);
T.MicGainDB := JD(O,'mic_gain_db', T.MicGainDB);
T.AttOnTX := EnsureRange(JI(O,'att_on_tx', T.AttOnTX), 0, 31);
T.TUNLevel := EnsureRange(JI(O,'tun_level', T.TUNLevel), 0, 100);
T.TUNFreq := EnsureRange(JI(O,'tun_freq', T.TUNFreq), 100, 5000);
T.FilterLow := JI(O,'filter_low', T.FilterLow);
T.FilterHigh := JI(O,'filter_high', T.FilterHigh);
T.FilterNC := JI(O,'filter_nc', T.FilterNC);
T.FilterMP := JI(O,'filter_mp', T.FilterMP);
T.FilterWindow := JI(O,'filter_window', T.FilterWindow);
T.CompressorOn := JB(O,'compressor_on', T.CompressorOn);
T.CompressorGain := JD(O,'compressor_gain', T.CompressorGain);
T.LevelerOn := JB(O,'leveler_on', T.LevelerOn);
T.LevelerTop := JD(O,'leveler_top', T.LevelerTop);
T.LevelerDecay := JI(O,'leveler_decay', T.LevelerDecay);
T.ALCOn := JB(O,'alc_on', T.ALCOn);
T.ALCMaxGain := JD(O,'alc_max_gain', T.ALCMaxGain);
T.ALCDecay := JI(O,'alc_decay', T.ALCDecay);
T.PhaseRotOn := JB(O,'phase_rot_on', T.PhaseRotOn);
T.PhaseRotStages := JI(O,'phase_rot_stages',T.PhaseRotStages);
T.PhaseRotFreq := JD(O,'phase_rot_freq', T.PhaseRotFreq);
T.EQOn := JB(O,'eq_on', T.EQOn);
T.EQNumBands := JI(O,'eq_num_bands', T.EQNumBands);
for i := 0 to 10 do
begin
T.EQGains[i] := JD(O,'eq_g_'+IntToStr(i), T.EQGains[i]);
T.EQFreqs[i] := JD(O,'eq_f_'+IntToStr(i), T.EQFreqs[i]);
end;
T.AMCarrierLevel := JD(O,'am_carrier_level',T.AMCarrierLevel);
T.FMDeviation := JD(O,'fm_deviation', T.FMDeviation);
T.FMLowCut := JI(O,'fm_low_cut', T.FMLowCut);
T.FMHighCut := JI(O,'fm_high_cut', T.FMHighCut);
T.FMEmphPosition := JI(O,'fm_emph_position',T.FMEmphPosition);
T.CTCSSOn := JB(O,'ctcss_on', T.CTCSSOn);
T.CTCSSFreq := JD(O,'ctcss_freq', T.CTCSSFreq);
T.TXFFTSize := JI(O,'tx_fft_size', T.TXFFTSize);
T.TXWindowType := JI(O,'tx_win_type', T.TXWindowType);
T.TXSpecDetector := JI(O,'tx_spec_detector', T.TXSpecDetector);
T.TXSpecAvgMode := JI(O,'tx_spec_avg_mode', T.TXSpecAvgMode);
T.TXSpecAvgTimeMS := JD(O,'tx_spec_avg_time_ms', T.TXSpecAvgTimeMS);
T.TXWfDetector := JI(O,'tx_wf_detector', T.TXWfDetector);
T.TXWfAvgMode := JI(O,'tx_wf_avg_mode', T.TXWfAvgMode);
T.TXWfAvgTimeMS := JD(O,'tx_wf_avg_time_ms', T.TXWfAvgTimeMS);
T.TXSpecRefLevel := JD(O,'tx_spec_ref_level', T.TXSpecRefLevel);
T.TXSpecRange := JD(O,'tx_spec_range', T.TXSpecRange);
T.TXSpecGridStep := JD(O,'tx_spec_grid_step', T.TXSpecGridStep);
// PureSignal
T.PSMoxDelay := JD(O,'ps_mox_delay', T.PSMoxDelay);
T.PSLoopDelay := JD(O,'ps_loop_delay', T.PSLoopDelay);
T.PSTXDelayNs := JI(O,'ps_tx_delay_ns', T.PSTXDelayNs);
T.PSRelaxPtol := JB(O,'ps_relax_ptol', T.PSRelaxPtol);
T.PSAutoAtten := JB(O,'ps_auto_atten', T.PSAutoAtten);
T.PSPin := JB(O,'ps_pin', T.PSPin);
T.PSMap := JB(O,'ps_map', T.PSMap);
T.PSStabilize := JB(O,'ps_stabilize', T.PSStabilize);
T.PSIntsIdx := EnsureRange(JI(O,'ps_ints_idx', T.PSIntsIdx), 0, 2);
T.PSHWPeak := JD(O,'ps_hw_peak', T.PSHWPeak);
T.PSTTFreq1 := JI(O,'ps_tt_freq1', T.PSTTFreq1);
T.PSTTFreq2 := JI(O,'ps_tt_freq2', T.PSTTFreq2);
T.PSFBTarget := EnsureRange(JI(O,'ps_fb_target', T.PSFBTarget), 0, 256);
T.PSOutlierSigma := JD(O,'ps_outlier_sigma', T.PSOutlierSigma);
end;
procedure TSettingsManager.SaveTX(const MAC: array of Byte;
const T: TTXSettings);
var O: TJSONObject; i: Integer;
begin
O := EnsureObj(GetDevObj(MacToStr(MAC)), 'tx');
JW(O,'mic_line_in', T.MicLineIn);
JW(O,'mic_boost', T.MicBoost);
JW(O,'mic_ptt_enabled', T.MicPTTEnabled);
JW(O,'mic_tip_ring', T.MicTipRing);
JW(O,'mic_bias', T.MicBias);
JW(O,'line_in_gain_db', T.LineInGainDB);
JW(O,'mic_gain_db', T.MicGainDB);
JW(O,'att_on_tx', T.AttOnTX);
JW(O,'tun_level', T.TUNLevel);
JW(O,'tun_freq', T.TUNFreq);
JW(O,'filter_low', T.FilterLow);
JW(O,'filter_high', T.FilterHigh);
JW(O,'filter_nc', T.FilterNC);
JW(O,'filter_mp', T.FilterMP);
JW(O,'filter_window', T.FilterWindow);
JW(O,'compressor_on', T.CompressorOn);
JW(O,'compressor_gain', T.CompressorGain);
JW(O,'leveler_on', T.LevelerOn);
JW(O,'leveler_top', T.LevelerTop);
JW(O,'leveler_decay', T.LevelerDecay);
JW(O,'alc_on', T.ALCOn);
JW(O,'alc_max_gain', T.ALCMaxGain);
JW(O,'alc_decay', T.ALCDecay);
JW(O,'phase_rot_on', T.PhaseRotOn);
JW(O,'phase_rot_stages',T.PhaseRotStages);
JW(O,'phase_rot_freq', T.PhaseRotFreq);
JW(O,'eq_on', T.EQOn);
JW(O,'eq_num_bands', T.EQNumBands);
for i := 0 to 10 do
begin
JW(O,'eq_g_'+IntToStr(i), T.EQGains[i]);
JW(O,'eq_f_'+IntToStr(i), T.EQFreqs[i]);
end;
JW(O,'am_carrier_level',T.AMCarrierLevel);
JW(O,'fm_deviation', T.FMDeviation);
JW(O,'fm_low_cut', T.FMLowCut);
JW(O,'fm_high_cut', T.FMHighCut);
JW(O,'fm_emph_position',T.FMEmphPosition);
JW(O,'ctcss_on', T.CTCSSOn);
JW(O,'ctcss_freq', T.CTCSSFreq);
JW(O,'tx_fft_size', T.TXFFTSize);
JW(O,'tx_win_type', T.TXWindowType);
JW(O,'tx_spec_detector', T.TXSpecDetector);
JW(O,'tx_spec_avg_mode', T.TXSpecAvgMode);
JW(O,'tx_spec_avg_time_ms', T.TXSpecAvgTimeMS);
JW(O,'tx_wf_detector', T.TXWfDetector);
JW(O,'tx_wf_avg_mode', T.TXWfAvgMode);
JW(O,'tx_wf_avg_time_ms', T.TXWfAvgTimeMS);
JW(O,'tx_spec_ref_level', T.TXSpecRefLevel);
JW(O,'tx_spec_range', T.TXSpecRange);
JW(O,'tx_spec_grid_step', T.TXSpecGridStep);
// PureSignal
JW(O,'ps_mox_delay', T.PSMoxDelay);
JW(O,'ps_loop_delay', T.PSLoopDelay);
JW(O,'ps_tx_delay_ns', T.PSTXDelayNs);
JW(O,'ps_relax_ptol', T.PSRelaxPtol);
JW(O,'ps_auto_atten', T.PSAutoAtten);
JW(O,'ps_pin', T.PSPin);
JW(O,'ps_map', T.PSMap);
JW(O,'ps_stabilize', T.PSStabilize);
JW(O,'ps_ints_idx', T.PSIntsIdx);
JW(O,'ps_hw_peak', T.PSHWPeak);
JW(O,'ps_tt_freq1', T.PSTTFreq1);
JW(O,'ps_tt_freq2', T.PSTTFreq2);
JW(O,'ps_fb_target', T.PSFBTarget);
JW(O,'ps_outlier_sigma',T.PSOutlierSigma);
end;
class procedure TSettingsManager.DefaultAlex(out A: TAlexSettings);
var i: Integer;
begin
FillChar(A, SizeOf(A), 0);
for i := 0 to CFG_BAND_COUNT - 1 do
begin
A.RxAnt[i] := 1; // ANT1 — безопасный дефолт
A.TxAnt[i] := 1; // ANT1
A.RxOnly[i] := 0; // вторичный вход выключен
A.DoNotTx[i] := False;
end;
A.RxBypassOnTx := False;
A.Ext1OnTx := False;
A.GndBpf2OnTx := False;
A.EnableXvtrHf := False;
end;
function TSettingsManager.LoadAlex(const MAC: array of Byte;
out A: TAlexSettings): Boolean;
var MacStr: string; DevObj, O: TJSONObject; i: Integer;
begin
DefaultAlex(A);
MacStr := MacToStr(MAC);
Result := FRoot.Find(MacStr) <> nil;
if not Result then Exit;
DevObj := GetDevObj(MacStr);
if DevObj.Find('alex') = nil then Exit;
O := EnsureObj(DevObj, 'alex');
for i := 0 to CFG_BAND_COUNT - 1 do
begin
A.RxAnt[i] := EnsureRange(JI(O, 'rx_ant_' + IntToStr(i), 1), 1, 3);
A.TxAnt[i] := EnsureRange(JI(O, 'tx_ant_' + IntToStr(i), 1), 1, 3);
A.RxOnly[i] := EnsureRange(JI(O, 'rx_only_' + IntToStr(i), 0), 0, 3);
A.DoNotTx[i] := JB(O, 'do_not_tx_' + IntToStr(i), False);
end;
A.RxBypassOnTx := JB(O, 'rx_bypass_on_tx', False);
A.Ext1OnTx := JB(O, 'ext1_on_tx', False);
A.GndBpf2OnTx := JB(O, 'gnd_bpf2_on_tx', False);
A.EnableXvtrHf := JB(O, 'enable_xvtr_hf', False);
end;
class procedure TSettingsManager.DefaultAnt936x(out A: TAD936xAnt);
var i: Integer;
begin
for i := 0 to CFG_BAND_COUNT - 1 do
begin
A.RxAnt[i] := 0; // RX1
A.TxAnt[i] := 0; // TX1
end;
for i := 0 to CFG_XVTR_COUNT - 1 do
begin
A.XvtrRxAnt[i] := 0;
A.XvtrTxAnt[i] := 0;
end;
end;
function TSettingsManager.LoadAnt936x(const MAC: array of Byte;
out A: TAD936xAnt): Boolean;
var MacStr: string; DevObj, O: TJSONObject; i: Integer;
begin
DefaultAnt936x(A);
MacStr := MacToStr(MAC);
Result := FRoot.Find(MacStr) <> nil;
if not Result then Exit;
DevObj := GetDevObj(MacStr);
if DevObj.Find('ant936x') = nil then begin Result := False; Exit; end;
O := EnsureObj(DevObj, 'ant936x');
for i := 0 to CFG_BAND_COUNT - 1 do
begin
A.RxAnt[i] := EnsureRange(JI(O, 'rx_' + IntToStr(i), 0), 0, 1);
A.TxAnt[i] := EnsureRange(JI(O, 'tx_' + IntToStr(i), 0), 0, 1);
end;
for i := 0 to CFG_XVTR_COUNT - 1 do
begin
A.XvtrRxAnt[i] := EnsureRange(JI(O, 'xvtr_rx_' + IntToStr(i), 0), 0, 1);
A.XvtrTxAnt[i] := EnsureRange(JI(O, 'xvtr_tx_' + IntToStr(i), 0), 0, 1);
end;
end;
procedure TSettingsManager.SaveAnt936x(const MAC: array of Byte;
const A: TAD936xAnt);
var O: TJSONObject; i: Integer;
begin
O := EnsureObj(GetDevObj(MacToStr(MAC)), 'ant936x');
for i := 0 to CFG_BAND_COUNT - 1 do
begin
JW(O, 'rx_' + IntToStr(i), Integer(A.RxAnt[i]));
JW(O, 'tx_' + IntToStr(i), Integer(A.TxAnt[i]));
end;
for i := 0 to CFG_XVTR_COUNT - 1 do
begin
JW(O, 'xvtr_rx_' + IntToStr(i), Integer(A.XvtrRxAnt[i]));
JW(O, 'xvtr_tx_' + IntToStr(i), Integer(A.XvtrTxAnt[i]));
end;
end;
procedure TSettingsManager.SaveAlex(const MAC: array of Byte;
const A: TAlexSettings);
var O: TJSONObject; i: Integer;
begin
O := EnsureObj(GetDevObj(MacToStr(MAC)), 'alex');
for i := 0 to CFG_BAND_COUNT - 1 do
begin
JW(O, 'rx_ant_' + IntToStr(i), Integer(A.RxAnt[i]));
JW(O, 'tx_ant_' + IntToStr(i), Integer(A.TxAnt[i]));
JW(O, 'rx_only_' + IntToStr(i), Integer(A.RxOnly[i]));
JW(O, 'do_not_tx_' + IntToStr(i), A.DoNotTx[i]);
end;
JW(O, 'rx_bypass_on_tx', A.RxBypassOnTx);
JW(O, 'ext1_on_tx', A.Ext1OnTx);
JW(O, 'gnd_bpf2_on_tx', A.GndBpf2OnTx);
JW(O, 'enable_xvtr_hf', A.EnableXvtrHf);
end;
// ---------------------------------------------------------------------------
// XVTR settings (per-device, JSON-секция "xvtr" под MAC)
// ---------------------------------------------------------------------------
class procedure TSettingsManager.DefaultXvtr(out X: TXvtrSettings);
var i: Integer;
begin
FillChar(X, SizeOf(X), 0);
X.UseXVTRTunePower := False;
// Все слоты по умолчанию disabled. Первые два предзаполнены типичными
// значениями (2m с LO=116MHz, 70cm с LO=404MHz) — чтобы пользователь
// мог быстро включить.
for i := 0 to CFG_XVTR_COUNT - 1 do
begin
X.Entries[i].Enabled := False;
X.Entries[i].ButtonText := '';
X.Entries[i].FreqBegin := 0.0;
X.Entries[i].FreqEnd := 0.0;
X.Entries[i].LOOffset := 0.0;
X.Entries[i].LOError := 0.0;
X.Entries[i].FullDuplexTx := False;
X.Entries[i].TxLOOffset := 0.0;
X.Entries[i].RXGain := 0.0;
X.Entries[i].RXOnly := False;
X.Entries[i].TXPower := 100;
X.Entries[i].DisablePA := False;
X.Entries[i].RXAntenna := 0;
X.Entries[i].LastFreq := 0.0;
X.Entries[i].LastFreqB := 0.0;
X.Entries[i].LastMode := MODE_USB; // VHF SSB calling default
X.Entries[i].LastFilterIdx := 0; // NFM
X.Entries[i].LastFMSQOn := False;
X.Entries[i].LastFMSQLevel := 30;
X.Entries[i].LastCTCSSOn := False;
X.Entries[i].LastCTCSSToneIdx := 0;
X.Entries[i].LastFMStepOn := True;
X.Entries[i].LastFMStepIdx := 3; // 25 kHz
X.Entries[i].LastCTun := False;
X.Entries[i].LastCenterHz := 0.0;
X.Entries[i].LastAGCMode := 1;
X.Entries[i].LastAGCTop := 90;
X.Entries[i].LastFMRptDir := 0;
X.Entries[i].LastFMRptOffsetHz := 0.0;
X.Entries[i].LastCTCSSAutoActive := False;
X.Entries[i].LastFMRptAutoActive := False;
X.Entries[i].LastZoomFactor := 0.0;
X.Entries[i].LastPanSlider := 0.5;
X.Entries[i].LastRxGainMode := 2; // slow_attack
X.Entries[i].LastRxGainDb := 40;
end;
// Шаблоны (пользователь должен сам Enable, чтобы трансверторы появились)
X.Entries[0].ButtonText := '2m';
X.Entries[0].FreqBegin := 144000000;
X.Entries[0].FreqEnd := 148000000;
X.Entries[0].LOOffset := 116000000;
X.Entries[0].LastFreq := 144300000;
X.Entries[0].LastFreqB := 145500000;
X.Entries[1].ButtonText := '70cm';
X.Entries[1].FreqBegin := 430000000;
X.Entries[1].FreqEnd := 440000000;
X.Entries[1].LOOffset := 404000000;
X.Entries[1].LastFreq := 432200000;
X.Entries[1].LastFreqB := 433000000;
// QO-100 (Es'hail-2) NB downlink: 10489.50010490.000 МГц, LO 9750 МГц.
// RX-трансвертер: tune = downlink LO. Калибровать LOError по маяку.
// QO-100 — в зарезервированном последнем слоте (отдельная страница настроек).
X.Entries[QO100_SLOT].ButtonText := 'QO-100';
X.Entries[QO100_SLOT].FreqBegin := 10489500000;
X.Entries[QO100_SLOT].FreqEnd := 10490000000;
X.Entries[QO100_SLOT].LOOffset := 9750000000; // LNB LO
X.Entries[QO100_SLOT].FullDuplexTx := True; // раздельный TX-LO + full-duplex
X.Entries[QO100_SLOT].TxLOOffset := 8089500000; // транспондер-offset (downlink-uplink)
X.Entries[QO100_SLOT].LastFreq := 10489750000;
X.Entries[QO100_SLOT].LastFreqB := 10489750000;
end;
function TSettingsManager.LoadXvtr(const MAC: array of Byte;
out X: TXvtrSettings): Boolean;
var
MacStr: string;
DevObj, Root, EObj: TJSONObject;
i: Integer;
begin
DefaultXvtr(X);
MacStr := MacToStr(MAC);
Result := FRoot.Find(MacStr) <> nil;
if not Result then Exit;
DevObj := GetDevObj(MacStr);
if DevObj.Find('xvtr') = nil then Exit;
Root := EnsureObj(DevObj, 'xvtr');
X.UseXVTRTunePower := JB(Root, 'use_tun_power', False);
for i := 0 to CFG_XVTR_COUNT - 1 do
begin
if Root.Find('e' + IntToStr(i)) = nil then Continue;
EObj := EnsureObj(Root, 'e' + IntToStr(i));
X.Entries[i].Enabled := JB(EObj, 'en', X.Entries[i].Enabled);
X.Entries[i].ButtonText := JS(EObj, 'name', X.Entries[i].ButtonText);
X.Entries[i].FreqBegin := JD(EObj, 'fbeg', X.Entries[i].FreqBegin);
X.Entries[i].FreqEnd := JD(EObj, 'fend', X.Entries[i].FreqEnd);
X.Entries[i].LOOffset := JD(EObj, 'lo_off', X.Entries[i].LOOffset);
X.Entries[i].LOError := JD(EObj, 'lo_err', X.Entries[i].LOError);
X.Entries[i].FullDuplexTx := JB(EObj, 'full_dup', X.Entries[i].FullDuplexTx);
X.Entries[i].TxLOOffset := JD(EObj, 'tx_lo_off', X.Entries[i].TxLOOffset);
X.Entries[i].RXGain := JD(EObj, 'rx_gain', X.Entries[i].RXGain);
X.Entries[i].RXOnly := JB(EObj, 'rx_only', X.Entries[i].RXOnly);
X.Entries[i].TXPower := EnsureRange(JI(EObj, 'tx_pwr', X.Entries[i].TXPower), 0, 100);
X.Entries[i].DisablePA := JB(EObj, 'dis_pa', X.Entries[i].DisablePA);
X.Entries[i].RXAntenna := EnsureRange(JI(EObj, 'rx_ant', X.Entries[i].RXAntenna), 0, 3);
X.Entries[i].LastFreq := JD(EObj, 'last_freq', X.Entries[i].LastFreq);
X.Entries[i].LastFreqB := JD(EObj, 'last_freq_b', X.Entries[i].LastFreqB);
X.Entries[i].LastMode := EnsureRange(JI(EObj, 'last_mode', X.Entries[i].LastMode), CFG_MODE_MIN, CFG_MODE_MAX);
X.Entries[i].LastFilterIdx := EnsureRange(JI(EObj, 'last_filt', X.Entries[i].LastFilterIdx), 0, 9);
X.Entries[i].LastFMSQOn := JB(EObj, 'last_fmsq_on', X.Entries[i].LastFMSQOn);
X.Entries[i].LastFMSQLevel := EnsureRange(JI(EObj, 'last_fmsq_lvl', X.Entries[i].LastFMSQLevel), 0, 100);
X.Entries[i].LastCTCSSOn := JB(EObj, 'last_ctcss_on', X.Entries[i].LastCTCSSOn);
X.Entries[i].LastCTCSSToneIdx := EnsureRange(JI(EObj, 'last_ctcss_idx', X.Entries[i].LastCTCSSToneIdx), 0, 37);
X.Entries[i].LastFMStepOn := JB(EObj, 'last_fmstep_on', X.Entries[i].LastFMStepOn);
X.Entries[i].LastFMStepIdx := EnsureRange(JI(EObj, 'last_fmstep_idx', X.Entries[i].LastFMStepIdx), 0, 3);
X.Entries[i].LastCTun := JB(EObj, 'last_ctun', X.Entries[i].LastCTun);
X.Entries[i].LastCenterHz := JD(EObj, 'last_center_hz', X.Entries[i].LastCenterHz);
X.Entries[i].LastAGCMode := EnsureRange(JI(EObj, 'last_agc_mode', X.Entries[i].LastAGCMode), 0, 4);
X.Entries[i].LastAGCTop := EnsureRange(JI(EObj, 'last_agc_top', X.Entries[i].LastAGCTop), 0, 120);
X.Entries[i].LastFMRptDir := EnsureRange(JI(EObj, 'last_fmrpt_dir', X.Entries[i].LastFMRptDir), 0, 2);
X.Entries[i].LastFMRptOffsetHz := JD(EObj, 'last_fmrpt_offset', X.Entries[i].LastFMRptOffsetHz);
X.Entries[i].LastCTCSSAutoActive := JB(EObj, 'last_ctcss_auto', X.Entries[i].LastCTCSSAutoActive);
X.Entries[i].LastFMRptAutoActive := JB(EObj, 'last_fmrpt_auto', X.Entries[i].LastFMRptAutoActive);
X.Entries[i].LastZoomFactor := EnsureRange(JD(EObj, 'last_zoom_factor', X.Entries[i].LastZoomFactor), 0.0, 1.0);
X.Entries[i].LastPanSlider := EnsureRange(JD(EObj, 'last_pan_slider', X.Entries[i].LastPanSlider), 0.0, 1.0);
X.Entries[i].LastRxGainMode := EnsureRange(JI(EObj, 'last_rx_gain_mode', X.Entries[i].LastRxGainMode), 0, 3);
X.Entries[i].LastRxGainDb := EnsureRange(JI(EObj, 'last_rx_gain_db', X.Entries[i].LastRxGainDb), 0, 73);
// Валидация: Last freqs должны быть в [FreqBegin..FreqEnd]
if (X.Entries[i].LastFreq < X.Entries[i].FreqBegin) or
(X.Entries[i].LastFreq > X.Entries[i].FreqEnd) then
X.Entries[i].LastFreq := (X.Entries[i].FreqBegin + X.Entries[i].FreqEnd) / 2.0;
if (X.Entries[i].LastFreqB < X.Entries[i].FreqBegin) or
(X.Entries[i].LastFreqB > X.Entries[i].FreqEnd) then
X.Entries[i].LastFreqB := (X.Entries[i].FreqBegin + X.Entries[i].FreqEnd) / 2.0;
end;
// QO-100 закреплён за зарезервированным слотом QO100_SLOT. Восстанавливаем
// шаблон, если слот не размечен как split-LO транспондер ИЛИ значения пустые
// (старый конфиг сохранял слот как обычный пустой XVTR с нулями — тогда
// FullDuplexTx по умолчанию True, но LO/offset == 0). Реальный QO-100 всегда
// имеет положительный транспондер-offset, так что валидный конфиг не трогаем.
if (not X.Entries[QO100_SLOT].FullDuplexTx)
or (X.Entries[QO100_SLOT].TxLOOffset <= 0)
or (X.Entries[QO100_SLOT].LOOffset <= 0) then
begin
X.Entries[QO100_SLOT].Enabled := False;
X.Entries[QO100_SLOT].ButtonText := 'QO-100';
X.Entries[QO100_SLOT].FreqBegin := 10489500000;
X.Entries[QO100_SLOT].FreqEnd := 10490000000;
X.Entries[QO100_SLOT].LOOffset := 9750000000;
X.Entries[QO100_SLOT].FullDuplexTx := True;
X.Entries[QO100_SLOT].TxLOOffset := 8089500000;
X.Entries[QO100_SLOT].LastFreq := 10489750000;
X.Entries[QO100_SLOT].LastFreqB := 10489750000;
end;
end;
procedure TSettingsManager.MigrateXvtrTemplate(var X: TXvtrSettings;
const AName: string; ABegin, AEnd, ALO: Double;
AFullDup: Boolean = False; ATxLO: Double = 0.0);
var
i, freeIdx: Integer;
begin
freeIdx := -1;
for i := 0 to CFG_XVTR_COUNT - 1 do
begin
if SameText(X.Entries[i].ButtonText, AName) then Exit; // уже есть — ничего не делаем
if (freeIdx < 0) and (not X.Entries[i].Enabled) and
(Trim(X.Entries[i].ButtonText) = '') then
freeIdx := i;
end;
if freeIdx < 0 then Exit; // нет свободных слотов
X.Entries[freeIdx].ButtonText := AName;
X.Entries[freeIdx].FreqBegin := ABegin;
X.Entries[freeIdx].FreqEnd := AEnd;
X.Entries[freeIdx].LOOffset := ALO;
X.Entries[freeIdx].FullDuplexTx := AFullDup;
X.Entries[freeIdx].TxLOOffset := ATxLO;
X.Entries[freeIdx].LastFreq := (ABegin + AEnd) / 2.0;
X.Entries[freeIdx].LastFreqB := (ABegin + AEnd) / 2.0;
end;
procedure TSettingsManager.SaveXvtr(const MAC: array of Byte;
const X: TXvtrSettings);
var
Root, EObj: TJSONObject;
i: Integer;
begin
Root := EnsureObj(GetDevObj(MacToStr(MAC)), 'xvtr');
JW(Root, 'use_tun_power', X.UseXVTRTunePower);
for i := 0 to CFG_XVTR_COUNT - 1 do
begin
EObj := EnsureObj(Root, 'e' + IntToStr(i));
JW(EObj, 'en', X.Entries[i].Enabled);
JWS(EObj, 'name', X.Entries[i].ButtonText);
JW(EObj, 'fbeg', X.Entries[i].FreqBegin);
JW(EObj, 'fend', X.Entries[i].FreqEnd);
JW(EObj, 'lo_off', X.Entries[i].LOOffset);
JW(EObj, 'lo_err', X.Entries[i].LOError);
JW(EObj, 'full_dup', X.Entries[i].FullDuplexTx);
JW(EObj, 'tx_lo_off', X.Entries[i].TxLOOffset);
JW(EObj, 'rx_gain', X.Entries[i].RXGain);
JW(EObj, 'rx_only', X.Entries[i].RXOnly);
JW(EObj, 'tx_pwr', X.Entries[i].TXPower);
JW(EObj, 'dis_pa', X.Entries[i].DisablePA);
JW(EObj, 'rx_ant', Integer(X.Entries[i].RXAntenna));
JW(EObj, 'last_freq', X.Entries[i].LastFreq);
JW(EObj, 'last_freq_b', X.Entries[i].LastFreqB);
JW(EObj, 'last_mode', X.Entries[i].LastMode);
JW(EObj, 'last_filt', X.Entries[i].LastFilterIdx);
JW(EObj, 'last_fmsq_on', X.Entries[i].LastFMSQOn);
JW(EObj, 'last_fmsq_lvl', X.Entries[i].LastFMSQLevel);
JW(EObj, 'last_ctcss_on', X.Entries[i].LastCTCSSOn);
JW(EObj, 'last_ctcss_idx',X.Entries[i].LastCTCSSToneIdx);
JW(EObj, 'last_fmstep_on', X.Entries[i].LastFMStepOn);
JW(EObj, 'last_fmstep_idx',X.Entries[i].LastFMStepIdx);
JW(EObj, 'last_ctun', X.Entries[i].LastCTun);
JW(EObj, 'last_center_hz', X.Entries[i].LastCenterHz);
JW(EObj, 'last_agc_mode', X.Entries[i].LastAGCMode);
JW(EObj, 'last_agc_top', X.Entries[i].LastAGCTop);
JW(EObj, 'last_fmrpt_dir', X.Entries[i].LastFMRptDir);
JW(EObj, 'last_fmrpt_offset', X.Entries[i].LastFMRptOffsetHz);
JW(EObj, 'last_ctcss_auto', X.Entries[i].LastCTCSSAutoActive);
JW(EObj, 'last_fmrpt_auto', X.Entries[i].LastFMRptAutoActive);
JW(EObj, 'last_zoom_factor', X.Entries[i].LastZoomFactor);
JW(EObj, 'last_pan_slider', X.Entries[i].LastPanSlider);
JW(EObj, 'last_rx_gain_mode', X.Entries[i].LastRxGainMode);
JW(EObj, 'last_rx_gain_db', X.Entries[i].LastRxGainDb);
end;
end;
class procedure TSettingsManager.DefaultWeb(out W: TWebSettings);
begin
W.Enabled := True;
W.Port := 8080;
W.BindAddr := '0.0.0.0';
W.User := 'admin';
W.Pass := 'hpsdr';
end;
procedure TSettingsManager.LoadWebSettings(out W: TWebSettings);
var O: TJSONObject;
begin
DefaultWeb(W);
if FRoot.Find('web') = nil then Exit;
O := EnsureObj(FRoot, 'web');
W.Enabled := JB(O, 'enabled', True);
W.Port := EnsureRange(JI(O, 'port', 8080), 1, 65535);
W.BindAddr := JS(O, 'bind_addr', '0.0.0.0');
W.User := JS(O, 'user', 'admin');
W.Pass := JS(O, 'pass', 'hpsdr');
end;
procedure TSettingsManager.SaveWebSettings(const W: TWebSettings);
var O: TJSONObject;
begin
O := EnsureObj(FRoot, 'web');
JW(O, 'enabled', W.Enabled);
JW(O, 'port', W.Port);
JWS(O, 'bind_addr', W.BindAddr);
JWS(O, 'user', W.User);
JWS(O, 'pass', W.Pass);
Save;
end;
procedure TSettingsManager.SaveTheme(ALightTheme: Boolean);
var O: TJSONObject;
begin
O := EnsureObj(FRoot, 'preferences');
JW(O, 'light_theme', ALightTheme);
end;
function TSettingsManager.LoadTheme: Boolean;
var O: TJSONObject;
begin
Result := False;
if FRoot.Find('preferences') = nil then Exit;
O := EnsureObj(FRoot, 'preferences');
Result := JB(O, 'light_theme', False);
end;
// ---------------------------------------------------------------------------
// Панадаптеры + слайсы (per-device, JSON-секция "pans" под MAC)
// ---------------------------------------------------------------------------
class procedure TSettingsManager.DefaultPans(out P: TPansConfig);
var i: Integer;
begin
P.GridMode := False;
P.GridRowShare := 0.5;
for i := 0 to CFG_PANS_MAX-1 do
begin
P.Pans[i].Active := False;
P.Pans[i].FreqHz := 0.0;
P.Pans[i].RateKHz := 384;
P.Pans[i].ADC := 0;
P.Pans[i].Zoom := 0.0;
P.Pans[i].PanSlider := 0.5;
P.Pans[i].StackShare := 1.0;
P.Pans[i].ColShare := 1.0;
P.Pans[i].SplitRatio := 0.40; // = TPanafallPanel дефолт
P.Pans[i].HasDisplay := False;
P.Pans[i].DPalette := 0;
P.Pans[i].DGamma := 1.0;
P.Pans[i].DSpecRef := -20.0;
P.Pans[i].DSpecRange := 110.0;
P.Pans[i].DWfAGC := True;
P.Pans[i].DWfOffset := 0.0;
P.Pans[i].DWfHigh := -60.0;
P.Pans[i].DWfLow := -120.0;
SetLength(P.Pans[i].Slices, 0);
end;
end;
procedure TSettingsManager.SavePans(const MAC: array of Byte; const Ctx: string;
const P: TPansConfig);
var
Root, PanObj, SliceObj, DispObj: TJSONObject;
PanArr, SliceArr: TJSONArray;
i, j, Idx: Integer;
begin
Root := EnsureObj(EnsureObj(GetDevObj(MacToStr(MAC)), 'pans'), Ctx);
JW(Root, 'grid_mode', P.GridMode);
JW(Root, 'grid_row_share', P.GridRowShare);
// Список панов пересоздаётся целиком (проще, чем merge по индексам).
Idx := Root.IndexOfName('list');
if Idx >= 0 then Root.Delete(Idx);
PanArr := TJSONArray.Create;
Root.Add('list', PanArr);
for i := 0 to CFG_PANS_MAX-1 do
begin
// Пан 0 пишем всегда (ради его доли стека/слайсов), паны N — только активные.
if (i > 0) and not P.Pans[i].Active then Continue;
PanObj := TJSONObject.Create;
PanArr.Add(PanObj);
PanObj.Add('id', i);
PanObj.Add('active', P.Pans[i].Active);
PanObj.Add('freq', P.Pans[i].FreqHz);
PanObj.Add('rate_khz', P.Pans[i].RateKHz);
PanObj.Add('adc', P.Pans[i].ADC);
PanObj.Add('zoom', P.Pans[i].Zoom);
PanObj.Add('pan', P.Pans[i].PanSlider);
PanObj.Add('stack_share', P.Pans[i].StackShare);
PanObj.Add('col_share', P.Pans[i].ColShare);
PanObj.Add('split_ratio', P.Pans[i].SplitRatio);
// Per-pan дисплей: пишем блок только при override (иначе пан = глоб. дефолт).
if P.Pans[i].HasDisplay then
begin
DispObj := TJSONObject.Create;
PanObj.Add('display', DispObj);
DispObj.Add('palette', P.Pans[i].DPalette);
DispObj.Add('gamma', P.Pans[i].DGamma);
DispObj.Add('spec_ref', P.Pans[i].DSpecRef);
DispObj.Add('spec_range', P.Pans[i].DSpecRange);
DispObj.Add('wf_agc', P.Pans[i].DWfAGC);
DispObj.Add('wf_offset', P.Pans[i].DWfOffset);
DispObj.Add('wf_high', P.Pans[i].DWfHigh);
DispObj.Add('wf_low', P.Pans[i].DWfLow);
end;
SliceArr := TJSONArray.Create;
PanObj.Add('slices', SliceArr);
for j := 0 to High(P.Pans[i].Slices) do
begin
SliceObj := TJSONObject.Create;
SliceArr.Add(SliceObj);
SliceObj.Add('target_hz', P.Pans[i].Slices[j].TargetHz);
SliceObj.Add('mode', P.Pans[i].Slices[j].Mode);
SliceObj.Add('filter_lo', P.Pans[i].Slices[j].FilterLo);
SliceObj.Add('filter_hi', P.Pans[i].Slices[j].FilterHi);
SliceObj.Add('agc', P.Pans[i].Slices[j].AGC);
SliceObj.Add('volume', P.Pans[i].Slices[j].Volume);
SliceObj.Add('muted', P.Pans[i].Slices[j].Muted);
SliceObj.Add('rx_mute_on_tx', P.Pans[i].Slices[j].RxMuteOnTx);
SliceObj.Add('fmsq_on', P.Pans[i].Slices[j].FMSQOn);
SliceObj.Add('fmsq_level', P.Pans[i].Slices[j].FMSQLevel);
SliceObj.Add('dev_name', P.Pans[i].Slices[j].DevName);
end;
end;
end;
function TSettingsManager.LoadPans(const MAC: array of Byte; const Ctx: string;
out P: TPansConfig): Boolean;
var
MacStr: string;
DevObj, PansObj, Root, PanObj, SliceObj, DispObj: TJSONObject;
ListData, SlData, DispData: TJSONData;
Arr, SArr: TJSONArray;
i, id, k, n: Integer;
begin
DefaultPans(P);
MacStr := MacToStr(MAC);
Result := FRoot.Find(MacStr) <> nil;
if not Result then Exit;
DevObj := GetDevObj(MacStr);
if DevObj.Find('pans') = nil then begin Result := False; Exit; end;
PansObj := EnsureObj(DevObj, 'pans');
if PansObj.Find(Ctx) = nil then begin Result := False; Exit; end;
Root := EnsureObj(PansObj, Ctx);
P.GridMode := JB(Root, 'grid_mode', False);
P.GridRowShare := EnsureRange(JD(Root, 'grid_row_share', 0.5), 0.15, 0.85);
ListData := Root.Find('list');
if (ListData = nil) or not (ListData is TJSONArray) then Exit;
Arr := TJSONArray(ListData);
for i := 0 to Arr.Count-1 do
begin
if not (Arr.Items[i] is TJSONObject) then Continue;
PanObj := TJSONObject(Arr.Items[i]);
id := JI(PanObj, 'id', -1);
if (id < 0) or (id >= CFG_PANS_MAX) then Continue;
P.Pans[id].Active := JB(PanObj, 'active', id > 0);
P.Pans[id].FreqHz := JD(PanObj, 'freq', 0.0);
P.Pans[id].RateKHz := JI(PanObj, 'rate_khz', 384);
P.Pans[id].ADC := JI(PanObj, 'adc', 0);
P.Pans[id].Zoom := EnsureRange(JD(PanObj, 'zoom', 0.0), 0.0, 1.0);
P.Pans[id].PanSlider := EnsureRange(JD(PanObj, 'pan', 0.5), 0.0, 1.0);
P.Pans[id].StackShare := JD(PanObj, 'stack_share', 1.0);
P.Pans[id].ColShare := JD(PanObj, 'col_share', 1.0);
P.Pans[id].SplitRatio := EnsureRange(JD(PanObj, 'split_ratio', 0.40), 0.05, 0.95);
// Per-pan дисплей: присутствие блока 'display' = override.
DispData := PanObj.Find('display');
if (DispData <> nil) and (DispData is TJSONObject) then
begin
DispObj := TJSONObject(DispData);
P.Pans[id].HasDisplay := True;
P.Pans[id].DPalette := EnsureRange(JI(DispObj, 'palette', 0), 0, 2);
P.Pans[id].DGamma := EnsureRange(JD(DispObj, 'gamma', 1.0), 0.30, 3.00);
P.Pans[id].DSpecRef := JD(DispObj, 'spec_ref', -20.0);
P.Pans[id].DSpecRange := JD(DispObj, 'spec_range', 110.0);
P.Pans[id].DWfAGC := JB(DispObj, 'wf_agc', True);
P.Pans[id].DWfOffset := JD(DispObj, 'wf_offset', 0.0);
P.Pans[id].DWfHigh := JD(DispObj, 'wf_high', -60.0);
P.Pans[id].DWfLow := JD(DispObj, 'wf_low', -120.0);
end;
SlData := PanObj.Find('slices');
if (SlData = nil) or not (SlData is TJSONArray) then Continue;
SArr := TJSONArray(SlData);
SetLength(P.Pans[id].Slices, 0);
for k := 0 to SArr.Count-1 do
begin
if not (SArr.Items[k] is TJSONObject) then Continue;
SliceObj := TJSONObject(SArr.Items[k]);
n := Length(P.Pans[id].Slices);
SetLength(P.Pans[id].Slices, n+1);
P.Pans[id].Slices[n].TargetHz := JD(SliceObj, 'target_hz', 0.0);
P.Pans[id].Slices[n].Mode := EnsureRange(
JI(SliceObj, 'mode', MODE_LSB), CFG_MODE_MIN, CFG_MODE_MAX);
P.Pans[id].Slices[n].FilterLo := JI(SliceObj, 'filter_lo', -1500);
P.Pans[id].Slices[n].FilterHi := JI(SliceObj, 'filter_hi', 1500);
P.Pans[id].Slices[n].AGC := JI(SliceObj, 'agc', 1);
P.Pans[id].Slices[n].Volume := EnsureRange(JD(SliceObj, 'volume', 0.7), 0.0, 1.0);
P.Pans[id].Slices[n].Muted := JB(SliceObj, 'muted', False);
P.Pans[id].Slices[n].RxMuteOnTx := JB(SliceObj, 'rx_mute_on_tx', True);
P.Pans[id].Slices[n].FMSQOn := JB(SliceObj, 'fmsq_on', False);
P.Pans[id].Slices[n].FMSQLevel := JI(SliceObj, 'fmsq_level', 30);
P.Pans[id].Slices[n].DevName := JS(SliceObj, 'dev_name', '');
end;
end;
end;
end.