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-слотов (трансверторов) OC_PIN_COUNT = 7; // количество Open Collector выходов (Penny/Alex) // TX pin action (byte-константы, как TXPinActions в Thetis Penny.cs): // при каком состоянии передачи пин активен. OCA_MOX = 0; // только обычный TX (не TUNE, не 2TON) OCA_TUNE = 1; // только TUNE OCA_TWOTONE = 2; // только 2TON OCA_MOX_TUNE = 3; // TX или TUNE OCA_MOX_TWOTONE = 4; // TX или 2TON OCA_TUNE_TWOTONE = 5; // TUNE или 2TON OCA_MOX_TUNE_TWOTONE = 6; // любое из трёх (дефолт) CFG_MODE_MIN = RadioModes.MODE_MIN; CFG_MODE_MAX = RadioModes.MODE_MAX; CFG_PANS_MAX = 4; // потолок панадаптеров (пан 0 + доп.) — = WDSPEngine.MAX_PANS CFG_CAT_SLICE_COUNT = 6; // слотов доп. слайсов (B..G) — = WDSPEngine.MAX_SLICES CFG_CAT_SLICE_PORT0 = 19091; // порт слайса B по умолчанию (главный — 19090) 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; // UI-подписи пресетов. Раньше жили в MainForm — переехали сюда, потому что // из них строится заводская таблица фильтров (DefaultFilterSet). FILT_SSB_NAMES: array[0..FILT_COUNT-1] of string = ('5.0k','4.4k','3.8k','3.3k','2.9k','2.7k','2.4k','2.1k','1.8k','1.0k'); FILT_CW_NAMES: array[0..FILT_COUNT-1] of string = ('1.0k','800','600','500','400','250','150','100','50','25'); FILT_DSB_NAMES: array[0..FILT_COUNT-1] of string = ('16k','12k','10k','8.0k','6.6k','5.2k','4.0k','3.1k','2.9k','2.4k'); FILT_AM_NAMES: array[0..FILT_COUNT-1] of string = ('20k','18k','16k','12k','10k','9.0k','8.0k','7.0k','6.0k','5.0k'); FILT_DIGI_NAMES: array[0..FILT_COUNT-1] of string = ('3.0k','2.7k','2.4k','2.1k','1.8k','1.5k','1.2k','900','600','300'); FILT_FM_NAMES: array[0..1] of string = ('NFM', 'FM'); FILT_WFM_NAMES: array[0..FILT_WFM_COUNT-1] of string = ('200k', '160k', '120k'); FILT_RAW_NAMES: array[0..FILT_RAW_COUNT-1] of string = ('6.25k', '10k', '12.5k', '20k', '25k'); // --- Редактируемая таблица фильтров ------------------------------------- // 10 пресетов + VAR1/VAR2 на режим (паритет с Thetis FilterForm). Кромки // ЗНАКОВЫЕ и хранятся ПО РЕЖИМУ: у LSB своя строка (-2800..-100), у USB // своя — поэтому при применении фильтра конвертировать боковую больше не // нужно, и таблица знака в проекте остаётся ровно одна (FilterEdgesFromBW, // ниже) — только для постройки заводских дефолтов. FILT_VAR1 = FILT_COUNT; // 10 FILT_VAR2 = FILT_COUNT + 1; // 11 FILT_SLOTS = FILT_COUNT + 2; // 12 — сетка кнопок 2 ряда по 6 // Минимальная ширина полосы: ниже неё фильтр звенит, а перетаскивание края // мышью легко схлопнуло бы полосу в ноль. FILT_MIN_WIDTH = 50; type TFilterSlot = record Name: string; Lo, Hi: Integer; // знаковые кромки в Гц относительно несущей end; TFilterSet = array[0..FILT_SLOTS-1] of TFilterSlot; // на режим TFilterTable = array[0..CFG_MODE_MAX] of TFilterSet; // по режиму TBandSettings = record VfoA: Double; VfoB: Double; Mode: Integer; FilterIdx: Integer; // FilterIdx — слот в таблице фильтров режима (см. TFilterSet); сами // кромки живут в таблице, а не здесь, поэтому диапазон помнит только // выбор. FilterBW остался легаси-полем для чтения старых конфигов. 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) // Шаговый аттенюатор ADC (openHPSDR) — на диапазон: на 40м ночью нужен, // на 10м нет. 0..31 дБ. AttenDB: Integer; 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) InDevName: string; // имя устройства ВВОДА слайса ('' = общий вход) 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; // Потолок точек в кадре спектра/водопада для веб-клиента (1024/2048/4096). // Реально уходит min(этот потолок, разрешение анализатора = ширина // панадаптера). Трафик линеен: 4096 точек — это ~650 КБ/с на клиента // (два потока по 20 кадров/с), 1024 — ~160 КБ/с. SpecPixels: Integer; 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 МГц) // Hz, поправка к TX LO-offset (TO Error). Сам offset держим стандартным // (8089.5 МГц у QO-100), а уход конкретного тракта правим здесь. // Знак как у LOError: положительная — TX IF растёт. TxLOError: Double; 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, дБ LastAttenDB: Integer; // шаговый аттенюатор ADC слота, дБ (0..31) 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; // OC Control (Open Collector выходы Penny/Alex, byte 1401 HP packet). // Только openHPSDR — у AD936x (Pluto/LibreSDR) такого выхода нет. // Хранятся в JSON-секции "oc" под MAC. Портировано из Thetis // (Console/setup.cs UpdateOCBits + Console/HPSDR/Penny.cs), объём сокращён: // без PA-override/hot-switching внешнего PA (в EWSDR нет такой концепции // на панели) и без split-pins VFO A/B (у EWSDR мультислайсы). // HF-группа — 11 диапазонов (0=160m..10=6m, как в TAlexSettings). // VHF-группа — 8 слотов XVTR (аналог Thetis VHF0-13, но по нашим слотам). TOCPinActions = array[0..OC_PIN_COUNT-1] of Byte; // OCA_* на пин TOCSettings = record Enabled: Boolean; RxMaskHF: array[0..CFG_BAND_COUNT-1] of Byte; // 7-бит маска, биты 0..6 = пины 1..7 TxMaskHF: array[0..CFG_BAND_COUNT-1] of Byte; ActionHF: TOCPinActions; // для HF TX-маски RxMaskXvtr: array[0..CFG_XVTR_COUNT-1] of Byte; TxMaskXvtr: array[0..CFG_XVTR_COUNT-1] of Byte; ActionXvtr: TOCPinActions; // для XVTR TX-маски 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; // Частотная калибровка опорника, ppm (Frequency Calibration в Thetis). // Смысл: «опора выше номинала на N ppm» ⇒ до правки сигналы показываются // ниже истинной частоты; положительное значение поднимает показания. // Действует на всё железо разом; ручные LOError/TO Error трансвертеров // остаются сверху — тракт тракту рознь. FreqCalPPM: 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 сервер (один, многоклиентский) — главный приёмник (слайс A) CATTcpEnabled: Boolean; CATTcpPort: Integer; // порт, по умолчанию 19090 // Слайс-CAT: свой TCP-порт на каждый ДОПОЛНИТЕЛЬНЫЙ слайс (B, C, …). // Индекс = слот слайса (0 = слайс B), см. CFG_CAT_SLICE_COUNT. CATSliceEnabled: array[0..CFG_CAT_SLICE_COUNT-1] of Boolean; CATSlicePort: array[0..CFG_CAT_SLICE_COUNT-1] of Integer; // 19091, 19092, … // Auto TX (аналог «Auto Switch TX Slice» в SmartSDR CAT): команда TX по // CAT-порту слайса сама назначает этот слайс источником передачи. CATSliceAutoTX: array[0..CFG_CAT_SLICE_COUNT-1] of Boolean; // Аудио слота слайса: имена PortAudio-устройств ('' = общий выход // приложения / общий вход). Имена, а не индексы: порядок перечисления // устройств меняется от запуска к запуску. CATSliceOutDev: array[0..CFG_CAT_SLICE_COUNT-1] of string; CATSliceInDev: array[0..CFG_CAT_SLICE_COUNT-1] of string; 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); // OC Control settings per-device (JSON-секция "oc" под MAC). class procedure DefaultOC(out O: TOCSettings); function LoadOC(const MAC: array of Byte; out O: TOCSettings): Boolean; procedure SaveOC(const MAC: array of Byte; const O: TOCSettings); // 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; // Таблица фильтров per-device (JSON-секция "filters" под MAC). Пишутся // только режимы, отличающиеся от заводских, — конфиг не пухнет. function LoadFilters(const MAC: array of Byte; out T: TFilterTable): Boolean; procedure SaveFilters(const MAC: array of Byte; const T: TFilterTable); end; // Сколько пресетов реально есть у режима (VAR1/VAR2 идут сверх этого числа). function FilterPresetCount(Mode: Integer): Integer; // Заводской индекс фильтра режима. function FilterDefIdx(Mode: Integer): Integer; // Единственная в проекте таблица «магнитуда полосы → знаковые кромки». // Нужна только для постройки заводских дефолтов и для чтения старых // конфигов, где хранилась одна ширина. procedure FilterEdgesFromBW(Mode, BW: Integer; out Lo, Hi: Integer); // Обратное преобразование для легаси-читателей FFilterBW. У SSB/DIGI «полоса» // исторически = внешняя кромка (2.7k при кромках 100..2700), у остальных // режимов — полная ширина; здесь это воспроизведено один в один. function FilterBWFromEdges(Mode, Lo, Hi: Integer): Integer; function DefaultFilterSet(Mode: Integer): TFilterSet; procedure DefaultFilterTable(out T: TFilterTable); implementation function FilterPresetCount(Mode: Integer): Integer; begin case Mode of MODE_FM: Result := FILT_FM_COUNT; MODE_WFM: Result := FILT_WFM_COUNT; MODE_DMR: Result := 1; MODE_FMRAW: Result := FILT_RAW_COUNT; else Result := FILT_COUNT; end; end; function FilterDefIdx(Mode: Integer): Integer; begin case Mode of MODE_LSB, MODE_USB: Result := FILT_SSB_DEF; MODE_DIGL, MODE_DIGU: Result := FILT_DIGI_DEF; MODE_DSB: Result := FILT_DSB_DEF; MODE_CWL, MODE_CWU: Result := FILT_CW_DEF; MODE_FM: Result := FILT_FM_DEF; MODE_WFM: Result := FILT_WFM_DEF; MODE_DMR: Result := 0; MODE_FMRAW: Result := FILT_RAW_DEF; else Result := FILT_AM_DEF; end; end; procedure FilterEdgesFromBW(Mode, BW: Integer; out Lo, Hi: Integer); var Half: Integer; begin Half := BW div 2; case Mode of MODE_LSB: begin Lo := -BW; Hi := -100; end; MODE_USB: begin Lo := 100; Hi := BW; end; // DIGI: полоса считается от нуля — поднесущие цифровых видов начинаются // низко, срез на 100 Гц как в SSB съел бы их. MODE_DIGL: begin Lo := -BW; Hi := 0; end; MODE_DIGU: begin Lo := 0; Hi := BW; end; else Lo := -Half; Hi := Half; // DSB/CW/FM/AM/SAM/WFM end; end; function FilterBWFromEdges(Mode, Lo, Hi: Integer): Integer; begin case Mode of MODE_LSB, MODE_USB, MODE_DIGL, MODE_DIGU: Result := Max(Abs(Lo), Abs(Hi)); else Result := Hi - Lo; end; end; function DefaultFilterSet(Mode: Integer): TFilterSet; var i, n, bw: Integer; begin n := FilterPresetCount(Mode); for i := 0 to FILT_SLOTS - 1 do begin Result[i].Name := ''; Result[i].Lo := 0; Result[i].Hi := 0; end; bw := 0; for i := 0 to n - 1 do begin case Mode of MODE_LSB, MODE_USB: begin bw := FILT_SSB_BW[i]; Result[i].Name := FILT_SSB_NAMES[i]; end; MODE_DIGL, MODE_DIGU: begin bw := FILT_DIGI_BW[i]; Result[i].Name := FILT_DIGI_NAMES[i]; end; MODE_DSB: begin bw := FILT_DSB_BW[i]; Result[i].Name := FILT_DSB_NAMES[i]; end; MODE_CWL, MODE_CWU: begin bw := FILT_CW_BW[i]; Result[i].Name := FILT_CW_NAMES[i]; end; MODE_FM: begin bw := FILT_FM_BW[i]; Result[i].Name := FILT_FM_NAMES[i]; end; MODE_WFM: begin bw := FILT_WFM_BW[i]; Result[i].Name := FILT_WFM_NAMES[i]; end; MODE_DMR: begin bw := FILT_DMR_BW; Result[i].Name := '12.5k'; end; MODE_FMRAW: begin bw := FILT_RAW_BW[i]; Result[i].Name := FILT_RAW_NAMES[i]; end; else begin bw := FILT_AM_BW[i]; Result[i].Name := FILT_AM_NAMES[i]; end; end; FilterEdgesFromBW(Mode, bw, Result[i].Lo, Result[i].Hi); end; // VAR1/VAR2 стартуют копией заводского пресета режима — «чистый лист», // который не сдвигает звук, пока его не тронули. Result[FILT_VAR1] := Result[FilterDefIdx(Mode)]; Result[FILT_VAR1].Name := 'VAR1'; Result[FILT_VAR2] := Result[FilterDefIdx(Mode)]; Result[FILT_VAR2].Name := 'VAR2'; end; procedure DefaultFilterTable(out T: TFilterTable); var m: Integer; begin for m := 0 to CFG_MODE_MAX do T[m] := DefaultFilterSet(m); end; 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.AttenDB := 0; 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; for i := 0 to CFG_CAT_SLICE_COUNT-1 do begin G.CATSliceEnabled[i] := False; G.CATSlicePort[i] := CFG_CAT_SLICE_PORT0 + i; G.CATSliceAutoTX[i] := False; G.CATSliceOutDev[i] := ''; G.CATSliceInDev[i] := ''; end; // 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; for i := 0 to CFG_CAT_SLICE_COUNT-1 do begin G.CATSliceEnabled[i] := JB(GObj,'cat_slice_en_' +IntToStr(i), False); G.CATSlicePort[i] := JI(GObj,'cat_slice_port_' +IntToStr(i), CFG_CAT_SLICE_PORT0 + i); G.CATSliceAutoTX[i] := JB(GObj,'cat_slice_autotx_'+IntToStr(i), False); G.CATSliceOutDev[i] := JS(GObj,'cat_slice_out_' +IntToStr(i), ''); G.CATSliceInDev[i] := JS(GObj,'cat_slice_in_' +IntToStr(i), ''); 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); G.Cal.FreqCalPPM := EnsureRange(JD(GObj,'cal_freq_ppm', 0.0), -100.0, 100.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); Bands[i].AttenDB := EnsureRange(JI(BObj,'atten_db', Bands[i].AttenDB), 0, 31); 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; for i := 0 to CFG_CAT_SLICE_COUNT-1 do begin JW(O,'cat_slice_en_' +IntToStr(i), G.CATSliceEnabled[i]); JW(O,'cat_slice_port_' +IntToStr(i), G.CATSlicePort[i]); JW(O,'cat_slice_autotx_'+IntToStr(i), G.CATSliceAutoTX[i]); JWS(O,'cat_slice_out_' +IntToStr(i), G.CATSliceOutDev[i]); JWS(O,'cat_slice_in_' +IntToStr(i), G.CATSliceInDev[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]); JW(O,'cal_freq_ppm',G.Cal.FreqCalPPM); 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); JW(O,'atten_db',B.AttenDB); 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.AttenDB := EnsureRange(JI(O,'atten_db', B.AttenDB), 0, 31); 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; for i := 0 to CFG_CAT_SLICE_COUNT-1 do begin JW(O, 'slice_en_' +IntToStr(i), G.CATSliceEnabled[i]); JW(O, 'slice_port_' +IntToStr(i), G.CATSlicePort[i]); JW(O, 'slice_autotx_'+IntToStr(i), G.CATSliceAutoTX[i]); JWS(O,'slice_out_' +IntToStr(i), G.CATSliceOutDev[i]); JWS(O,'slice_in_' +IntToStr(i), G.CATSliceInDev[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; for i := 0 to CFG_CAT_SLICE_COUNT-1 do begin G.CATSliceEnabled[i] := JB(O, 'slice_en_' +IntToStr(i), False); G.CATSlicePort[i] := JI(O, 'slice_port_' +IntToStr(i), CFG_CAT_SLICE_PORT0 + i); G.CATSliceAutoTX[i] := JB(O, 'slice_autotx_'+IntToStr(i), False); G.CATSliceOutDev[i] := JS(O, 'slice_out_' +IntToStr(i), ''); G.CATSliceInDev[i] := JS(O, 'slice_in_' +IntToStr(i), ''); 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 := 200; T.FilterHigh := 3100; 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.DefaultOC(out O: TOCSettings); var i: Integer; begin FillChar(O, SizeOf(O), 0); O.Enabled := False; for i := 0 to CFG_BAND_COUNT - 1 do begin O.RxMaskHF[i] := 0; O.TxMaskHF[i] := 0; end; for i := 0 to CFG_XVTR_COUNT - 1 do begin O.RxMaskXvtr[i] := 0; O.TxMaskXvtr[i] := 0; end; for i := 0 to OC_PIN_COUNT - 1 do begin O.ActionHF[i] := OCA_MOX_TUNE_TWOTONE; O.ActionXvtr[i] := OCA_MOX_TUNE_TWOTONE; end; end; function TSettingsManager.LoadOC(const MAC: array of Byte; out O: TOCSettings): Boolean; var MacStr: string; DevObj, Obj: TJSONObject; i: Integer; begin DefaultOC(O); MacStr := MacToStr(MAC); Result := FRoot.Find(MacStr) <> nil; if not Result then Exit; DevObj := GetDevObj(MacStr); if DevObj.Find('oc') = nil then begin Result := False; Exit; end; Obj := EnsureObj(DevObj, 'oc'); O.Enabled := JB(Obj, 'enabled', False); for i := 0 to CFG_BAND_COUNT - 1 do begin O.RxMaskHF[i] := EnsureRange(JI(Obj, 'rx_hf_' + IntToStr(i), 0), 0, 127); O.TxMaskHF[i] := EnsureRange(JI(Obj, 'tx_hf_' + IntToStr(i), 0), 0, 127); end; for i := 0 to CFG_XVTR_COUNT - 1 do begin O.RxMaskXvtr[i] := EnsureRange(JI(Obj, 'rx_xvtr_' + IntToStr(i), 0), 0, 127); O.TxMaskXvtr[i] := EnsureRange(JI(Obj, 'tx_xvtr_' + IntToStr(i), 0), 0, 127); end; for i := 0 to OC_PIN_COUNT - 1 do begin O.ActionHF[i] := EnsureRange(JI(Obj, 'action_hf_' + IntToStr(i), OCA_MOX_TUNE_TWOTONE), 0, 6); O.ActionXvtr[i] := EnsureRange(JI(Obj, 'action_xvtr_' + IntToStr(i), OCA_MOX_TUNE_TWOTONE), 0, 6); end; end; procedure TSettingsManager.SaveOC(const MAC: array of Byte; const O: TOCSettings); var Obj: TJSONObject; i: Integer; begin Obj := EnsureObj(GetDevObj(MacToStr(MAC)), 'oc'); JW(Obj, 'enabled', O.Enabled); for i := 0 to CFG_BAND_COUNT - 1 do begin JW(Obj, 'rx_hf_' + IntToStr(i), Integer(O.RxMaskHF[i])); JW(Obj, 'tx_hf_' + IntToStr(i), Integer(O.TxMaskHF[i])); end; for i := 0 to CFG_XVTR_COUNT - 1 do begin JW(Obj, 'rx_xvtr_' + IntToStr(i), Integer(O.RxMaskXvtr[i])); JW(Obj, 'tx_xvtr_' + IntToStr(i), Integer(O.TxMaskXvtr[i])); end; for i := 0 to OC_PIN_COUNT - 1 do begin JW(Obj, 'action_hf_' + IntToStr(i), Integer(O.ActionHF[i])); JW(Obj, 'action_xvtr_' + IntToStr(i), Integer(O.ActionXvtr[i])); end; end; function TSettingsManager.LoadFilters(const MAC: array of Byte; out T: TFilterTable): Boolean; var MacStr, K: string; DevObj, Root, Obj: TJSONObject; D: TJSONData; m, s: Integer; begin DefaultFilterTable(T); MacStr := MacToStr(MAC); Result := FRoot.Find(MacStr) <> nil; if not Result then Exit; DevObj := GetDevObj(MacStr); if DevObj.Find('filters') = nil then begin Result := False; Exit; end; Root := EnsureObj(DevObj, 'filters'); for m := 0 to CFG_MODE_MAX do begin D := Root.Find('m' + IntToStr(m)); if (D = nil) or not (D is TJSONObject) then Continue; Obj := TJSONObject(D); for s := 0 to FILT_SLOTS - 1 do begin K := 's' + IntToStr(s) + '_'; T[m][s].Name := JS(Obj, K + 'n', T[m][s].Name); T[m][s].Lo := JI(Obj, K + 'lo', T[m][s].Lo); T[m][s].Hi := JI(Obj, K + 'hi', T[m][s].Hi); if T[m][s].Hi <= T[m][s].Lo then // битый конфиг не должен глушить приём begin T[m][s].Lo := DefaultFilterSet(m)[s].Lo; T[m][s].Hi := DefaultFilterSet(m)[s].Hi; end; end; end; end; procedure TSettingsManager.SaveFilters(const MAC: array of Byte; const T: TFilterTable); var K: string; Root, Obj: TJSONObject; Def: TFilterSet; m, s, Idx: Integer; Differs: Boolean; begin Root := EnsureObj(GetDevObj(MacToStr(MAC)), 'filters'); for m := 0 to CFG_MODE_MAX do begin Def := DefaultFilterSet(m); Differs := False; for s := 0 to FILT_SLOTS - 1 do if (T[m][s].Lo <> Def[s].Lo) or (T[m][s].Hi <> Def[s].Hi) or (T[m][s].Name <> Def[s].Name) then Differs := True; // Секцию режима всегда сносим целиком: иначе возврат слота к заводскому // оставил бы старый ключ и он всплыл бы при следующей загрузке. Idx := Root.IndexOfName('m' + IntToStr(m)); if Idx >= 0 then Root.Delete(Idx); if not Differs then Continue; Obj := EnsureObj(Root, 'm' + IntToStr(m)); for s := 0 to FILT_SLOTS - 1 do begin K := 's' + IntToStr(s) + '_'; JWS(Obj, K + 'n', T[m][s].Name); JW (Obj, K + 'lo', T[m][s].Lo); JW (Obj, K + 'hi', T[m][s].Hi); end; end; 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].TxLOError := 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; X.Entries[i].LastAttenDB := 0; 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.500–10490.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].TxLOError := JD(EObj, 'tx_lo_error', X.Entries[i].TxLOError); 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); X.Entries[i].LastAttenDB := EnsureRange(JI(EObj, 'last_atten_db', X.Entries[i].LastAttenDB), 0, 31); // Валидация: 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, 'tx_lo_error', X.Entries[i].TxLOError); 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); JW(EObj, 'last_atten_db', X.Entries[i].LastAttenDB); 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'; W.SpecPixels := 1024; // экономный дефолт: столько же, сколько слал протокол раньше 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'); W.SpecPixels := EnsureRange(JI(O, 'spec_pixels', 1024), 256, 4096); 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); JW(O, 'spec_pixels', W.SpecPixels); 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); SliceObj.Add('in_dev_name', P.Pans[i].Slices[j].InDevName); 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', ''); P.Pans[id].Slices[n].InDevName := JS(SliceObj, 'in_dev_name', ''); end; end; end; end.