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, SerialPort, 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) CW_MSG_COUNT = 8; // ячеек памяти телеграфных сообщений (F1..F8) TXPROF_MAX = 16; // потолок именованных TX-профилей на устройство 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]]. // Привязка «режим → индекс TX-профиля» (-1 = не переключать). Такая таблица // есть у каждого флага: у главного в TTXProfileList, у слайса в TSliceCfg. TTXProfModeTable = array[0..CFG_MODE_MAX] of Integer; 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; // имя устройства ВВОДА слайса ('' = общий вход) // TX-профиль слайса ПО РЕЖИМУ: -1 = какой активен, тот и звучит. Таблица, // а не одно значение: слайс на FT8 и слайс голосом помнят каждый своё. TXProfByMode: TTXProfModeTable; 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; // ---- CW ------------------------------------------------------------------ // Телеграф в openHPSDR P2 делает ПРОШИВКА: точки/тире формирует кейер в FPGA, // мы только отдаём ему параметры (DUC Specific байты 5..13, 17). Поэтому // почти всё здесь — свойство устройства, а не «звука», и живёт отдельно от // TTXSettings/TTXProfile (профиль голоса телеграф не переключает). // Раскладка байта 5 — HPSDRProtocol.CW_*; эталон Thetis (console.cs/setup.cs) // и pihpsdr (new_protocol.c:1353). TCWSettings = record Pitch: Integer; // Гц, тон приёма/сайдтона (Thetis default 600) // Память сообщений для программной передачи (окно CW Messages, F1..F8). Messages: array[0..CW_MSG_COUNT-1] of string[63]; // Манипуляция разрешена вообще (False = телеграф без кейера: MOX + голосом // ключа нет). ВМЕСТЕ с LocalKeyer образует одну развилку «кто рисует точки», // в UI это один список: прошивка / программа / выкл. FWKeyer: Boolean; KeyerMode: Integer; // 0=Straight, 1=Iambic A, 2=Iambic B ReverseKeys: Boolean; // поменять точку и тире местами StrictSpacing: Boolean; // строгие межзнаковые интервалы Speed: Integer; // WPM, 5..60 Weight: Integer; // 33..66 (50 = точка:пауза 1:1) BreakIn: Boolean; // прошивка сама поднимает PTT на нажатие ключа HangTimeMS: Integer; // сколько держать TX после последнего элемента RampMS: Integer; // форма фронта посылки (Thetis зашил 9 мс) RFDelayMS: Integer; // задержка РЧ после PTT (byte 13, реле/усилитель) SidetoneHW: Boolean; // тон в наушники САМОГО трансивера (байт 5/6) SidetoneHWLevel: Integer; // 0..127 // Программный сайдтон — свой генератор в аудиотракте PC. Нужен тем, кто // слушает через звуковую карту: аппаратный тон туда физически не попадает // (он подмешивается в наушники трансивера, а не в IQ-поток). // ★Точен там, где момент нажатия знает САМ PC: программная передача текста // (CWX) и прямой ключ. При иамбике таймингом владеет FPGA, и PC видит лишь // состояние лепестков манипулятора — там верен только аппаратный тон. SidetoneSW: Boolean; SidetoneSWLevel: Integer; // 0..100 % от полной шкалы аудио // ---- Локальный (программный) кейер -------------------------------------- // Манипуляцию формирует САМ EWSDR: генератор рисует несущую с огибающей // прямо в поток IQ (CWKeyer.pas). Для Pluto/LibreSDR это единственный // возможный путь — кейера в железе там нет, и выбор «прошивка» вырождается // туда сам; для openHPSDR это опция, по умолчанию выключенная (тайминги в // FPGA точнее любой программы). LocalKeyer: Boolean; // Сдвиг несущей манипуляции от гетеродина. 0 = авто: у openHPSDR 0 (DUC — // цифровой, утекать нечему), у zero-IF AD936x — CW_AUTO_OFFSET_HZ, иначе // утечка LO сидит РОВНО на рабочей частоте и слышна между посылками. CarrierOffsetHz: Integer; // Манипулятор на модемных линиях COM/USB-serial (DTR/RTS питают ключ, // CTS/DSR/DCD/RI читаются как лепестки). KeyPortEnabled: Boolean; KeyPort: string[63]; KeyDotLine: Integer; // 0=CTS, 1=DSR, 2=DCD, 3=RI KeyDashLine: Integer; KeyInvert: Boolean; KeyPowerDTR: Boolean; KeyPowerRTS: Boolean; // ---- Окно-терминал: набор с клавиатуры + декодер приёма ---------------- Decoder: Boolean; // разбирать принимаемый телеграф в текст DecoderStrip: Boolean; // бегущая лента расшифровки под спектром KbdKey: Boolean; // Ctrl в окне терминала работает манипулятором end; // ---- TX-профили (именованные снимки «как я звучу») ---------------------- // Профиль хранит ТОЛЬКО то, что определяет звук в эфире: маршрутизацию // микрофонного входа, усиление, кромки TX-фильтра, динамическую обработку, // эквалайзер, уровень несущей AM и мощность. Всё остальное из TTXSettings // (FIR-длина/фаза/окно фильтра, ATT on TX, FM/CTCSS, параметры анализатора // и сетки, калибровка PureSignal) — свойство устройства, от профиля НЕ // зависит: переключение профиля не должно перерисовывать спектр, менять // задержку тракта или ломать калибровку PS. // Разбиение — по образцу таблицы TXProfile у Thetis (database.cs), но без // её хвоста из VAC/DSP-буферов, которых у нас нет. TTXProfile = record Name: string[31]; // Микрофонный вход (гарнитура / микшер-ESSB — физически разные входы) MicLineIn: Boolean; MicBoost: Boolean; MicPTTEnabled: Boolean; MicTipRing: Boolean; MicBias: Boolean; LineInGainDB: Double; MicGainDB: Double; // Кромки TX-фильтра (FIR-длина/фаза/окно остаются у устройства) FilterLow: Integer; FilterHigh: Integer; // Динамическая обработка CompressorOn: Boolean; CompressorGain: Double; LevelerOn: Boolean; LevelerTop: Double; LevelerDecay: Integer; ALCOn: Boolean; ALCMaxGain: Double; ALCDecay: Integer; PhaseRotOn: Boolean; PhaseRotStages: Integer; PhaseRotFreq: Double; // Эквалайзер EQOn: Boolean; EQNumBands: Integer; EQGains: array[0..10] of Double; EQFreqs: array[0..10] of Double; // Модуляция AM (несущая — часть «звука», в отличие от девиации FM) AMCarrierLevel: Double; // Мощность (Thetis держит Power/Tune_Power в профиле) DrivePercent: Integer; // 0..100 TUNLevel: Integer; // 0..100 end; TTXProfileList = record Count: Integer; ActiveIdx: Integer; // Привязка профиля к ГЛАВНОМУ приёмнику (слайс A) ПО РЕЖИМУ; -1 = ничего // не переключать. У слайсов B..G своя такая же в TSliceCfg.TXProfByMode. // Таблица главного одна на устройство — общая для КВ и трансвертера // (решение пользователя); у слайсов она сама разъезжается по контекстам, // потому что их записи хранятся отдельно для КВ и трансвертера. MainByMode: TTXProfModeTable; Items: array[0..TXPROF_MAX-1] of TTXProfile; end; // Web-сервер — глобальные настройки (не привязаны к устройству, секция "web"). // DX-кластер: телнет-соединение + вид спотов на панадаптере. // Хранится глобально (секция "dxcluster"), не per-device: кластер и позывной // от железа не зависят. TDXClusterSettings = record Enabled: Boolean; // подключаться при старте ShowSpots: Boolean; // рисовать оверлей на спектре Host: string; Port: Integer; // 1..65535 Login: string; // позывной для логина в кластер (он же «свой» в оверлее) Password: string; // редкий кластер спрашивает — обычно пусто PostLogin: string; // команды после логина (по строке; сюда же set/filter) TTLMinutes: Integer; // сколько держать спот (он же горизонт затухания) MaxRows: Integer; // рядов лесенки подписей (1..DXSPOT_MAX_ROWS) // Фильтр по модам. Пустая маска = показывать все (в т.ч. неопознанные). // Биты соответствуют TDXMode: 1=CW,2=SSB,3=DIGI,4=FT8,5=FT4,6=RTTY,7=PSK, // 8=FM,9=SSTV; бит 0 = споты без распознанной моды. ModeMask: LongWord; end; 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; // TCI-сервер (протокол Expert Electronics поверх WebSocket). Авторизации в // протоколе нет, поэтому умолчание слушает только петлю: логгеры и цифровые // программы обычно живут на том же ПК, а наружу порт открывается осознанно. TTCISettings = record Enabled: Boolean; Port: Integer; // 1..65535, умолчание 40001 (как у ExpertSDR3) BindAddr: string; // '127.0.0.1' или '0.0.0.0' // ★Единственный каталог, куда TCI пишет записи линейного выхода // (LINE_OUT_RECORDER_SAVE). Пусто — <каталог конфигурации>/records. // Имя файла клиент выбирает сам, но КАТАЛОГ из его строки не берётся // никогда: авторизации в протоколе нет, и полный путь из сети означал бы // запись в любой доступный процессу файл (см. TCIRecordPath). RecordDir: 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 МГц) // 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; // слот фильтра 0..FILT_SLOTS-1 (10/11 = VAR1/VAR2) 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 // --- Лупа наведения на маяк QO-100 (карточка BEACON TUNING) --- // Центр хранится СДВИГОМ от опорной частоты маяка, а не абсолютом: так // окно переживает и правку опорной частоты, и калибровку LOError. BeaconZoomSpanHz: Double; // ширина окна лупы, Гц BeaconZoomOffsetHz: Double; // центр окна = BeaconRefHz + этот сдвиг BeaconZoomFollow: Boolean; // держать наведение декодера в окне BeaconZoomAuto: Boolean; // автопоиск маяка в окне лупы // --- 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); // CW settings per-device (JSON-секция "cw" под MAC). class procedure DefaultCW(out C: TCWSettings); function LoadCW(const MAC: array of Byte; out C: TCWSettings): Boolean; procedure SaveCW(const MAC: array of Byte; const C: TCWSettings); 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 DefaultDXCluster(out D: TDXClusterSettings); procedure LoadDXClusterSettings(out D: TDXClusterSettings); procedure SaveDXClusterSettings(const D: TDXClusterSettings); class procedure DefaultWeb(out W: TWebSettings); procedure LoadWebSettings(out W: TWebSettings); procedure SaveWebSettings(const W: TWebSettings); // TCI-сервер — глобальные настройки (секция "tci" в корне JSON). class procedure DefaultTCI(out T: TTCISettings); procedure LoadTCISettings(out T: TTCISettings); procedure SaveTCISettings(const T: TTCISettings); // Тема — глобальная настройка, не привязана к устройству 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); // TX-профили per-device (JSON-секция "tx_profiles" под MAC). LoadTXProfiles // возвращает False, если секции ещё нет: вызывающий строит заводской набор // из текущих TX-настроек (DefaultTXProfiles) — миграция старого конфига. function LoadTXProfiles(const MAC: array of Byte; out L: TTXProfileList): Boolean; procedure SaveTXProfiles(const MAC: array of Byte; const L: TTXProfileList); end; // ---- TX-профили ------------------------------------------------------------ // Снимок «звуковых» полей живых настроек в профиль (имя не трогается) и // обратное применение. Единственные две точки, знающие состав профиля, — // добавляя поле в TTXProfile, правь обе. procedure TXProfileCapture(const T: TTXSettings; DrivePct: Integer; var P: TTXProfile); procedure TXProfileApply(const P: TTXProfile; var T: TTXSettings; var DrivePct: Integer); // Заводской набор профилей. Base — текущие настройки устройства: из них // наследуются микрофонный вход, мощность и всё, что профиль не переопределяет // (иначе переключение на «ESSB» сбрасывало бы Mic In/Line In и drive). procedure DefaultTXProfiles(const Base: TTXSettings; DrivePct: Integer; out L: TTXProfileList); // Таблица «режим → профиль» ↔ JSON-массив. Все элементы вне 0..TXPROF_MAX-1 // читаются как -1 («не переключать»), поэтому чужой/старый конфиг безопасен. procedure ClearTXProfModeTable(var T: TTXProfModeTable); function ModeTableToJSON(const T: TTXProfModeTable): TJSONArray; procedure JSONToModeTable(Node: TJSONData; var T: TTXProfModeTable); // Сколько пресетов реально есть у режима (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.BeaconZoomSpanHz := 40000.0; G.BeaconZoomOffsetHz := 0.0; G.BeaconZoomFollow := True; G.BeaconZoomAuto := False; 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; G.CATSerialPort[i] := SerDefaultPortName(i); 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); G.BeaconZoomSpanHz := JD(GObj,'beacon_zoom_span',40000.0); G.BeaconZoomOffsetHz := JD(GObj,'beacon_zoom_offset',0.0); G.BeaconZoomFollow := JB(GObj,'beacon_zoom_follow',True); G.BeaconZoomAuto := JB(GObj,'beacon_zoom_auto',False); // 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); G.CATSerialPort[i] := JS(GObj,'cat_serial_port_'+IntToStr(i), SerDefaultPortName(i)); 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); JW(O,'beacon_zoom_span',G.BeaconZoomSpanHz); JW(O,'beacon_zoom_offset',G.BeaconZoomOffsetHz); JW(O,'beacon_zoom_follow',G.BeaconZoomFollow); JW(O,'beacon_zoom_auto',G.BeaconZoomAuto); // 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); G.CATSerialPort[i] := JS(O, 'ser_port_' +IntToStr(i), SerDefaultPortName(i)); 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; const // Заводская 10-полосная голосовая сетка EQ (Hz). [0]=слот преампа (частота // не используется), [1..10]=узлы кривой. Подобрано под голос, а не под // музыку: больше точек в 100..3500 Hz. Нужна и DefaultTX, и заводским // профилям (сброс тембра в ноль), поэтому живёт на уровне модуля. DEF_EQ_FREQS: array[0..10] of Double = ( 0, 100, 200, 400, 700, 1100, 1500, 2000, 2500, 3000, 3500); // --------------------------------------------------------------------------- // TX settings (per-device, JSON-секция "tx" под MAC) // --------------------------------------------------------------------------- class procedure TSettingsManager.DefaultTX(out T: TTXSettings); var i: Integer; 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] := DEF_EQ_FREQS[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; class procedure TSettingsManager.DefaultCW(out C: TCWSettings); begin FillChar(C, SizeOf(C), 0); C.Pitch := 600; // Thetis cw_pitch default C.FWKeyer := True; // тайминги в FPGA — джиттер PC не влияет C.KeyerMode := 2; // Iambic B C.ReverseKeys := False; C.StrictSpacing := False; C.Speed := 20; C.Weight := 50; C.BreakIn := True; C.HangTimeMS := 300; C.RampMS := 9; // Thetis SetCWEdgeLength(9) C.RFDelayMS := 0; C.SidetoneHW := True; C.SidetoneHWLevel := 50; C.SidetoneSW := False; C.SidetoneSWLevel := 30; C.LocalKeyer := False; // у openHPSDR тайминги в FPGA; Pluto включит сам C.CarrierOffsetHz := 0; // 0 = авто (по типу бэкенда) C.KeyPortEnabled := False; C.KeyPort := ''; C.KeyDotLine := 0; // CTS C.KeyDashLine := 1; // DSR C.KeyInvert := False; C.KeyPowerDTR := True; // типовая распайка: ключ питается от DTR/RTS C.KeyPowerRTS := True; C.Decoder := True; // окно терминала без декодера бессмысленно C.DecoderStrip := False; // лента под спектром — по желанию C.KbdKey := False; // Ctrl-манипулятор включают осознанно // Заводские заготовки: типовой вызов, ответ и стандартные концовки. C.Messages[0] := 'CQ CQ DE '; C.Messages[1] := 'DE '; C.Messages[2] := 'UR RST 599 599 '; C.Messages[3] := 'TU 73 GL'; C.Messages[4] := '?'; C.Messages[5] := 'AGN'; C.Messages[6] := 'QRZ?'; C.Messages[7] := 'TEST'; end; function TSettingsManager.LoadCW(const MAC: array of Byte; out C: TCWSettings): Boolean; var MacStr: string; DevObj, O: TJSONObject; i: Integer; begin DefaultCW(C); MacStr := MacToStr(MAC); Result := FRoot.Find(MacStr) <> nil; if not Result then Exit; DevObj := GetDevObj(MacStr); if DevObj.Find('cw') = nil then Exit; // нет секции — остаются дефолты O := EnsureObj(DevObj, 'cw'); C.Pitch := EnsureRange(JI(O,'pitch', C.Pitch), 200, 1200); C.FWKeyer := JB(O,'fw_keyer', C.FWKeyer); C.KeyerMode := EnsureRange(JI(O,'keyer_mode', C.KeyerMode), 0, 2); C.ReverseKeys := JB(O,'reverse_keys', C.ReverseKeys); C.StrictSpacing := JB(O,'strict_spacing',C.StrictSpacing); C.Speed := EnsureRange(JI(O,'speed', C.Speed), 5, 60); C.Weight := EnsureRange(JI(O,'weight', C.Weight), 33, 66); C.BreakIn := JB(O,'break_in', C.BreakIn); C.HangTimeMS := EnsureRange(JI(O,'hang_time_ms', C.HangTimeMS), 0, 2000); C.RampMS := EnsureRange(JI(O,'ramp_ms', C.RampMS), 0, 20); C.RFDelayMS := EnsureRange(JI(O,'rf_delay_ms', C.RFDelayMS), 0, 255); C.SidetoneHW := JB(O,'sidetone_hw', C.SidetoneHW); C.SidetoneHWLevel := EnsureRange(JI(O,'sidetone_hw_level', C.SidetoneHWLevel), 0, 127); C.SidetoneSW := JB(O,'sidetone_sw', C.SidetoneSW); C.SidetoneSWLevel := EnsureRange(JI(O,'sidetone_sw_level', C.SidetoneSWLevel), 0, 100); C.LocalKeyer := JB(O,'local_keyer', C.LocalKeyer); C.CarrierOffsetHz := EnsureRange(JI(O,'carrier_offset_hz', C.CarrierOffsetHz), -1, 48000); C.KeyPortEnabled := JB(O,'key_port_on', C.KeyPortEnabled); C.KeyPort := Copy(JS(O,'key_port', C.KeyPort), 1, 63); C.KeyDotLine := EnsureRange(JI(O,'key_dot_line', C.KeyDotLine), 0, 3); C.KeyDashLine := EnsureRange(JI(O,'key_dash_line', C.KeyDashLine), 0, 3); C.KeyInvert := JB(O,'key_invert', C.KeyInvert); C.KeyPowerDTR := JB(O,'key_power_dtr', C.KeyPowerDTR); C.KeyPowerRTS := JB(O,'key_power_rts', C.KeyPowerRTS); C.Decoder := JB(O,'decoder', C.Decoder); C.DecoderStrip := JB(O,'decoder_strip', C.DecoderStrip); C.KbdKey := JB(O,'kbd_key', C.KbdKey); for i := 0 to CW_MSG_COUNT - 1 do C.Messages[i] := Copy(JS(O, 'msg_' + IntToStr(i), C.Messages[i]), 1, 63); end; procedure TSettingsManager.SaveCW(const MAC: array of Byte; const C: TCWSettings); var O: TJSONObject; i: Integer; begin O := EnsureObj(GetDevObj(MacToStr(MAC)), 'cw'); JW(O,'pitch', C.Pitch); JW(O,'fw_keyer', C.FWKeyer); JW(O,'keyer_mode', C.KeyerMode); JW(O,'reverse_keys', C.ReverseKeys); JW(O,'strict_spacing', C.StrictSpacing); JW(O,'speed', C.Speed); JW(O,'weight', C.Weight); JW(O,'break_in', C.BreakIn); JW(O,'hang_time_ms', C.HangTimeMS); JW(O,'ramp_ms', C.RampMS); JW(O,'rf_delay_ms', C.RFDelayMS); JW(O,'sidetone_hw', C.SidetoneHW); JW(O,'sidetone_hw_level', C.SidetoneHWLevel); JW(O,'sidetone_sw', C.SidetoneSW); JW(O,'sidetone_sw_level', C.SidetoneSWLevel); JW(O,'local_keyer', C.LocalKeyer); JW(O,'carrier_offset_hz', C.CarrierOffsetHz); JW(O,'key_port_on', C.KeyPortEnabled); JWS(O,'key_port', C.KeyPort); JW(O,'key_dot_line', C.KeyDotLine); JW(O,'key_dash_line', C.KeyDashLine); JW(O,'key_invert', C.KeyInvert); JW(O,'key_power_dtr', C.KeyPowerDTR); JW(O,'key_power_rts', C.KeyPowerRTS); JW(O,'decoder', C.Decoder); JW(O,'decoder_strip', C.DecoderStrip); JW(O,'kbd_key', C.KbdKey); for i := 0 to CW_MSG_COUNT - 1 do JWS(O, 'msg_' + IntToStr(i), C.Messages[i]); 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; // --------------------------------------------------------------------------- // TX-профили: снимок/применение + заводской набор + персист // --------------------------------------------------------------------------- procedure TXProfileCapture(const T: TTXSettings; DrivePct: Integer; var P: TTXProfile); var i: Integer; begin P.MicLineIn := T.MicLineIn; P.MicBoost := T.MicBoost; P.MicPTTEnabled := T.MicPTTEnabled; P.MicTipRing := T.MicTipRing; P.MicBias := T.MicBias; P.LineInGainDB := T.LineInGainDB; P.MicGainDB := T.MicGainDB; P.FilterLow := T.FilterLow; P.FilterHigh := T.FilterHigh; P.CompressorOn := T.CompressorOn; P.CompressorGain := T.CompressorGain; P.LevelerOn := T.LevelerOn; P.LevelerTop := T.LevelerTop; P.LevelerDecay := T.LevelerDecay; P.ALCOn := T.ALCOn; P.ALCMaxGain := T.ALCMaxGain; P.ALCDecay := T.ALCDecay; P.PhaseRotOn := T.PhaseRotOn; P.PhaseRotStages := T.PhaseRotStages; P.PhaseRotFreq := T.PhaseRotFreq; P.EQOn := T.EQOn; P.EQNumBands := T.EQNumBands; for i := 0 to 10 do begin P.EQGains[i] := T.EQGains[i]; P.EQFreqs[i] := T.EQFreqs[i]; end; P.AMCarrierLevel := T.AMCarrierLevel; P.DrivePercent := EnsureRange(DrivePct, 0, 100); P.TUNLevel := EnsureRange(T.TUNLevel, 0, 100); end; procedure TXProfileApply(const P: TTXProfile; var T: TTXSettings; var DrivePct: Integer); var i: Integer; begin T.MicLineIn := P.MicLineIn; T.MicBoost := P.MicBoost; T.MicPTTEnabled := P.MicPTTEnabled; T.MicTipRing := P.MicTipRing; T.MicBias := P.MicBias; T.LineInGainDB := EnsureRange(P.LineInGainDB, -34.5, 12.0); T.MicGainDB := P.MicGainDB; T.FilterLow := P.FilterLow; T.FilterHigh := P.FilterHigh; T.CompressorOn := P.CompressorOn; T.CompressorGain := P.CompressorGain; T.LevelerOn := P.LevelerOn; T.LevelerTop := P.LevelerTop; T.LevelerDecay := P.LevelerDecay; T.ALCOn := P.ALCOn; T.ALCMaxGain := P.ALCMaxGain; T.ALCDecay := P.ALCDecay; T.PhaseRotOn := P.PhaseRotOn; T.PhaseRotStages := P.PhaseRotStages; T.PhaseRotFreq := P.PhaseRotFreq; T.EQOn := P.EQOn; T.EQNumBands := P.EQNumBands; for i := 0 to 10 do begin T.EQGains[i] := P.EQGains[i]; T.EQFreqs[i] := P.EQFreqs[i]; end; T.AMCarrierLevel := P.AMCarrierLevel; T.TUNLevel := EnsureRange(P.TUNLevel, 0, 100); DrivePct := EnsureRange(P.DrivePercent, 0, 100); end; procedure ClearTXProfModeTable(var T: TTXProfModeTable); var i: Integer; begin for i := 0 to CFG_MODE_MAX do T[i] := -1; end; function ModeTableToJSON(const T: TTXProfModeTable): TJSONArray; var i: Integer; begin Result := TJSONArray.Create; for i := 0 to CFG_MODE_MAX do Result.Add(T[i]); end; procedure JSONToModeTable(Node: TJSONData; var T: TTXProfModeTable); var Arr: TJSONArray; i, V: Integer; begin ClearTXProfModeTable(T); if not (Node is TJSONArray) then Exit; Arr := TJSONArray(Node); for i := 0 to Min(CFG_MODE_MAX, Arr.Count - 1) do begin V := Arr.Integers[i]; if (V >= 0) and (V < TXPROF_MAX) then T[i] := V; end; end; procedure DefaultTXProfiles(const Base: TTXSettings; DrivePct: Integer; out L: TTXProfileList); var // Заводской звук + ЖИВАЯ обвязка микрофона. Всё, что определяет тембр // (полоса, динамика, EQ, AM carrier, TUN level), берётся из DefaultTX: // «сброс к заводским» обязан сбрасывать именно это, иначе Default наследует // ту полосу, на которой оператора застали (например 5000 от ESSB 5k). // Не сбрасываем то, что описывает ЖЕЛЕЗО и безопасность: разъём и режим // микрофона с его усилением (иначе слетит Line In у микшера) и мощность // (внезапный скачок drive пошёл бы в усилитель). Stock: TTXSettings; const // Кривые EQ для заводских ESSB. Значения — УЗЛЫ ломаной АЧХ WDSP (eq.c), // а не «полосы»: между узлами интерполяция по дБ, а ЗА крайними узлами // ctfmode=0 обрывает АЧХ на нет. Отсюда железное правило, по которому // подобраны частоты: нижний узел ≤ FilterLow, верхний ≥ FilterHigh, иначе // включённый EQ сам срежет полосу, ради которой ESSB и включают. // Форма: срез бубнения снизу, ровная середина, мягкий подъём присутствия // к верху. Преамп в минус — EQ стоит ДО компрессора и ALC, и плюсовые // гейны иначе просто загоняют ALC в ограничение. ESSB35_F: array[1..10] of Double = (100, 160, 250, 400, 700, 1200, 1800, 2500, 3100, 3600); ESSB35_G: array[1..10] of Double = ( -6, -4, -2, 0, 0, 1, 2, 3, 4, 4); ESSB35_PRE = -2.0; ESSB50_F: array[1..10] of Double = (100, 180, 300, 550, 950, 1600, 2400, 3300, 4200, 5100); ESSB50_G: array[1..10] of Double = ( -5, -3, -1, 0, 0, 1, 2, 3.5, 5, 5); ESSB50_PRE = -3.0; // FM: узлы охватывают AF-полосу модулятора (штатно 300..3000, вкладка // Hardware), крайние с запасом — за ними EQ обрывает АЧХ. Верх НЕ поднимаем: // предыскажение (pre-emphasis) само задирает его 6 дБ/окт и по умолчанию // стоит ПОСЛЕ динамики (xemphp вторым, сразу перед xfmmod), то есть // компрессор его уже не удержит — лишний буст ушёл бы в перемодуляцию на // свистящих. Снизу режем бубнение, лёгкий подъём разборчивости 1.4..2.3 кГц. FM_F: array[1..10] of Double = (200, 300, 450, 700, 1000, 1400, 1800, 2300, 2800, 3400); FM_G: array[1..10] of Double = ( -8, -4, -1, 0, 0, 1, 2, 2, 1, -2); FM_PRE = -1.0; // Тембр у заводского профиля — заводской: EQ выключен, все усиления и // преамп в нуле, сетка узлов штатная. Наследовать сюда живой EQ нельзя — // иначе «сброс к заводским» не сбрасывал бы то, ради чего его жмут. procedure FlatEQ(var P: TTXProfile); var i: Integer; begin P.EQOn := False; P.EQNumBands := 10; for i := 0 to 10 do begin P.EQGains[i] := 0.0; P.EQFreqs[i] := DEF_EQ_FREQS[i]; end; end; procedure Add(const AName: string; ALow, AHigh: Integer; AComp: Boolean; ACompGain: Double); begin if L.Count >= TXPROF_MAX then Exit; TXProfileCapture(Stock, DrivePct, L.Items[L.Count]); L.Items[L.Count].Name := AName; L.Items[L.Count].FilterLow := ALow; L.Items[L.Count].FilterHigh := AHigh; L.Items[L.Count].CompressorOn := AComp; L.Items[L.Count].CompressorGain := ACompGain; FlatEQ(L.Items[L.Count]); Inc(L.Count); end; // То же плюс своя кривая EQ: у ESSB тембр — часть профиля, наследовать // сетку 100..3500 от текущих настроек здесь нельзя (см. правило выше). procedure AddEQ(const AName: string; ALow, AHigh: Integer; AComp: Boolean; ACompGain: Double; const AF, AG: array of Double; APreamp: Double); var i, Before: Integer; begin Before := L.Count; Add(AName, ALow, AHigh, AComp, ACompGain); if L.Count = Before then Exit; // упёрлись в TXPROF_MAX — ничего не добавлено with L.Items[L.Count - 1] do begin EQOn := True; EQNumBands := 10; EQFreqs[0] := 0.0; // слот преампа — частота не используется EQGains[0] := APreamp; for i := 1 to 10 do begin EQFreqs[i] := AF[i - 1]; EQGains[i] := AG[i - 1]; end; end; end; begin FillChar(L, SizeOf(L), 0); L.Count := 0; L.ActiveIdx := 0; ClearTXProfModeTable(L.MainByMode); // главный ничего не переключает // База всех заводских профилей: заводской звук, живая обвязка микрофона. TSettingsManager.DefaultTX(Stock); Stock.MicLineIn := Base.MicLineIn; Stock.MicBoost := Base.MicBoost; Stock.MicBias := Base.MicBias; Stock.MicPTTEnabled := Base.MicPTTEnabled; Stock.MicTipRing := Base.MicTipRing; Stock.LineInGainDB := Base.LineInGainDB; Stock.MicGainDB := Base.MicGainDB; // «Default» — ровно заводской звук: полоса 200..3100, компрессор выкл, // leveler/ALC как в DefaultTX, EQ ровный. TXProfileCapture(Stock, DrivePct, L.Items[0]); L.Items[0].Name := 'Default'; FlatEQ(L.Items[0]); L.Count := 1; // SSB-профили отличаются от Default только полосой и компрессором; // микрофонный вход и мощность наследуются, EQ у всех троих заводской-ровный. // ESSB несут ещё и свою кривую: без неё «широкий» профиль звучит тем же // телефонным тембром, а с чужой сеткой (верхний узел 3500) EQ обрезал бы // верх ESSB 5k. Add('SSB DX', 300, 2700, True, 8.0); Add('SSB Wide', 200, 3100, False, 5.0); AddEQ('ESSB 3.5k', 100, 3500, False, 5.0, ESSB35_F, ESSB35_G, ESSB35_PRE); AddEQ('ESSB 5k', 100, 5000, False, 5.0, ESSB50_F, ESSB50_G, ESSB50_PRE); // FM: плотная модуляция за счёт компрессора — девиация ограничена жёстко, // поэтому громкость даёт средний уровень, а не пики. Полоса 300..3000 здесь // косметическая: в FM края bp0 считает сам движок по Карсону (девиация + // AF-срез), профильные значения работают, только если привязать этот профиль // к голосовому режиму. Девиация и AF-срез — свойства аппарата (вкладка // Hardware), профилем не переключаются. AddEQ('FM', 300, 3000, True, 8.0, FM_F, FM_G, FM_PRE); end; function TSettingsManager.LoadTXProfiles(const MAC: array of Byte; out L: TTXProfileList): Boolean; var MacStr: string; DevObj, Root, O: TJSONObject; Node: TJSONData; Arr: TJSONArray; BaseTX: TTXSettings; DefP: TTXProfile; i, j: Integer; begin FillChar(L, SizeOf(L), 0); L.Count := 0; L.ActiveIdx := 0; ClearTXProfModeTable(L.MainByMode); Result := False; MacStr := MacToStr(MAC); if FRoot.Find(MacStr) = nil then Exit; DevObj := GetDevObj(MacStr); if DevObj.Find('tx_profiles') = nil then Exit; Root := EnsureObj(DevObj, 'tx_profiles'); Node := Root.Find('list'); if not (Node is TJSONArray) then Exit; Arr := TJSONArray(Node); // Заводской профиль как источник значений по умолчанию для отсутствующих // ключей: конфиг, записанный старой версией, дочитывается без дыр. DefaultTX(BaseTX); TXProfileCapture(BaseTX, 50, DefP); DefP.Name := 'Profile'; for i := 0 to Arr.Count - 1 do begin if L.Count >= TXPROF_MAX then Break; if not (Arr.Items[i] is TJSONObject) then Continue; O := TJSONObject(Arr.Items[i]); L.Items[L.Count] := DefP; with L.Items[L.Count] do begin Name := Copy(JS(O,'name', DefP.Name), 1, 31); MicLineIn := JB(O,'mic_line_in', MicLineIn); MicBoost := JB(O,'mic_boost', MicBoost); MicPTTEnabled := JB(O,'mic_ptt', MicPTTEnabled); MicTipRing := JB(O,'mic_tip_ring', MicTipRing); MicBias := JB(O,'mic_bias', MicBias); LineInGainDB := EnsureRange(JD(O,'line_in_gain_db', LineInGainDB), -34.5, 12.0); MicGainDB := JD(O,'mic_gain_db', MicGainDB); FilterLow := JI(O,'filter_low', FilterLow); FilterHigh := JI(O,'filter_high', FilterHigh); CompressorOn := JB(O,'comp_on', CompressorOn); CompressorGain := JD(O,'comp_gain', CompressorGain); LevelerOn := JB(O,'lev_on', LevelerOn); LevelerTop := JD(O,'lev_top', LevelerTop); LevelerDecay := JI(O,'lev_decay', LevelerDecay); ALCOn := JB(O,'alc_on', ALCOn); ALCMaxGain := JD(O,'alc_max_gain', ALCMaxGain); ALCDecay := JI(O,'alc_decay', ALCDecay); PhaseRotOn := JB(O,'phr_on', PhaseRotOn); PhaseRotStages := JI(O,'phr_stages', PhaseRotStages); PhaseRotFreq := JD(O,'phr_freq', PhaseRotFreq); EQOn := JB(O,'eq_on', EQOn); EQNumBands := JI(O,'eq_bands', EQNumBands); for j := 0 to 10 do begin EQGains[j] := JD(O,'eq_g_'+IntToStr(j), EQGains[j]); EQFreqs[j] := JD(O,'eq_f_'+IntToStr(j), EQFreqs[j]); end; AMCarrierLevel := JD(O,'am_carrier', AMCarrierLevel); DrivePercent := EnsureRange(JI(O,'drive', DrivePercent), 0, 100); TUNLevel := EnsureRange(JI(O,'tun_level', TUNLevel), 0, 100); if Name = '' then Name := 'Profile ' + IntToStr(L.Count + 1); end; Inc(L.Count); end; if L.Count = 0 then Exit; // пустой список = как будто секции нет L.ActiveIdx := JI(Root, 'active', 0); if (L.ActiveIdx < 0) or (L.ActiveIdx >= L.Count) then L.ActiveIdx := 0; JSONToModeTable(Root.Find('main_prof_modes'), L.MainByMode); for i := 0 to CFG_MODE_MAX do if L.MainByMode[i] >= L.Count then L.MainByMode[i] := -1; Result := True; end; procedure TSettingsManager.SaveTXProfiles(const MAC: array of Byte; const L: TTXProfileList); var Root, O: TJSONObject; Arr: TJSONArray; i, j, Idx: Integer; begin Root := EnsureObj(GetDevObj(MacToStr(MAC)), 'tx_profiles'); JW(Root, 'active', EnsureRange(L.ActiveIdx, 0, Max(0, L.Count - 1))); Idx := Root.IndexOfName('main_prof_modes'); if Idx >= 0 then Root.Delete(Idx); Root.Add('main_prof_modes', ModeTableToJSON(L.MainByMode)); // Список пересоздаётся целиком: удаление профиля не должно оставлять хвост. Idx := Root.IndexOfName('list'); if Idx >= 0 then Root.Delete(Idx); Arr := TJSONArray.Create; Root.Add('list', Arr); for i := 0 to L.Count - 1 do begin O := TJSONObject.Create; Arr.Add(O); JWS(O,'name', L.Items[i].Name); JW(O,'mic_line_in', L.Items[i].MicLineIn); JW(O,'mic_boost', L.Items[i].MicBoost); JW(O,'mic_ptt', L.Items[i].MicPTTEnabled); JW(O,'mic_tip_ring', L.Items[i].MicTipRing); JW(O,'mic_bias', L.Items[i].MicBias); JW(O,'line_in_gain_db', L.Items[i].LineInGainDB); JW(O,'mic_gain_db', L.Items[i].MicGainDB); JW(O,'filter_low', L.Items[i].FilterLow); JW(O,'filter_high', L.Items[i].FilterHigh); JW(O,'comp_on', L.Items[i].CompressorOn); JW(O,'comp_gain', L.Items[i].CompressorGain); JW(O,'lev_on', L.Items[i].LevelerOn); JW(O,'lev_top', L.Items[i].LevelerTop); JW(O,'lev_decay', L.Items[i].LevelerDecay); JW(O,'alc_on', L.Items[i].ALCOn); JW(O,'alc_max_gain', L.Items[i].ALCMaxGain); JW(O,'alc_decay', L.Items[i].ALCDecay); JW(O,'phr_on', L.Items[i].PhaseRotOn); JW(O,'phr_stages', L.Items[i].PhaseRotStages); JW(O,'phr_freq', L.Items[i].PhaseRotFreq); JW(O,'eq_on', L.Items[i].EQOn); JW(O,'eq_bands', L.Items[i].EQNumBands); for j := 0 to 10 do begin JW(O,'eq_g_'+IntToStr(j), L.Items[i].EQGains[j]); JW(O,'eq_f_'+IntToStr(j), L.Items[i].EQFreqs[j]); end; JW(O,'am_carrier', L.Items[i].AMCarrierLevel); JW(O,'drive', L.Items[i].DrivePercent); JW(O,'tun_level', L.Items[i].TUNLevel); end; 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); // Верхняя граница = FILT_SLOTS-1, а не 9: слоты VAR1/VAR2 (10/11) иначе // схлопывались в 9, и при восстановлении трансвертера ClampFilterIdx // видел индекс вне таблицы режима и откатывал на заводской фильтр. X.Entries[i].LastFilterIdx := EnsureRange(JI(EObj, 'last_filt', X.Entries[i].LastFilterIdx), 0, FILT_SLOTS - 1); 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.DefaultDXCluster(out D: TDXClusterSettings); begin D.Enabled := False; // без позывного подключаться всё равно нечем D.ShowSpots := False; // подписи включает пользователь кнопкой DX D.Host := 'cluster.dxfun.com'; D.Port := 8000; D.Login := ''; D.Password := ''; D.PostLogin := ''; D.TTLMinutes := 30; D.MaxRows := 4; D.ModeMask := 0; // 0 = без фильтра по модам end; procedure TSettingsManager.LoadDXClusterSettings(out D: TDXClusterSettings); var O: TJSONObject; begin DefaultDXCluster(D); if FRoot.Find('dxcluster') = nil then Exit; O := EnsureObj(FRoot, 'dxcluster'); D.Enabled := JB(O, 'enabled', False); D.ShowSpots := JB(O, 'show_spots', False); D.Host := JS(O, 'host', 'cluster.dxfun.com'); D.Port := EnsureRange(JI(O, 'port', 8000), 1, 65535); D.Login := JS(O, 'login', ''); D.Password := JS(O, 'password', ''); D.PostLogin := JS(O, 'post_login', ''); D.TTLMinutes := EnsureRange(JI(O, 'ttl_minutes', 30), 1, 1440); D.MaxRows := EnsureRange(JI(O, 'max_rows', 4), 1, 6); D.ModeMask := LongWord(JI(O, 'mode_mask', 0)); end; procedure TSettingsManager.SaveDXClusterSettings(const D: TDXClusterSettings); var O: TJSONObject; begin O := EnsureObj(FRoot, 'dxcluster'); JW(O, 'enabled', D.Enabled); JW(O, 'show_spots', D.ShowSpots); JWS(O, 'host', D.Host); JW(O, 'port', D.Port); JWS(O, 'login', D.Login); JWS(O, 'password', D.Password); JWS(O, 'post_login', D.PostLogin); JW(O, 'ttl_minutes', D.TTLMinutes); JW(O, 'max_rows', D.MaxRows); JW(O, 'mode_mask', Integer(D.ModeMask)); Save; 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; class procedure TSettingsManager.DefaultTCI(out T: TTCISettings); begin T.Enabled := False; // включается осознанно, как CAT-транспорты T.Port := 40001; T.BindAddr := '127.0.0.1'; T.RecordDir := ''; // пусто = <каталог конфигурации>/records end; procedure TSettingsManager.LoadTCISettings(out T: TTCISettings); var O: TJSONObject; begin DefaultTCI(T); if FRoot.Find('tci') = nil then Exit; O := EnsureObj(FRoot, 'tci'); T.Enabled := JB(O, 'enabled', False); T.Port := EnsureRange(JI(O, 'port', 40001), 1, 65535); T.BindAddr := JS(O, 'bind_addr', '127.0.0.1'); T.RecordDir := JS(O, 'record_dir', ''); end; procedure TSettingsManager.SaveTCISettings(const T: TTCISettings); var O: TJSONObject; begin O := EnsureObj(FRoot, 'tci'); JW(O, 'enabled', T.Enabled); JW(O, 'port', T.Port); JWS(O, 'bind_addr', T.BindAddr); JWS(O, 'record_dir', T.RecordDir); 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); SliceObj.Add('tx_prof_modes', ModeTableToJSON(P.Pans[i].Slices[j].TXProfByMode)); 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', ''); JSONToModeTable(SliceObj.Find('tx_prof_modes'), P.Pans[id].Slices[n].TXProfByMode); end; end; end; end.