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; const SETTINGS_FILE = 'hpsdr_settings.json'; CFG_BAND_COUNT = 12; CFG_XVTR_COUNT = 8; // количество XVTR-слотов (трансверторов) CFG_MODE_MAX = 11; // LSB..DMR; keep persisted-mode validation current CFG_PANS_MAX = 4; // потолок панадаптеров (пан 0 + доп.) — = WDSPEngine.MAX_PANS QO100_SLOT = CFG_XVTR_COUNT - 1; // последний слот зарезервирован под QO-100 // (split-LO транспондер; настраивается на // отдельной странице, не в общей XVTR-таблице) // PA gain defaults (dB) — match Thetis HERMES/HPSDR defaults // order: 160m,80m,60m,40m,30m,20m,17m,15m,12m,10m,6m // 12-й слот — пустой в HF-плане (используется только 12-местным VHF/UHF-планом // Pluto, где частота/режим приходят из плана BoardUtils, а не отсюда). CFG_PA_BAND_CAL_DEFAULT: array[0..CFG_BAND_COUNT-1] of Double = ( 41.0, 41.2, 41.3, 41.3, 41.0, 40.5, 39.9, 38.8, 38.8, 38.8, 38.8, 38.8); CFG_VHF_CAL_DEFAULT = 56.2; // dB, default for all XVTR slots CFG_BAND_DEFAULT_FREQ: array[0..CFG_BAND_COUNT-1] of Double = ( 1900000, 3750000, 5357000, 7100000, 10125000, 14200000, 18120000, 21200000, 24940000, 28500000, 50150000, 50150000); CFG_BAND_DEFAULT_MODE: array[0..CFG_BAND_COUNT-1] of Integer = ( 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1); // Полосы фильтров по типу модуляции (Гц) + дефолтные индексы. Нужны // TRadioController для headless-выбора фильтра при смене режима; UI-подписи // (FILT_*_NAMES) остаются в MainForm. NAMES-массивы там используют FILT_COUNT. FILT_COUNT = 10; FILT_SSB_BW: array[0..FILT_COUNT-1] of Integer = (5000, 4400, 3800, 3300, 2900, 2700, 2400, 2100, 1800, 1000); FILT_SSB_DEF = 5; // 2.7k FILT_CW_BW: array[0..FILT_COUNT-1] of Integer = (1000, 800, 600, 500, 400, 250, 150, 100, 50, 25); FILT_CW_DEF = 3; // 500 FILT_DSB_BW: array[0..FILT_COUNT-1] of Integer = (16000, 12000, 10000, 8000, 6600, 5200, 4000, 3100, 2900, 2400); FILT_DSB_DEF = 3; // 8.0k FILT_AM_BW: array[0..FILT_COUNT-1] of Integer = (20000, 18000, 16000, 12000, 10000, 9000, 8000, 7000, 6000, 5000); FILT_AM_DEF = 4; // 10k // DIGI: полоса считается от нуля (кромки 0..BW), поэтому таблица своя — // SSB-полосы отмерены от среза на 100 Гц и здесь не подходят. FILT_DIGI_BW: array[0..FILT_COUNT-1] of Integer = (3000, 2700, 2400, 2100, 1800, 1500, 1200, 900, 600, 300); FILT_DIGI_DEF = 0; // 3.0k FILT_FM_COUNT = 2; FILT_FM_BW: array[0..1] of Integer = (11000, 16000); FILT_FM_DEV: array[0..1] of Double = (2500.0, 5000.0); FILT_FM_DEF = 0; // NFM // WFM: вещательный канал. 200 кГц требует SampleRate 384k — при 192k // WDSPEngine.SetFilter сам зажмёт кромки по Найквисту dsp_rate. FILT_WFM_COUNT = 3; FILT_WFM_BW: array[0..2] of Integer = (200000, 160000, 120000); FILT_WFM_DEF = 1; // 160k — влезает и в 192k // DMR Tier II: 12.5 kHz RF channel, 4FSK nominal deviation +/-1.944 kHz. FILT_DMR_BW = 12500; DMR_FM_DEV = 2500; type TBandSettings = record VfoA: Double; VfoB: Double; Mode: Integer; FilterIdx: Integer; FilterBW: Integer; AGCMode: Integer; AGCTop: Integer; CTun: Boolean; CenterHz: Double; // DDC-центр при CTun (восстановление offset VFO в полосе) SpanHz: Double; FMSQOn: Boolean; FMSQLevel: Integer; // 0..100 → WDSP threshold 0.0..0.5 CTCSSOn: Boolean; CTCSSToneIdx: Integer; // 0..37 FMStepOn: Boolean; FMStepIdx: Integer; // 0..3 (6.25/12.5/20/25 kHz) FMRptDir: Integer; // RPT_NONE/RPT_MINUS/RPT_PLUS FMRptOffsetHz: Double; // Hz, 0 = use band default ZoomFactor: Double; // зум спектра/водопада 0.0..1.0 (0.0 = без зума) PanSlider: Double; // положение окна зума 0..1 (0.5 = центр) // Аппаратный AGC AD936x (Pluto/LibreSDR) — на диапазон: усиление тракта // сильно разное на 2м и на QO-100, поэтому режим и manual-гейн per-band. RxGainMode: Integer; // 0=manual,1=fast,2=slow,3=hybrid RxGainDb: Integer; // manual gain, дБ (0..73) end; // Персист панадаптеров и слайсов (секция "pans" под MAC). Восстанавливается // один раз при выходе в Run. Слайс = приёмный WDSP-канал внутри пана; частоты // абсолютные (видимые). См. [[project_slices_plan]]. TSliceCfg = record TargetHz: Double; // абсолютная видимая частота слайса Mode: Integer; FilterLo: Integer; FilterHi: Integer; AGC: Integer; // Ord(TWDSPAGCMode) Volume: Double; // 0..1 Muted: Boolean; RxMuteOnTx: Boolean; // True = слайс молчит на передаче (self-monitor выкл) FMSQOn: Boolean; FMSQLevel: Integer; DevName: string; // имя PortAudio-устройства ('' = default) end; TPanCfg = record Active: Boolean; // пан 1..N существует (пан 0 всегда есть) FreqHz: Double; // видимый центр DDC (только паны N) RateKHz: Integer; ADC: Integer; Zoom: Double; // 0..1 PanSlider: Double; // 0..1 StackShare: Double; // доля высоты в стеке панов ColShare: Double; // доля ширины в грид-раскладке SplitRatio: Double; // доля высоты спектра (сплиттер спектр/водопад) // Per-pan дисплей (3.6): override глобального рендера. HasDisplay=False ⇒ // пан наследует глобальный дефолт (поля игнорируются). HasDisplay: Boolean; DPalette: Integer; // 0=Classic/1=Inferno/2=Turbo DGamma: Double; DSpecRef: Double; // верх шкалы спектра, dBm DSpecRange: Double; // диапазон шкалы, dB DWfAGC: Boolean; // авто-уровень водопада DWfOffset: Double; // яркость водопада при AGC, dB DWfHigh: Double; // ручной верх водопада, dBm DWfLow: Double; // ручной низ водопада, dBm Slices: array of TSliceCfg; end; TPansConfig = record GridMode: Boolean; // грид-раскладка (▦) vs вертикальный стек (▤) GridRowShare: Double; // доля высоты пана 0 в гриде Pans: array[0..CFG_PANS_MAX-1] of TPanCfg; // [0]=главный (только Slices) end; // TX-настройки per-device. Хранятся в JSON-секции "tx" под MAC, // отдельно от global/bands. Применяются к WDSP через SetTX* методы. TTXSettings = record // Radio mic options (DUC Specific byte 50/51) MicLineIn: Boolean; // bit0: 1=Line In, 0=Mic In MicBoost: Boolean; // bit1: +20 dB preamp (только когда Mic In) MicPTTEnabled: Boolean; // bit2: Mic PTT enable (Orion) MicTipRing: Boolean; // bit3: False=Tip, True=Ring (Orion/OrionMkII) MicBias: Boolean; // bit4: электретный +5V bias (Orion) LineInGainDB: Double; // byte51: -34.5..+12 dB (шаг 1.5; только когда Line In) // Mic gain (dB, применяется в WDSP PanelGain1) MicGainDB: Double; // -10..+30 // ATT on TX — ослабление RX-ADC во время передачи (защита от утечки TX). // DUC Specific bytes 57/58/59 (StepAtten 0/1/2). Thetis default: 31 dB. AttOnTX: Integer; // 0..31 dB // Tune (кнопка TUN) — генерация тонального сигнала через WDSP PostGen. // Уровень — отдельный от слайдера Drive, проходит через ту же // per-band калибровку. Thetis defaults: level=10 %, freq=750 Hz. TUNLevel: Integer; // 0..100 (% от полной шкалы Drive) TUNFreq: Integer; // Hz, обычно 600..1500 для SSB tune // TX bandpass filter FilterLow: Integer; // Hz, для SSB обычно 100 FilterHigh: Integer; // Hz, для SSB обычно 2800 FilterNC: Integer; // FIR length (1024/2048/4096) FilterMP: Integer; // 0=Linear Phase, 1=Low Latency FilterWindow: Integer; // 0=Rect..6=BH7 // Compressor CompressorOn: Boolean; CompressorGain: Double; // 0..20 dB // Leveler (TX AGC) LevelerOn: Boolean; LevelerTop: Double; // 0..10 dB LevelerDecay: Integer; // ms // ALC (Auto Level Control) ALCOn: Boolean; ALCMaxGain: Double; // -10..+10 dB ALCDecay: Integer; // ms // Phase Rotator PhaseRotOn: Boolean; PhaseRotStages: Integer; // 4/6/8 PhaseRotFreq: Double; // Hz, обычно 338 // EQ TX (10-band: [0]=preamp, [1..10]=bands) EQOn: Boolean; EQNumBands: Integer; // 3 или 10 EQGains: array[0..10] of Double; // dB, ±15 EQFreqs: array[0..10] of Double; // Hz // AM modulator AMCarrierLevel: Double; // 0..1 // FM modulator FMDeviation: Double; // Hz, 2500..5000 FMLowCut: Integer; // AF filter low FMHighCut: Integer; // AF filter high FMEmphPosition: Integer; // 0=PreEQ, 1=PostEQ // CTCSS CTCSSOn: Boolean; CTCSSFreq: Double; // Hz // TX Display (отдельный analyzer для TX-спектра/водопада) TXFFTSize: Integer; // 4096..262144 TXWindowType: Integer; // 0..6 TXSpecDetector: Integer; // 0=Peak,1=Rosenfell,2=Average,3=Sample,4=RMS TXSpecAvgMode: Integer; // 0=None,1=Recursive,2=TimeWindow,3=LogRecursive TXSpecAvgTimeMS: Double; TXWfDetector: Integer; TXWfAvgMode: Integer; TXWfAvgTimeMS: Double; // TX Grid (отдельные от RX — Thetis отрисовывает спектр TX в своих границах) TXSpecRefLevel: Double; // dBm reference (top of scale), e.g. 0 TXSpecRange: Double; // dB range, e.g. 80 TXSpecGridStep: Double; // grid step in dB, e.g. 10 end; // Web-сервер — глобальные настройки (не привязаны к устройству, секция "web"). TWebSettings = record Enabled: Boolean; Port: Integer; // 1..65535 BindAddr: string; // e.g. '0.0.0.0' (все интерфейсы) User: string; Pass: string; end; // Transverter (XVTR) entry — один трансвертер. // Видимая частота VFO в [FreqBegin..FreqEnd] транслируется в IF-частоту, // которую слышит трансивер: f_IF = f_visible - LOOffset + LOError. // Примеры: 2m XVTR с LO=116 MHz → visible 144.200 ⇒ IF 28.200 MHz. TXvtrEntry = record Enabled: Boolean; ButtonText: string[15]; // подпись на band-кнопке (например, '2m', '70cm') FreqBegin: Double; // Hz FreqEnd: Double; // Hz LOOffset: Double; // Hz, отнимается от видимой частоты для получения IF (RX) LOError: Double; // Hz, корректировка LO (положительная — IF растёт) — RX/LNB дрейф // QO-100 / split-LO транспондер: RX и TX идут на РАЗНЫХ LO. RX = visible-LOOffset // (+LOError, дрейф LNB по маяку). TX = visible-TxLOOffset (без LOError — uplink-LO // собственный у SDR). FullDuplexTx помечает слот как такой транспондер. FullDuplexTx: Boolean; // True — раздельный TX-LO (QO-100); авто display-duplex TxLOOffset: Double; // Hz, TX LO-offset (для QO-100 = транспондер-offset, ~8089.5 МГц) RXGain: Double; // dB, компенсация усиления RX тракта RXOnly: Boolean; // True — запрещает TX на этом трансверторе TXPower: Integer; // 0..100 % (drive level специально для XVTR) DisablePA: Boolean; // True — отключить внутренний PA при TX (XVTR имеет свой) RXAntenna: Byte; // 0=по-умолчанию(основной band), 1=ANT1, 2=ANT2, 3=ANT3 LastFreq: Double; // Hz — последняя tuned visible частота VFO A (BandStack) LastFreqB: Double; // Hz — последняя tuned visible частота VFO B LastMode: Integer; // Mode (LSB=0,USB=1,…FM=5) LastFilterIdx: Integer; // FM: 0=NFM, 1=WFM; иначе 0..FILT_COUNT-1 LastFMSQOn: Boolean; LastFMSQLevel: Integer; LastCTCSSOn: Boolean; LastCTCSSToneIdx: Integer; LastFMStepOn: Boolean; LastFMStepIdx: Integer; LastCTun: Boolean; LastCenterHz: Double; // Hz — DDC-центр при CTun (восстановление offset VFO) LastAGCMode: Integer; LastAGCTop: Integer; LastFMRptDir: Integer; LastFMRptOffsetHz: Double; LastCTCSSAutoActive: Boolean; // CTCSS был включён каналом (авто) LastFMRptAutoActive: Boolean; // RPT был включён каналом (авто) LastZoomFactor: Double; // зум спектра/водопада 0.0..1.0 (0.0 = без зума) LastPanSlider: Double; // положение окна зума 0..1 (0.5 = центр) // Аппаратный AGC AD936x на слот трансвертера (у QO-100 своё усиление тракта). LastRxGainMode: Integer; // 0=manual,1=fast,2=slow,3=hybrid LastRxGainDb: Integer; // manual gain, дБ end; TXvtrSettings = record Entries: array[0..CFG_XVTR_COUNT-1] of TXvtrEntry; // Use XVTR TUN power — кнопка TUN использует XVTR-specific power. UseXVTRTunePower: Boolean; end; // Alex (Antenna + Filter board) settings per-device. // Хранятся в JSON-секции "alex" под MAC. // Безопасные дефолты: ANT1 для всех диапазонов, вторичный вход выключен. // Антенные разъёмы AD936x (Pluto/LibreSDR): какой физический вход/выход // используется на каждом диапазоне. 0 = RX1/TX1, 1 = RX2/TX2. // Диапазоны — VU-план BoardUtils (12 слотов), плюс слоты трансвертеров. TAD936xAnt = record RxAnt: array[0..CFG_BAND_COUNT-1] of Byte; TxAnt: array[0..CFG_BAND_COUNT-1] of Byte; XvtrRxAnt: array[0..CFG_XVTR_COUNT-1] of Byte; XvtrTxAnt: array[0..CFG_XVTR_COUNT-1] of Byte; end; TAlexSettings = record // Per-band (11 bands: 0=160m..10=6m) RxAnt: array[0..CFG_BAND_COUNT-1] of Byte; // 1..3 (ANT1/2/3) TxAnt: array[0..CFG_BAND_COUNT-1] of Byte; // 1..3 (ANT1/2/3) // Вторичный RX вход: 0=выкл, 1=BYPS(Ext1+Bypass), 2=EXT1(Ext2+Bypass), 3=XVTR(XvtrDDC+Bypass) RxOnly: array[0..CFG_BAND_COUNT-1] of Byte; DoNotTx: array[0..CFG_BAND_COUNT-1] of Boolean; // программный запрет TX на диапазоне // Глобальные флаги в режиме TX RxBypassOnTx: Boolean; // бит 11 (Bypass relay) во время TX Ext1OnTx: Boolean; // бит 9 (Ext1) + бит 11 во время TX GndBpf2OnTx: Boolean; // заземление BPF2/RX2 во время TX (Alex1 бит 8, ANAN-8000DLE) EnableXvtrHf: Boolean; // режим XVTR HF end; 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) // --- 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 // --- ADC settings --- DitherEnabled: Boolean; // DDC Specific byte 5 — уменьшает интермодуляцию RandomEnabled: Boolean; // DDC Specific byte 6 — рандомизация ADC // --- CAT (Computer Aided Transceiver) settings --- // До 4 последовательных портов (Linux: /dev/ttyS0, Windows: COM1 и т.д.) CATSerialEnabled: array[0..3] of Boolean; CATSerialPort: array[0..3] of string; // имя порта ОС CATSerialBaud: array[0..3] of Integer; // скорость: 1200..115200 CATSerialDataBits: array[0..3] of Integer; // 7 или 8 CATSerialStopBits: array[0..3] of Integer; // 1 или 2 CATSerialParity: array[0..3] of Integer; // 0=None, 1=Odd, 2=Even CATSerialAndromeda:array[0..3] of Boolean; // порт — передняя панель Andromeda/G2 // TCP CAT сервер (один, многоклиентский) CATTcpEnabled: Boolean; CATTcpPort: Integer; // порт, по умолчанию 19090 end; TSettingsManager = class private FFilePath: string; FRoot: TJSONObject; function EnsureObj(P: TJSONObject; const K: string): TJSONObject; function GetDevObj(const M: string): TJSONObject; function GetGlobalObj(D: TJSONObject): TJSONObject; function GetBandObj(D: TJSONObject; Idx: Integer): TJSONObject; function JI(O: TJSONObject; const K: string; Def: Integer): Integer; function JD(O: TJSONObject; const K: string; Def: Double): Double; function JB(O: TJSONObject; const K: string; Def: Boolean): Boolean; function JS(O: TJSONObject; const K: string; const Def: string): string; procedure JW(O: TJSONObject; const K: string; V: Integer); overload; procedure JW(O: TJSONObject; const K: string; V: Double); overload; procedure JW(O: TJSONObject; const K: string; V: Boolean); overload; procedure JWS(O: TJSONObject; const K: string; const V: string); procedure MigrateXvtrTemplate(var X: TXvtrSettings; const AName: string; ABegin, AEnd, ALO: Double; AFullDup: Boolean = False; ATxLO: Double = 0.0); public constructor Create(const FilePath: string = ''); destructor Destroy; override; procedure Load; procedure Save; function LoadDevice(const MAC: array of Byte; out G: TGlobalSettings; var Bands: array of TBandSettings): Boolean; procedure SaveGlobal(const MAC: array of Byte; const G: TGlobalSettings); procedure SaveBand(const MAC: array of Byte; BandIdx: Integer; const B: TBandSettings); function LoadBand(const MAC: array of Byte; BandIdx: Integer; out B: TBandSettings): Boolean; // TX settings per-device (JSON-секция "tx" под MAC). // LoadTX возвращает True если устройство уже зарегистрировано в JSON; // в любом случае T заполняется (дефолтами или сохранёнными значениями). function LoadTX(const MAC: array of Byte; out T: TTXSettings): Boolean; procedure SaveTX(const MAC: array of Byte; const T: TTXSettings); class function MacToStr(const MAC: array of Byte): string; class procedure DefaultBand(BandIdx: Integer; out B: TBandSettings); class procedure DefaultGlobal(out G: TGlobalSettings); class procedure DefaultTX(out T: TTXSettings); // Размер окна — не привязан к MAC, хранится в корне JSON procedure SaveWindowBounds(L, T, W, H, DPI: Integer; Maximized: Boolean); procedure LoadWindowBounds(out L, T, W, H, DPI: Integer; out Maximized: Boolean); procedure SaveStartupPreview(VfoA, VfoB: Double; SampleRate: Integer); function LoadStartupPreview(out VfoA, VfoB: Double; out SampleRate: Integer): Boolean; procedure SaveAudioBufferSize(BufferSize: Integer); function LoadAudioBufferSize: Integer; // CAT settings — не привязаны к MAC, хранятся в корне JSON (доступны до подключения) procedure SaveCATSettings(const G: TGlobalSettings); procedure LoadCATSettings(var G: TGlobalSettings); // Alex (antenna) settings per-device (JSON-секция "alex" под MAC). class procedure DefaultAnt936x(out A: TAD936xAnt); function LoadAnt936x(const MAC: array of Byte; out A: TAD936xAnt): Boolean; procedure SaveAnt936x(const MAC: array of Byte; const A: TAD936xAnt); class procedure DefaultAlex(out A: TAlexSettings); function LoadAlex(const MAC: array of Byte; out A: TAlexSettings): Boolean; procedure SaveAlex(const MAC: array of Byte; const A: TAlexSettings); // XVTR (Transverter) settings per-device (JSON-секция "xvtr" под MAC). class procedure DefaultXvtr(out X: TXvtrSettings); function LoadXvtr(const MAC: array of Byte; out X: TXvtrSettings): Boolean; procedure SaveXvtr(const MAC: array of Byte; const X: TXvtrSettings); // Панадаптеры + слайсы per-device (JSON-секция "pans" под MAC). Ctx — // ключ контекста ('hf' или 'xvtr_N'): КВ и каждый трансвертер раздельно. class procedure DefaultPans(out P: TPansConfig); function LoadPans(const MAC: array of Byte; const Ctx: string; out P: TPansConfig): Boolean; procedure SavePans(const MAC: array of Byte; const Ctx: string; const P: TPansConfig); // Web-сервер — глобальные настройки (секция "web" в корне JSON). class procedure DefaultWeb(out W: TWebSettings); procedure LoadWebSettings(out W: TWebSettings); procedure SaveWebSettings(const W: TWebSettings); // Тема — глобальная настройка, не привязана к устройству procedure SaveTheme(ALightTheme: Boolean); function LoadTheme: Boolean; end; implementation function LegacyAverageTimeMS(FPS, AvgMode, AvgCount: Integer; Backmult, DefaultMS: Double): Double; var FPSv: Integer; begin if AvgMode <= 0 then Exit(0.0); FPSv := EnsureRange(FPS, 1, 100); if (Backmult > 0.0) and (Backmult < 1.0) then Result := -1000.0 / (FPSv * Ln(Backmult)) else Result := (1000.0 * Max(1, AvgCount)) / FPSv; if IsNan(Result) or IsInfinite(Result) or (Result <= 0.0) then Result := DefaultMS; Result := EnsureRange(Result, 10.0, 5000.0); end; class function TSettingsManager.MacToStr(const MAC: array of Byte): string; begin Result := Format('%02X:%02X:%02X:%02X:%02X:%02X', [MAC[0],MAC[1],MAC[2],MAC[3],MAC[4],MAC[5]]); end; class procedure TSettingsManager.DefaultBand(BandIdx: Integer; out B: TBandSettings); begin FillChar(B, SizeOf(B), 0); B.VfoA := CFG_BAND_DEFAULT_FREQ[BandIdx]; B.VfoB := CFG_BAND_DEFAULT_FREQ[BandIdx]; B.Mode := CFG_BAND_DEFAULT_MODE[BandIdx]; B.FilterIdx := 5; B.FilterBW := 2700; B.AGCMode := 1; B.AGCTop := 90; B.CTun := False; B.CenterHz := 0; // 0 = нет сохранённого центра → центрировать на VFO B.SpanHz := 192000; B.RxGainMode := 2; // slow_attack (как глобальный дефолт Pluto) B.RxGainDb := 40; B.FMSQOn := False; B.FMSQLevel := 30; B.CTCSSOn := False; B.CTCSSToneIdx := 0; B.FMStepOn := True; B.FMStepIdx := 3; // 25 kHz B.FMRptDir := 0; B.FMRptOffsetHz := 0.0; B.ZoomFactor := 0.0; B.PanSlider := 0.5; end; class procedure TSettingsManager.DefaultGlobal(out G: TGlobalSettings); var i: Integer; begin FillChar(G, SizeOf(G), 0); G.Volume := 70; G.TxMonVolume := 40; G.DriveLevel := 50; G.ActiveVfo := 0; G.NRMode := 0; G.NBMode := 0; G.SNBEnabled := False; G.ANFEnabled := False; G.AGCSlope := 0; G.AGCHangThreshold := 100; G.LastBand := 5; G.LastXvtr := -1; G.WfAGCEnabled := True; G.WfNFEnabled := False; G.SampleRate := 192000; G.RxGainMode := 2; G.RxGainDb := 40; G.PAMaxPower := 100.0; G.PlutoTxMaxAttDb := -10.0; for i := 0 to CFG_BAND_COUNT-1 do G.PABandCal[i] := CFG_PA_BAND_CAL_DEFAULT[i]; for i := 0 to CFG_XVTR_COUNT-1 do G.VHFBandCal[i] := CFG_VHF_CAL_DEFAULT; // Display defaults G.FFTSize := 131072; G.WindowType := 2; // Hann G.SpecDetector := 0; // Peak G.SpecAvgMode := 3; // Log Recursive G.SpecAvgTimeMS := 30.0; G.WfDetector := 0; // Peak G.WfAvgMode := 3; // Log Recursive G.WfAvgTimeMS := 120.0; G.WfManualHigh := -80.0; G.WfManualLow := -130.0; G.WfAGCOffset := 0.0; G.WfPalette := 0; // Classic G.WfGamma := 1.0; G.DisplayPixels := 4096; // 1:1 до 4K-ширины G.WfSpeedDiv := 1; // полная скорость G.SpecMSAA := 4; // сглаживание кривой спектра G.SpecRefLevel := -20.0; G.SpecRange := 110.0; G.SpecGridStep := 10.0; G.AudioOutDevice := ''; G.AudioSampleRate := 48000; G.AudioInDevice := ''; G.SendAudioToRadio := False; G.ShowSpectrum := True; G.ShowWaterfall := True; G.ShowWideband := False; G.WidebandFill := False; G.SpectrumFill := True; G.DisplayDuplex := False; G.RxMuteOnTx := True; G.DisplayFPS := 60; G.FreqMhzDigits := 3; // CAT defaults for i := 0 to 3 do begin G.CATSerialEnabled[i] := False; {$IFDEF MSWINDOWS} G.CATSerialPort[i] := 'COM' + IntToStr(i+1); {$ELSE} G.CATSerialPort[i] := '/dev/ttyS' + IntToStr(i); {$ENDIF} G.CATSerialBaud[i] := 9600; G.CATSerialDataBits[i] := 8; G.CATSerialStopBits[i] := 1; G.CATSerialParity[i] := 0; // None G.CATSerialAndromeda[i]:= False; end; G.CATTcpEnabled := False; G.CATTcpPort := 19090; // ADC — включены по умолчанию: поддерживаются на Hermes, Angelia, Orion G.DitherEnabled := True; G.RandomEnabled := True; 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; 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); // Visibility settings G.ShowSpectrum := JB(GObj,'show_spectrum',True); G.ShowWaterfall := JB(GObj,'show_waterfall',True); G.ShowWideband := JB(GObj,'show_wideband',False); G.WidebandFill := JB(GObj,'wideband_fill',False); G.SpectrumFill := JB(GObj,'spectrum_fill',True); G.DisplayDuplex := JB(GObj,'display_duplex',False); G.RxMuteOnTx := JB(GObj,'rx_mute_on_tx',True); G.DisplayFPS := JI(GObj,'display_fps',60); G.LightTheme := JB(GObj,'light_theme',False); G.FreqMhzDigits := EnsureRange(JI(GObj,'freq_mhz_digits',3), 3, 5); // PA settings G.PAMaxPower := JD(GObj,'pa_max_power',100.0); G.PlutoTxMaxAttDb := JD(GObj,'pluto_tx_max_att',-10.0); for i := 0 to CFG_BAND_COUNT-1 do G.PABandCal[i] := JD(GObj,'pa_band_cal_'+IntToStr(i), CFG_PA_BAND_CAL_DEFAULT[i]); for i := 0 to CFG_XVTR_COUNT-1 do G.VHFBandCal[i] := JD(GObj,'vhf_cal_'+IntToStr(i), CFG_VHF_CAL_DEFAULT); // CAT serial settings for i := 0 to 3 do begin G.CATSerialEnabled[i] := JB(GObj,'cat_serial_en_'+IntToStr(i), False); {$IFDEF MSWINDOWS} G.CATSerialPort[i] := JS(GObj,'cat_serial_port_'+IntToStr(i), 'COM'+IntToStr(i+1)); {$ELSE} G.CATSerialPort[i] := JS(GObj,'cat_serial_port_'+IntToStr(i), '/dev/ttyS'+IntToStr(i)); {$ENDIF} G.CATSerialBaud[i] := JI(GObj,'cat_serial_baud_'+IntToStr(i), 9600); G.CATSerialDataBits[i] := JI(GObj,'cat_serial_dbits_'+IntToStr(i), 8); G.CATSerialStopBits[i] := JI(GObj,'cat_serial_sbits_'+IntToStr(i), 1); G.CATSerialParity[i] := JI(GObj,'cat_serial_parity_'+IntToStr(i), 0); G.CATSerialAndromeda[i]:= JB(GObj,'cat_serial_androm_'+IntToStr(i), False); end; G.CATTcpEnabled := JB(GObj,'cat_tcp_enabled', False); G.CATTcpPort := JI(GObj,'cat_tcp_port', 19090); // ADC settings G.DitherEnabled := JB(GObj,'dither_enabled', True); G.RandomEnabled := JB(GObj,'random_enabled', True); 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 := JI(BObj,'mode', Bands[i].Mode); Bands[i].FilterIdx := JI(BObj,'filter_idx', Bands[i].FilterIdx); Bands[i].FilterBW := JI(BObj,'filter_bw', Bands[i].FilterBW); Bands[i].AGCMode := JI(BObj,'agc_mode', Bands[i].AGCMode); Bands[i].AGCTop := JI(BObj,'agc_top', Bands[i].AGCTop); Bands[i].CTun := JB(BObj,'ctun', Bands[i].CTun); Bands[i].CenterHz := JD(BObj,'center_hz', Bands[i].CenterHz); Bands[i].SpanHz := JD(BObj,'span_hz', Bands[i].SpanHz); // FM squelch / CTCSS / step / repeater — как в LoadBand (раньше пропускались // здесь → на старте сбрасывались в дефолт, хотя SaveBand их пишет). Bands[i].FMSQOn := JB(BObj,'fmsq_on', Bands[i].FMSQOn); Bands[i].FMSQLevel := JI(BObj,'fmsq_level', Bands[i].FMSQLevel); Bands[i].CTCSSOn := JB(BObj,'ctcss_on', Bands[i].CTCSSOn); Bands[i].CTCSSToneIdx := EnsureRange(JI(BObj,'ctcss_idx', Bands[i].CTCSSToneIdx), 0, 37); Bands[i].FMStepOn := JB(BObj,'fmstep_on', Bands[i].FMStepOn); Bands[i].FMStepIdx := EnsureRange(JI(BObj,'fmstep_idx', Bands[i].FMStepIdx), 0, 3); Bands[i].FMRptDir := EnsureRange(JI(BObj,'fmrpt_dir', Bands[i].FMRptDir), 0, 2); Bands[i].FMRptOffsetHz := JD(BObj,'fmrpt_offset', Bands[i].FMRptOffsetHz); Bands[i].RxGainMode := EnsureRange(JI(BObj,'rx_gain_mode', Bands[i].RxGainMode), 0, 3); Bands[i].RxGainDb := EnsureRange(JI(BObj,'rx_gain_db', Bands[i].RxGainDb), 0, 73); end; end; procedure TSettingsManager.SaveGlobal(const MAC: array of Byte; const G: TGlobalSettings); var O: TJSONObject; i: Integer; begin O := GetGlobalObj(GetDevObj(MacToStr(MAC))); JW(O,'volume',G.Volume); JW(O,'tx_mon_volume',G.TxMonVolume); JW(O,'drive_level',G.DriveLevel); JW(O,'active_vfo',G.ActiveVfo); JW(O,'nr_mode',G.NRMode); JW(O,'nb_mode',G.NBMode); JW(O,'nr_enabled',G.NRMode <> 0); JW(O,'nb_enabled',G.NBMode <> 0); JW(O,'snb_enabled',G.SNBEnabled); JW(O,'anf_enabled',G.ANFEnabled); JW(O,'agc_slope',G.AGCSlope); JW(O,'agc_hang_threshold',G.AGCHangThreshold); JW(O,'wf_agc_enabled',G.WfAGCEnabled); JW(O,'wf_nf_enabled',G.WfNFEnabled); JW(O,'last_band',G.LastBand); JW(O,'last_xvtr',G.LastXvtr); JW(O,'sample_rate',G.SampleRate); JW(O,'rx_gain_mode',G.RxGainMode); JW(O,'rx_gain_db',G.RxGainDb); // Display settings JW(O,'fft_size',G.FFTSize); JW(O,'win_type',G.WindowType); JW(O,'spec_detector',G.SpecDetector); JW(O,'spec_avg_mode',G.SpecAvgMode); JW(O,'spec_avg_time_ms',G.SpecAvgTimeMS); JW(O,'wf_detector',G.WfDetector); JW(O,'wf_avg_mode',G.WfAvgMode); JW(O,'wf_avg_time_ms',G.WfAvgTimeMS); JW(O,'wf_manual_high',G.WfManualHigh); JW(O,'wf_manual_low',G.WfManualLow); JW(O,'wf_agc_offset',G.WfAGCOffset); JW(O,'wf_palette',G.WfPalette); JW(O,'wf_gamma',G.WfGamma); JW(O,'display_pixels',G.DisplayPixels); JW(O,'wf_speed_div',G.WfSpeedDiv); JW(O,'spec_msaa',G.SpecMSAA); JW(O,'spec_ref_level',G.SpecRefLevel); JW(O,'spec_range',G.SpecRange); JW(O,'spec_grid_step',G.SpecGridStep); // Audio settings JWS(O,'audio_out_device',G.AudioOutDevice); JW(O,'audio_sample_rate',G.AudioSampleRate); JWS(O,'audio_in_device',G.AudioInDevice); JW(O,'send_audio_to_radio',G.SendAudioToRadio); // Visibility settings JW(O,'show_spectrum',G.ShowSpectrum); JW(O,'show_waterfall',G.ShowWaterfall); JW(O,'show_wideband',G.ShowWideband); JW(O,'wideband_fill',G.WidebandFill); JW(O,'spectrum_fill',G.SpectrumFill); JW(O,'display_duplex',G.DisplayDuplex); JW(O,'rx_mute_on_tx',G.RxMuteOnTx); JW(O,'display_fps',G.DisplayFPS); JW(O,'light_theme',G.LightTheme); JW(O,'freq_mhz_digits',G.FreqMhzDigits); // PA settings JW(O,'pa_max_power',G.PAMaxPower); JW(O,'pluto_tx_max_att',G.PlutoTxMaxAttDb); for i := 0 to CFG_BAND_COUNT-1 do JW(O,'pa_band_cal_'+IntToStr(i),G.PABandCal[i]); for i := 0 to CFG_XVTR_COUNT-1 do JW(O,'vhf_cal_'+IntToStr(i),G.VHFBandCal[i]); // CAT serial settings for i := 0 to 3 do begin JW(O,'cat_serial_en_'+IntToStr(i), G.CATSerialEnabled[i]); JWS(O,'cat_serial_port_'+IntToStr(i),G.CATSerialPort[i]); JW(O,'cat_serial_baud_'+IntToStr(i), G.CATSerialBaud[i]); JW(O,'cat_serial_dbits_'+IntToStr(i),G.CATSerialDataBits[i]); JW(O,'cat_serial_sbits_'+IntToStr(i),G.CATSerialStopBits[i]); JW(O,'cat_serial_parity_'+IntToStr(i),G.CATSerialParity[i]); JW(O,'cat_serial_androm_'+IntToStr(i),G.CATSerialAndromeda[i]); end; JW(O,'cat_tcp_enabled',G.CATTcpEnabled); JW(O,'cat_tcp_port',G.CATTcpPort); // ADC settings JW(O,'dither_enabled',G.DitherEnabled); JW(O,'random_enabled',G.RandomEnabled); end; procedure TSettingsManager.SaveBand(const MAC: array of Byte; BandIdx: Integer; const B: TBandSettings); var O: TJSONObject; begin if (BandIdx < 0) or (BandIdx >= CFG_BAND_COUNT) then Exit; O := GetBandObj(GetDevObj(MacToStr(MAC)), BandIdx); JW(O,'vfo_a',B.VfoA); JW(O,'vfo_b',B.VfoB); JW(O,'mode',B.Mode); JW(O,'filter_idx',B.FilterIdx); JW(O,'filter_bw',B.FilterBW); JW(O,'agc_mode',B.AGCMode); JW(O,'agc_top',B.AGCTop); JW(O,'ctun',B.CTun); JW(O,'center_hz',B.CenterHz); JW(O,'span_hz',B.SpanHz); JW(O,'fmsq_on',B.FMSQOn); JW(O,'fmsq_level',B.FMSQLevel); JW(O,'ctcss_on',B.CTCSSOn); JW(O,'ctcss_idx',B.CTCSSToneIdx); JW(O,'fmstep_on',B.FMStepOn); JW(O,'fmstep_idx',B.FMStepIdx); JW(O,'fmrpt_dir',B.FMRptDir); JW(O,'fmrpt_offset',B.FMRptOffsetHz); JW(O,'zoom_factor',B.ZoomFactor); JW(O,'pan_slider',B.PanSlider); JW(O,'rx_gain_mode',B.RxGainMode); JW(O,'rx_gain_db',B.RxGainDb); end; function TSettingsManager.LoadBand(const MAC: array of Byte; BandIdx: Integer; out B: TBandSettings): Boolean; var MacStr: string; O: TJSONObject; begin DefaultBand(BandIdx, B); MacStr := MacToStr(MAC); Result := FRoot.Find(MacStr) <> nil; if not Result then Exit; O := GetBandObj(GetDevObj(MacStr), BandIdx); B.VfoA := JD(O,'vfo_a', B.VfoA); B.VfoB := JD(O,'vfo_b', B.VfoB); B.Mode := JI(O,'mode', B.Mode); B.FilterIdx := JI(O,'filter_idx', B.FilterIdx); B.FilterBW := JI(O,'filter_bw', B.FilterBW); B.AGCMode := JI(O,'agc_mode', B.AGCMode); B.AGCTop := JI(O,'agc_top', B.AGCTop); B.CTun := JB(O,'ctun', B.CTun); B.CenterHz := JD(O,'center_hz', B.CenterHz); B.SpanHz := JD(O,'span_hz', B.SpanHz); B.FMSQOn := JB(O,'fmsq_on', B.FMSQOn); B.FMSQLevel := JI(O,'fmsq_level', B.FMSQLevel); B.CTCSSOn := JB(O,'ctcss_on', B.CTCSSOn); B.CTCSSToneIdx := EnsureRange(JI(O,'ctcss_idx', B.CTCSSToneIdx), 0, 37); B.FMStepOn := JB(O,'fmstep_on', B.FMStepOn); B.FMStepIdx := EnsureRange(JI(O,'fmstep_idx', B.FMStepIdx), 0, 3); B.FMRptDir := EnsureRange(JI(O,'fmrpt_dir', B.FMRptDir), 0, 2); B.FMRptOffsetHz := JD(O,'fmrpt_offset', B.FMRptOffsetHz); B.RxGainMode := EnsureRange(JI(O,'rx_gain_mode', B.RxGainMode), 0, 3); B.RxGainDb := EnsureRange(JI(O,'rx_gain_db', B.RxGainDb), 0, 73); B.ZoomFactor := EnsureRange(JD(O,'zoom_factor', B.ZoomFactor), 0.0, 1.0); B.PanSlider := EnsureRange(JD(O,'pan_slider', B.PanSlider), 0.0, 1.0); end; procedure TSettingsManager.SaveWindowBounds(L, T, W, H, DPI: Integer; Maximized: Boolean); var O: TJSONObject; begin O := EnsureObj(FRoot, 'window'); JW(O, 'left', L); JW(O, 'top', T); JW(O, 'width', W); JW(O, 'height', H); JW(O, 'dpi', DPI); JW(O, 'maximized', Maximized); end; procedure TSettingsManager.LoadWindowBounds(out L, T, W, H, DPI: Integer; out Maximized: Boolean); var O: TJSONObject; begin L := 80; T := 80; W := 1400; H := 900; DPI := 0; Maximized := False; if FRoot.Find('window') = nil then Exit; O := EnsureObj(FRoot, 'window'); L := JI(O, 'left', 80); T := JI(O, 'top', 80); W := JI(O, 'width', 1400); H := JI(O, 'height', 900); DPI := JI(O, 'dpi', 0); Maximized := JB(O, 'maximized', False); end; procedure TSettingsManager.SaveStartupPreview(VfoA, VfoB: Double; SampleRate: Integer); var O: TJSONObject; begin O := EnsureObj(FRoot, 'startup'); JW(O, 'vfo_a', VfoA); JW(O, 'vfo_b', VfoB); JW(O, 'sample_rate', SampleRate); end; function TSettingsManager.LoadStartupPreview(out VfoA, VfoB: Double; out SampleRate: Integer): Boolean; var O: TJSONObject; begin Result := FRoot.Find('startup') <> nil; if not Result then Exit; O := EnsureObj(FRoot, 'startup'); VfoA := JD(O, 'vfo_a', 14200000); VfoB := JD(O, 'vfo_b', 7100000); SampleRate := JI(O, 'sample_rate', 192000); end; procedure TSettingsManager.SaveAudioBufferSize(BufferSize: Integer); var O: TJSONObject; begin if BufferSize <= 128 then BufferSize := 128 else if BufferSize <= 256 then BufferSize := 256 else BufferSize := 512; O := EnsureObj(FRoot, 'audio'); JW(O, 'output_buffer_size', BufferSize); end; function TSettingsManager.LoadAudioBufferSize: Integer; var O: TJSONObject; begin if FRoot.Find('audio') = nil then Exit(128); O := EnsureObj(FRoot, 'audio'); Result := JI(O, 'output_buffer_size', 128); if Result <= 128 then Result := 128 else if Result <= 256 then Result := 256 else Result := 512; end; procedure TSettingsManager.SaveCATSettings(const G: TGlobalSettings); var O: TJSONObject; i: Integer; begin O := EnsureObj(FRoot, 'cat'); JW(O, 'tcp_en', G.CATTcpEnabled); JW(O, 'tcp_port', G.CATTcpPort); for i := 0 to 3 do begin JW(O, 'ser_en_' +IntToStr(i), G.CATSerialEnabled[i]); JWS(O, 'ser_port_' +IntToStr(i), G.CATSerialPort[i]); JW(O, 'ser_baud_' +IntToStr(i), G.CATSerialBaud[i]); JW(O, 'ser_dbits_' +IntToStr(i), G.CATSerialDataBits[i]); JW(O, 'ser_sbits_' +IntToStr(i), G.CATSerialStopBits[i]); JW(O, 'ser_parity_'+IntToStr(i), G.CATSerialParity[i]); JW(O, 'ser_androm_'+IntToStr(i), G.CATSerialAndromeda[i]); end; end; procedure TSettingsManager.LoadCATSettings(var G: TGlobalSettings); var O: TJSONObject; i: Integer; begin if FRoot.Find('cat') = nil then Exit; O := EnsureObj(FRoot, 'cat'); G.CATTcpEnabled := JB(O, 'tcp_en', False); G.CATTcpPort := JI(O, 'tcp_port', 19090); for i := 0 to 3 do begin G.CATSerialEnabled[i] := JB(O, 'ser_en_' +IntToStr(i), False); {$IFDEF MSWINDOWS} G.CATSerialPort[i] := JS(O, 'ser_port_' +IntToStr(i), 'COM'+IntToStr(i+1)); {$ELSE} G.CATSerialPort[i] := JS(O, 'ser_port_' +IntToStr(i), '/dev/ttyS'+IntToStr(i)); {$ENDIF} G.CATSerialBaud[i] := JI(O, 'ser_baud_' +IntToStr(i), 9600); G.CATSerialDataBits[i] := JI(O, 'ser_dbits_' +IntToStr(i), 8); G.CATSerialStopBits[i] := JI(O, 'ser_sbits_' +IntToStr(i), 1); G.CATSerialParity[i] := JI(O, 'ser_parity_'+IntToStr(i), 0); G.CATSerialAndromeda[i]:= JB(O, 'ser_androm_'+IntToStr(i), False); end; end; // --------------------------------------------------------------------------- // TX settings (per-device, JSON-секция "tx" под MAC) // --------------------------------------------------------------------------- class procedure TSettingsManager.DefaultTX(out T: TTXSettings); var i: Integer; const // 10-полосный голосовой EQ (Hz). [0]=preamp slot, [1..10]=bands. // Подобрано под голос, а не под музыку: больше точек в 100..3500 Hz. DefaultEQFreqs: array[0..10] of Double = ( 0, // [0] preamp slot — частота не используется 100, 200, 400, 700, 1100, 1500, 2000, 2500, 3000, 3500); begin FillChar(T, SizeOf(T), 0); T.MicLineIn := False; T.MicBoost := False; T.MicPTTEnabled := False; T.MicTipRing := False; // Tip (mic at Tip, PTT at Ring) T.MicBias := True; T.LineInGainDB := 0.0; // 0 dB T.MicGainDB := 10.0; // ATT on TX — Thetis default 31 dB (полная защита RX-ADC во время передачи) T.AttOnTX := 31; // Tune — Thetis defaults T.TUNLevel := 10; T.TUNFreq := 750; // SSB-фильтр по умолчанию T.FilterLow := 100; T.FilterHigh := 2800; T.FilterNC := 1024; T.FilterMP := 1; // Low Latency (как в piHPSDR) T.FilterWindow := 1; // BH4 / Blackman-Harris // Compressor — выкл по умолчанию T.CompressorOn := False; T.CompressorGain:= 5.0; // Leveler — вкл, типичные значения piHPSDR/Thetis T.LevelerOn := True; T.LevelerTop := 5.0; T.LevelerDecay := 500; // ALC — вкл всегда (защита от перегруза DAC) T.ALCOn := True; T.ALCMaxGain := 0.0; T.ALCDecay := 10; // Phase Rotator — выкл T.PhaseRotOn := False; T.PhaseRotStages:= 8; T.PhaseRotFreq := 338.0; // EQ — выкл, нулевые гейны T.EQOn := False; T.EQNumBands := 10; for i := 0 to 10 do begin T.EQGains[i] := 0.0; T.EQFreqs[i] := DefaultEQFreqs[i]; end; // AM T.AMCarrierLevel := 0.5; // FM T.FMDeviation := 5000.0; T.FMLowCut := 300; T.FMHighCut := 3000; T.FMEmphPosition := 1; // PostEQ // CTCSS T.CTCSSOn := False; T.CTCSSFreq := 100.0; // TX Display defaults — те же что у RX (см. DefaultGlobal) T.TXFFTSize := 16384; // меньше чем RX — TX-полоса узкая, FFT не нужен большой T.TXWindowType := 2; // Hann T.TXSpecDetector := 0; // Peak T.TXSpecAvgMode := 3; // Log Recursive T.TXSpecAvgTimeMS := 30.0; T.TXWfDetector := 0; T.TXWfAvgMode := 3; T.TXWfAvgTimeMS := 120.0; // TX Grid — потолок 0 dBm (TX-уровни выше RX), 80 dB динамики, шаг 10 dB T.TXSpecRefLevel := 0.0; T.TXSpecRange := 80.0; T.TXSpecGridStep := 10.0; 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); 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); end; class procedure TSettingsManager.DefaultAlex(out A: TAlexSettings); var i: Integer; begin FillChar(A, SizeOf(A), 0); for i := 0 to CFG_BAND_COUNT - 1 do begin A.RxAnt[i] := 1; // ANT1 — безопасный дефолт A.TxAnt[i] := 1; // ANT1 A.RxOnly[i] := 0; // вторичный вход выключен A.DoNotTx[i] := False; end; A.RxBypassOnTx := False; A.Ext1OnTx := False; A.GndBpf2OnTx := False; A.EnableXvtrHf := False; end; function TSettingsManager.LoadAlex(const MAC: array of Byte; out A: TAlexSettings): Boolean; var MacStr: string; DevObj, O: TJSONObject; i: Integer; begin DefaultAlex(A); MacStr := MacToStr(MAC); Result := FRoot.Find(MacStr) <> nil; if not Result then Exit; DevObj := GetDevObj(MacStr); if DevObj.Find('alex') = nil then Exit; O := EnsureObj(DevObj, 'alex'); for i := 0 to CFG_BAND_COUNT - 1 do begin A.RxAnt[i] := EnsureRange(JI(O, 'rx_ant_' + IntToStr(i), 1), 1, 3); A.TxAnt[i] := EnsureRange(JI(O, 'tx_ant_' + IntToStr(i), 1), 1, 3); A.RxOnly[i] := EnsureRange(JI(O, 'rx_only_' + IntToStr(i), 0), 0, 3); A.DoNotTx[i] := JB(O, 'do_not_tx_' + IntToStr(i), False); end; A.RxBypassOnTx := JB(O, 'rx_bypass_on_tx', False); A.Ext1OnTx := JB(O, 'ext1_on_tx', False); A.GndBpf2OnTx := JB(O, 'gnd_bpf2_on_tx', False); A.EnableXvtrHf := JB(O, 'enable_xvtr_hf', False); end; class procedure TSettingsManager.DefaultAnt936x(out A: TAD936xAnt); var i: Integer; begin for i := 0 to CFG_BAND_COUNT - 1 do begin A.RxAnt[i] := 0; // RX1 A.TxAnt[i] := 0; // TX1 end; for i := 0 to CFG_XVTR_COUNT - 1 do begin A.XvtrRxAnt[i] := 0; A.XvtrTxAnt[i] := 0; end; end; function TSettingsManager.LoadAnt936x(const MAC: array of Byte; out A: TAD936xAnt): Boolean; var MacStr: string; DevObj, O: TJSONObject; i: Integer; begin DefaultAnt936x(A); MacStr := MacToStr(MAC); Result := FRoot.Find(MacStr) <> nil; if not Result then Exit; DevObj := GetDevObj(MacStr); if DevObj.Find('ant936x') = nil then begin Result := False; Exit; end; O := EnsureObj(DevObj, 'ant936x'); for i := 0 to CFG_BAND_COUNT - 1 do begin A.RxAnt[i] := EnsureRange(JI(O, 'rx_' + IntToStr(i), 0), 0, 1); A.TxAnt[i] := EnsureRange(JI(O, 'tx_' + IntToStr(i), 0), 0, 1); end; for i := 0 to CFG_XVTR_COUNT - 1 do begin A.XvtrRxAnt[i] := EnsureRange(JI(O, 'xvtr_rx_' + IntToStr(i), 0), 0, 1); A.XvtrTxAnt[i] := EnsureRange(JI(O, 'xvtr_tx_' + IntToStr(i), 0), 0, 1); end; end; procedure TSettingsManager.SaveAnt936x(const MAC: array of Byte; const A: TAD936xAnt); var O: TJSONObject; i: Integer; begin O := EnsureObj(GetDevObj(MacToStr(MAC)), 'ant936x'); for i := 0 to CFG_BAND_COUNT - 1 do begin JW(O, 'rx_' + IntToStr(i), Integer(A.RxAnt[i])); JW(O, 'tx_' + IntToStr(i), Integer(A.TxAnt[i])); end; for i := 0 to CFG_XVTR_COUNT - 1 do begin JW(O, 'xvtr_rx_' + IntToStr(i), Integer(A.XvtrRxAnt[i])); JW(O, 'xvtr_tx_' + IntToStr(i), Integer(A.XvtrTxAnt[i])); end; end; procedure TSettingsManager.SaveAlex(const MAC: array of Byte; const A: TAlexSettings); var O: TJSONObject; i: Integer; begin O := EnsureObj(GetDevObj(MacToStr(MAC)), 'alex'); for i := 0 to CFG_BAND_COUNT - 1 do begin JW(O, 'rx_ant_' + IntToStr(i), Integer(A.RxAnt[i])); JW(O, 'tx_ant_' + IntToStr(i), Integer(A.TxAnt[i])); JW(O, 'rx_only_' + IntToStr(i), Integer(A.RxOnly[i])); JW(O, 'do_not_tx_' + IntToStr(i), A.DoNotTx[i]); end; JW(O, 'rx_bypass_on_tx', A.RxBypassOnTx); JW(O, 'ext1_on_tx', A.Ext1OnTx); JW(O, 'gnd_bpf2_on_tx', A.GndBpf2OnTx); JW(O, 'enable_xvtr_hf', A.EnableXvtrHf); end; // --------------------------------------------------------------------------- // XVTR settings (per-device, JSON-секция "xvtr" под MAC) // --------------------------------------------------------------------------- class procedure TSettingsManager.DefaultXvtr(out X: TXvtrSettings); var i: Integer; begin FillChar(X, SizeOf(X), 0); X.UseXVTRTunePower := False; // Все слоты по умолчанию disabled. Первые два предзаполнены типичными // значениями (2m с LO=116MHz, 70cm с LO=404MHz) — чтобы пользователь // мог быстро включить. for i := 0 to CFG_XVTR_COUNT - 1 do begin X.Entries[i].Enabled := False; X.Entries[i].ButtonText := ''; X.Entries[i].FreqBegin := 0.0; X.Entries[i].FreqEnd := 0.0; X.Entries[i].LOOffset := 0.0; X.Entries[i].LOError := 0.0; X.Entries[i].FullDuplexTx := False; X.Entries[i].TxLOOffset := 0.0; X.Entries[i].RXGain := 0.0; X.Entries[i].RXOnly := False; X.Entries[i].TXPower := 100; X.Entries[i].DisablePA := False; X.Entries[i].RXAntenna := 0; X.Entries[i].LastFreq := 0.0; X.Entries[i].LastFreqB := 0.0; X.Entries[i].LastMode := 1; // USB — VHF SSB calling default X.Entries[i].LastFilterIdx := 0; // NFM X.Entries[i].LastFMSQOn := False; X.Entries[i].LastFMSQLevel := 30; X.Entries[i].LastCTCSSOn := False; X.Entries[i].LastCTCSSToneIdx := 0; X.Entries[i].LastFMStepOn := True; X.Entries[i].LastFMStepIdx := 3; // 25 kHz X.Entries[i].LastCTun := False; X.Entries[i].LastCenterHz := 0.0; X.Entries[i].LastAGCMode := 1; X.Entries[i].LastAGCTop := 90; X.Entries[i].LastFMRptDir := 0; X.Entries[i].LastFMRptOffsetHz := 0.0; X.Entries[i].LastCTCSSAutoActive := False; X.Entries[i].LastFMRptAutoActive := False; X.Entries[i].LastZoomFactor := 0.0; X.Entries[i].LastPanSlider := 0.5; X.Entries[i].LastRxGainMode := 2; // slow_attack X.Entries[i].LastRxGainDb := 40; end; // Шаблоны (пользователь должен сам Enable, чтобы трансверторы появились) X.Entries[0].ButtonText := '2m'; X.Entries[0].FreqBegin := 144000000; X.Entries[0].FreqEnd := 148000000; X.Entries[0].LOOffset := 116000000; X.Entries[0].LastFreq := 144300000; X.Entries[0].LastFreqB := 145500000; X.Entries[1].ButtonText := '70cm'; X.Entries[1].FreqBegin := 430000000; X.Entries[1].FreqEnd := 440000000; X.Entries[1].LOOffset := 404000000; X.Entries[1].LastFreq := 432200000; X.Entries[1].LastFreqB := 433000000; // QO-100 (Es'hail-2) NB downlink: 10489.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].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), 0, CFG_MODE_MAX); X.Entries[i].LastFilterIdx := EnsureRange(JI(EObj, 'last_filt', X.Entries[i].LastFilterIdx), 0, 9); X.Entries[i].LastFMSQOn := JB(EObj, 'last_fmsq_on', X.Entries[i].LastFMSQOn); X.Entries[i].LastFMSQLevel := EnsureRange(JI(EObj, 'last_fmsq_lvl', X.Entries[i].LastFMSQLevel), 0, 100); X.Entries[i].LastCTCSSOn := JB(EObj, 'last_ctcss_on', X.Entries[i].LastCTCSSOn); X.Entries[i].LastCTCSSToneIdx := EnsureRange(JI(EObj, 'last_ctcss_idx', X.Entries[i].LastCTCSSToneIdx), 0, 37); X.Entries[i].LastFMStepOn := JB(EObj, 'last_fmstep_on', X.Entries[i].LastFMStepOn); X.Entries[i].LastFMStepIdx := EnsureRange(JI(EObj, 'last_fmstep_idx', X.Entries[i].LastFMStepIdx), 0, 3); X.Entries[i].LastCTun := JB(EObj, 'last_ctun', X.Entries[i].LastCTun); X.Entries[i].LastCenterHz := JD(EObj, 'last_center_hz', X.Entries[i].LastCenterHz); X.Entries[i].LastAGCMode := EnsureRange(JI(EObj, 'last_agc_mode', X.Entries[i].LastAGCMode), 0, 4); X.Entries[i].LastAGCTop := EnsureRange(JI(EObj, 'last_agc_top', X.Entries[i].LastAGCTop), 0, 120); X.Entries[i].LastFMRptDir := EnsureRange(JI(EObj, 'last_fmrpt_dir', X.Entries[i].LastFMRptDir), 0, 2); X.Entries[i].LastFMRptOffsetHz := JD(EObj, 'last_fmrpt_offset', X.Entries[i].LastFMRptOffsetHz); X.Entries[i].LastCTCSSAutoActive := JB(EObj, 'last_ctcss_auto', X.Entries[i].LastCTCSSAutoActive); X.Entries[i].LastFMRptAutoActive := JB(EObj, 'last_fmrpt_auto', X.Entries[i].LastFMRptAutoActive); X.Entries[i].LastZoomFactor := EnsureRange(JD(EObj, 'last_zoom_factor', X.Entries[i].LastZoomFactor), 0.0, 1.0); X.Entries[i].LastPanSlider := EnsureRange(JD(EObj, 'last_pan_slider', X.Entries[i].LastPanSlider), 0.0, 1.0); X.Entries[i].LastRxGainMode := EnsureRange(JI(EObj, 'last_rx_gain_mode', X.Entries[i].LastRxGainMode), 0, 3); X.Entries[i].LastRxGainDb := EnsureRange(JI(EObj, 'last_rx_gain_db', X.Entries[i].LastRxGainDb), 0, 73); // Валидация: Last freqs должны быть в [FreqBegin..FreqEnd] if (X.Entries[i].LastFreq < X.Entries[i].FreqBegin) or (X.Entries[i].LastFreq > X.Entries[i].FreqEnd) then X.Entries[i].LastFreq := (X.Entries[i].FreqBegin + X.Entries[i].FreqEnd) / 2.0; if (X.Entries[i].LastFreqB < X.Entries[i].FreqBegin) or (X.Entries[i].LastFreqB > X.Entries[i].FreqEnd) then X.Entries[i].LastFreqB := (X.Entries[i].FreqBegin + X.Entries[i].FreqEnd) / 2.0; end; // QO-100 закреплён за зарезервированным слотом QO100_SLOT. Восстанавливаем // шаблон, если слот не размечен как split-LO транспондер ИЛИ значения пустые // (старый конфиг сохранял слот как обычный пустой XVTR с нулями — тогда // FullDuplexTx по умолчанию True, но LO/offset == 0). Реальный QO-100 всегда // имеет положительный транспондер-offset, так что валидный конфиг не трогаем. if (not X.Entries[QO100_SLOT].FullDuplexTx) or (X.Entries[QO100_SLOT].TxLOOffset <= 0) or (X.Entries[QO100_SLOT].LOOffset <= 0) then begin X.Entries[QO100_SLOT].Enabled := False; X.Entries[QO100_SLOT].ButtonText := 'QO-100'; X.Entries[QO100_SLOT].FreqBegin := 10489500000; X.Entries[QO100_SLOT].FreqEnd := 10490000000; X.Entries[QO100_SLOT].LOOffset := 9750000000; X.Entries[QO100_SLOT].FullDuplexTx := True; X.Entries[QO100_SLOT].TxLOOffset := 8089500000; X.Entries[QO100_SLOT].LastFreq := 10489750000; X.Entries[QO100_SLOT].LastFreqB := 10489750000; end; end; procedure TSettingsManager.MigrateXvtrTemplate(var X: TXvtrSettings; const AName: string; ABegin, AEnd, ALO: Double; AFullDup: Boolean = False; ATxLO: Double = 0.0); var i, freeIdx: Integer; begin freeIdx := -1; for i := 0 to CFG_XVTR_COUNT - 1 do begin if SameText(X.Entries[i].ButtonText, AName) then Exit; // уже есть — ничего не делаем if (freeIdx < 0) and (not X.Entries[i].Enabled) and (Trim(X.Entries[i].ButtonText) = '') then freeIdx := i; end; if freeIdx < 0 then Exit; // нет свободных слотов X.Entries[freeIdx].ButtonText := AName; X.Entries[freeIdx].FreqBegin := ABegin; X.Entries[freeIdx].FreqEnd := AEnd; X.Entries[freeIdx].LOOffset := ALO; X.Entries[freeIdx].FullDuplexTx := AFullDup; X.Entries[freeIdx].TxLOOffset := ATxLO; X.Entries[freeIdx].LastFreq := (ABegin + AEnd) / 2.0; X.Entries[freeIdx].LastFreqB := (ABegin + AEnd) / 2.0; end; procedure TSettingsManager.SaveXvtr(const MAC: array of Byte; const X: TXvtrSettings); var Root, EObj: TJSONObject; i: Integer; begin Root := EnsureObj(GetDevObj(MacToStr(MAC)), 'xvtr'); JW(Root, 'use_tun_power', X.UseXVTRTunePower); for i := 0 to CFG_XVTR_COUNT - 1 do begin EObj := EnsureObj(Root, 'e' + IntToStr(i)); JW(EObj, 'en', X.Entries[i].Enabled); JWS(EObj, 'name', X.Entries[i].ButtonText); JW(EObj, 'fbeg', X.Entries[i].FreqBegin); JW(EObj, 'fend', X.Entries[i].FreqEnd); JW(EObj, 'lo_off', X.Entries[i].LOOffset); JW(EObj, 'lo_err', X.Entries[i].LOError); JW(EObj, 'full_dup', X.Entries[i].FullDuplexTx); JW(EObj, 'tx_lo_off', X.Entries[i].TxLOOffset); JW(EObj, 'rx_gain', X.Entries[i].RXGain); JW(EObj, 'rx_only', X.Entries[i].RXOnly); JW(EObj, 'tx_pwr', X.Entries[i].TXPower); JW(EObj, 'dis_pa', X.Entries[i].DisablePA); JW(EObj, 'rx_ant', Integer(X.Entries[i].RXAntenna)); JW(EObj, 'last_freq', X.Entries[i].LastFreq); JW(EObj, 'last_freq_b', X.Entries[i].LastFreqB); JW(EObj, 'last_mode', X.Entries[i].LastMode); JW(EObj, 'last_filt', X.Entries[i].LastFilterIdx); JW(EObj, 'last_fmsq_on', X.Entries[i].LastFMSQOn); JW(EObj, 'last_fmsq_lvl', X.Entries[i].LastFMSQLevel); JW(EObj, 'last_ctcss_on', X.Entries[i].LastCTCSSOn); JW(EObj, 'last_ctcss_idx',X.Entries[i].LastCTCSSToneIdx); JW(EObj, 'last_fmstep_on', X.Entries[i].LastFMStepOn); JW(EObj, 'last_fmstep_idx',X.Entries[i].LastFMStepIdx); JW(EObj, 'last_ctun', X.Entries[i].LastCTun); JW(EObj, 'last_center_hz', X.Entries[i].LastCenterHz); JW(EObj, 'last_agc_mode', X.Entries[i].LastAGCMode); JW(EObj, 'last_agc_top', X.Entries[i].LastAGCTop); JW(EObj, 'last_fmrpt_dir', X.Entries[i].LastFMRptDir); JW(EObj, 'last_fmrpt_offset', X.Entries[i].LastFMRptOffsetHz); JW(EObj, 'last_ctcss_auto', X.Entries[i].LastCTCSSAutoActive); JW(EObj, 'last_fmrpt_auto', X.Entries[i].LastFMRptAutoActive); JW(EObj, 'last_zoom_factor', X.Entries[i].LastZoomFactor); JW(EObj, 'last_pan_slider', X.Entries[i].LastPanSlider); JW(EObj, 'last_rx_gain_mode', X.Entries[i].LastRxGainMode); JW(EObj, 'last_rx_gain_db', X.Entries[i].LastRxGainDb); end; end; class procedure TSettingsManager.DefaultWeb(out W: TWebSettings); begin W.Enabled := True; W.Port := 8080; W.BindAddr := '0.0.0.0'; W.User := 'admin'; W.Pass := 'hpsdr'; end; procedure TSettingsManager.LoadWebSettings(out W: TWebSettings); var O: TJSONObject; begin DefaultWeb(W); if FRoot.Find('web') = nil then Exit; O := EnsureObj(FRoot, 'web'); W.Enabled := JB(O, 'enabled', True); W.Port := EnsureRange(JI(O, 'port', 8080), 1, 65535); W.BindAddr := JS(O, 'bind_addr', '0.0.0.0'); W.User := JS(O, 'user', 'admin'); W.Pass := JS(O, 'pass', 'hpsdr'); end; procedure TSettingsManager.SaveWebSettings(const W: TWebSettings); var O: TJSONObject; begin O := EnsureObj(FRoot, 'web'); JW(O, 'enabled', W.Enabled); JW(O, 'port', W.Port); JWS(O, 'bind_addr', W.BindAddr); JWS(O, 'user', W.User); JWS(O, 'pass', W.Pass); Save; end; procedure TSettingsManager.SaveTheme(ALightTheme: Boolean); var O: TJSONObject; begin O := EnsureObj(FRoot, 'preferences'); JW(O, 'light_theme', ALightTheme); end; function TSettingsManager.LoadTheme: Boolean; var O: TJSONObject; begin Result := False; if FRoot.Find('preferences') = nil then Exit; O := EnsureObj(FRoot, 'preferences'); Result := JB(O, 'light_theme', False); end; // --------------------------------------------------------------------------- // Панадаптеры + слайсы (per-device, JSON-секция "pans" под MAC) // --------------------------------------------------------------------------- class procedure TSettingsManager.DefaultPans(out P: TPansConfig); var i: Integer; begin P.GridMode := False; P.GridRowShare := 0.5; for i := 0 to CFG_PANS_MAX-1 do begin P.Pans[i].Active := False; P.Pans[i].FreqHz := 0.0; P.Pans[i].RateKHz := 384; P.Pans[i].ADC := 0; P.Pans[i].Zoom := 0.0; P.Pans[i].PanSlider := 0.5; P.Pans[i].StackShare := 1.0; P.Pans[i].ColShare := 1.0; P.Pans[i].SplitRatio := 0.40; // = TPanafallPanel дефолт P.Pans[i].HasDisplay := False; P.Pans[i].DPalette := 0; P.Pans[i].DGamma := 1.0; P.Pans[i].DSpecRef := -20.0; P.Pans[i].DSpecRange := 110.0; P.Pans[i].DWfAGC := True; P.Pans[i].DWfOffset := 0.0; P.Pans[i].DWfHigh := -60.0; P.Pans[i].DWfLow := -120.0; SetLength(P.Pans[i].Slices, 0); end; end; procedure TSettingsManager.SavePans(const MAC: array of Byte; const Ctx: string; const P: TPansConfig); var Root, PanObj, SliceObj, DispObj: TJSONObject; PanArr, SliceArr: TJSONArray; i, j, Idx: Integer; begin Root := EnsureObj(EnsureObj(GetDevObj(MacToStr(MAC)), 'pans'), Ctx); JW(Root, 'grid_mode', P.GridMode); JW(Root, 'grid_row_share', P.GridRowShare); // Список панов пересоздаётся целиком (проще, чем merge по индексам). Idx := Root.IndexOfName('list'); if Idx >= 0 then Root.Delete(Idx); PanArr := TJSONArray.Create; Root.Add('list', PanArr); for i := 0 to CFG_PANS_MAX-1 do begin // Пан 0 пишем всегда (ради его доли стека/слайсов), паны N — только активные. if (i > 0) and not P.Pans[i].Active then Continue; PanObj := TJSONObject.Create; PanArr.Add(PanObj); PanObj.Add('id', i); PanObj.Add('active', P.Pans[i].Active); PanObj.Add('freq', P.Pans[i].FreqHz); PanObj.Add('rate_khz', P.Pans[i].RateKHz); PanObj.Add('adc', P.Pans[i].ADC); PanObj.Add('zoom', P.Pans[i].Zoom); PanObj.Add('pan', P.Pans[i].PanSlider); PanObj.Add('stack_share', P.Pans[i].StackShare); PanObj.Add('col_share', P.Pans[i].ColShare); PanObj.Add('split_ratio', P.Pans[i].SplitRatio); // Per-pan дисплей: пишем блок только при override (иначе пан = глоб. дефолт). if P.Pans[i].HasDisplay then begin DispObj := TJSONObject.Create; PanObj.Add('display', DispObj); DispObj.Add('palette', P.Pans[i].DPalette); DispObj.Add('gamma', P.Pans[i].DGamma); DispObj.Add('spec_ref', P.Pans[i].DSpecRef); DispObj.Add('spec_range', P.Pans[i].DSpecRange); DispObj.Add('wf_agc', P.Pans[i].DWfAGC); DispObj.Add('wf_offset', P.Pans[i].DWfOffset); DispObj.Add('wf_high', P.Pans[i].DWfHigh); DispObj.Add('wf_low', P.Pans[i].DWfLow); end; SliceArr := TJSONArray.Create; PanObj.Add('slices', SliceArr); for j := 0 to High(P.Pans[i].Slices) do begin SliceObj := TJSONObject.Create; SliceArr.Add(SliceObj); SliceObj.Add('target_hz', P.Pans[i].Slices[j].TargetHz); SliceObj.Add('mode', P.Pans[i].Slices[j].Mode); SliceObj.Add('filter_lo', P.Pans[i].Slices[j].FilterLo); SliceObj.Add('filter_hi', P.Pans[i].Slices[j].FilterHi); SliceObj.Add('agc', P.Pans[i].Slices[j].AGC); SliceObj.Add('volume', P.Pans[i].Slices[j].Volume); SliceObj.Add('muted', P.Pans[i].Slices[j].Muted); SliceObj.Add('rx_mute_on_tx', P.Pans[i].Slices[j].RxMuteOnTx); SliceObj.Add('fmsq_on', P.Pans[i].Slices[j].FMSQOn); SliceObj.Add('fmsq_level', P.Pans[i].Slices[j].FMSQLevel); SliceObj.Add('dev_name', P.Pans[i].Slices[j].DevName); end; end; end; function TSettingsManager.LoadPans(const MAC: array of Byte; const Ctx: string; out P: TPansConfig): Boolean; var MacStr: string; DevObj, PansObj, Root, PanObj, SliceObj, DispObj: TJSONObject; ListData, SlData, DispData: TJSONData; Arr, SArr: TJSONArray; i, id, k, n: Integer; begin DefaultPans(P); MacStr := MacToStr(MAC); Result := FRoot.Find(MacStr) <> nil; if not Result then Exit; DevObj := GetDevObj(MacStr); if DevObj.Find('pans') = nil then begin Result := False; Exit; end; PansObj := EnsureObj(DevObj, 'pans'); if PansObj.Find(Ctx) = nil then begin Result := False; Exit; end; Root := EnsureObj(PansObj, Ctx); P.GridMode := JB(Root, 'grid_mode', False); P.GridRowShare := EnsureRange(JD(Root, 'grid_row_share', 0.5), 0.15, 0.85); ListData := Root.Find('list'); if (ListData = nil) or not (ListData is TJSONArray) then Exit; Arr := TJSONArray(ListData); for i := 0 to Arr.Count-1 do begin if not (Arr.Items[i] is TJSONObject) then Continue; PanObj := TJSONObject(Arr.Items[i]); id := JI(PanObj, 'id', -1); if (id < 0) or (id >= CFG_PANS_MAX) then Continue; P.Pans[id].Active := JB(PanObj, 'active', id > 0); P.Pans[id].FreqHz := JD(PanObj, 'freq', 0.0); P.Pans[id].RateKHz := JI(PanObj, 'rate_khz', 384); P.Pans[id].ADC := JI(PanObj, 'adc', 0); P.Pans[id].Zoom := EnsureRange(JD(PanObj, 'zoom', 0.0), 0.0, 1.0); P.Pans[id].PanSlider := EnsureRange(JD(PanObj, 'pan', 0.5), 0.0, 1.0); P.Pans[id].StackShare := JD(PanObj, 'stack_share', 1.0); P.Pans[id].ColShare := JD(PanObj, 'col_share', 1.0); P.Pans[id].SplitRatio := EnsureRange(JD(PanObj, 'split_ratio', 0.40), 0.05, 0.95); // Per-pan дисплей: присутствие блока 'display' = override. DispData := PanObj.Find('display'); if (DispData <> nil) and (DispData is TJSONObject) then begin DispObj := TJSONObject(DispData); P.Pans[id].HasDisplay := True; P.Pans[id].DPalette := EnsureRange(JI(DispObj, 'palette', 0), 0, 2); P.Pans[id].DGamma := EnsureRange(JD(DispObj, 'gamma', 1.0), 0.30, 3.00); P.Pans[id].DSpecRef := JD(DispObj, 'spec_ref', -20.0); P.Pans[id].DSpecRange := JD(DispObj, 'spec_range', 110.0); P.Pans[id].DWfAGC := JB(DispObj, 'wf_agc', True); P.Pans[id].DWfOffset := JD(DispObj, 'wf_offset', 0.0); P.Pans[id].DWfHigh := JD(DispObj, 'wf_high', -60.0); P.Pans[id].DWfLow := JD(DispObj, 'wf_low', -120.0); end; SlData := PanObj.Find('slices'); if (SlData = nil) or not (SlData is TJSONArray) then Continue; SArr := TJSONArray(SlData); SetLength(P.Pans[id].Slices, 0); for k := 0 to SArr.Count-1 do begin if not (SArr.Items[k] is TJSONObject) then Continue; SliceObj := TJSONObject(SArr.Items[k]); n := Length(P.Pans[id].Slices); SetLength(P.Pans[id].Slices, n+1); P.Pans[id].Slices[n].TargetHz := JD(SliceObj, 'target_hz', 0.0); P.Pans[id].Slices[n].Mode := JI(SliceObj, 'mode', 0); P.Pans[id].Slices[n].FilterLo := JI(SliceObj, 'filter_lo', -1500); P.Pans[id].Slices[n].FilterHi := JI(SliceObj, 'filter_hi', 1500); P.Pans[id].Slices[n].AGC := JI(SliceObj, 'agc', 1); P.Pans[id].Slices[n].Volume := EnsureRange(JD(SliceObj, 'volume', 0.7), 0.0, 1.0); P.Pans[id].Slices[n].Muted := JB(SliceObj, 'muted', False); P.Pans[id].Slices[n].RxMuteOnTx := JB(SliceObj, 'rx_mute_on_tx', True); P.Pans[id].Slices[n].FMSQOn := JB(SliceObj, 'fmsq_on', False); P.Pans[id].Slices[n].FMSQLevel := JI(SliceObj, 'fmsq_level', 30); P.Pans[id].Slices[n].DevName := JS(SliceObj, 'dev_name', ''); end; end; end; end.