mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:27:33 +00:00
CW не работал ни на приём, ни на передачу: фильтр стоял симметрично вокруг нуля (тона не было), понятия pitch не существовало, MOX в CWL отправлял в эфир голос 2.9 кГц (в WDSP TXA_CWL — ветка SSB), байт CW-опций DUC всегда был нулевым, поэтому кейер, сайдтон и break-in в прошивке молчали. Pitch сделан ОДНИМ сдвигом в движке. Кромки фильтра везде остаются относительно VFO (у CW — симметрично нулю), а гетеродин и полосу двигают три метода WDSPEngine: CWLOOffset / PushShift / PushPassband. Прямых вызовов SetRXAShiftFreq и RXASetPassband в проекте больше нет. Так таблица фильтров не перестраивается при смене pitch (в отличие от Thetis), полоска на спектре и маркер VFO верны без правок в UI, а слайсы получают свой сдвиг по своему режиму. Правила эфира: - голосовой TXA в CW не запускается вообще (гард по режиму), MOX = только PTT; несущую даёт кейер, бит CWX или тон TUN; - байт 5 собирается из настроек и перепосылается на каждой смене режима и TX-слайса — вне CW он обязан быть нулевым, иначе прошивка поднимет PTT на замыкание ключа посреди SSB; - TUN в CW: тон = pitch и встречный сдвиг DUC, несущая встаёт ровно на VFO; - приёмник на передаче в CW не глушится, иначе умолкает программный сайдтон и эфир между посылками. Программная передача текста (CWMorse): поток с абсолютными дедлайнами дёргает бит CWX в High Priority, элементы рисует прошивка. Касание манипулятора или снятие MOX обрывают передачу. Память сообщений — окно CW Messages и F1..F8 в главном окне. Оживлены CAT KS/KY/ZZKM/ZZKS/ZZKY. Настройки — вкладка Transmit, подвкладка Hardware, перед блоком FM/CTCSS. Программный сайдтон точен для передачи текста и прямого ключа; при иамбике таймингом владеет FPGA, поэтому там верен только аппаратный тон. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3032 lines
145 KiB
ObjectPascal
3032 lines
145 KiB
ObjectPascal
unit Settings;
|
||
{
|
||
Settings.pas — JSON-конфигурация HPSDR SDR приложения.
|
||
Структура hpsdr_settings.json:
|
||
{ "AA:BB:CC:DD:EE:FF": { "global": {...}, "bands": {"5": {...}} } }
|
||
}
|
||
{$mode objfpc}{$H+}
|
||
interface
|
||
uses SysUtils, Classes, Math, fpJSON, jsonparser, jsonscanner, PlatformUtils,
|
||
RadioModes;
|
||
|
||
const
|
||
SETTINGS_FILE = 'hpsdr_settings.json';
|
||
CFG_BAND_COUNT = 12;
|
||
CFG_XVTR_COUNT = 8; // количество XVTR-слотов (трансверторов)
|
||
OC_PIN_COUNT = 7; // количество Open Collector выходов (Penny/Alex)
|
||
// TX pin action (byte-константы, как TXPinActions в Thetis Penny.cs):
|
||
// при каком состоянии передачи пин активен.
|
||
OCA_MOX = 0; // только обычный TX (не TUNE, не 2TON)
|
||
OCA_TUNE = 1; // только TUNE
|
||
OCA_TWOTONE = 2; // только 2TON
|
||
OCA_MOX_TUNE = 3; // TX или TUNE
|
||
OCA_MOX_TWOTONE = 4; // TX или 2TON
|
||
OCA_TUNE_TWOTONE = 5; // TUNE или 2TON
|
||
OCA_MOX_TUNE_TWOTONE = 6; // любое из трёх (дефолт)
|
||
CFG_MODE_MIN = RadioModes.MODE_MIN;
|
||
CFG_MODE_MAX = RadioModes.MODE_MAX;
|
||
CFG_PANS_MAX = 4; // потолок панадаптеров (пан 0 + доп.) — = WDSPEngine.MAX_PANS
|
||
CFG_CAT_SLICE_COUNT = 6; // слотов доп. слайсов (B..G) — = WDSPEngine.MAX_SLICES
|
||
CFG_CAT_SLICE_PORT0 = 19091; // порт слайса B по умолчанию (главный — 19090)
|
||
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];
|
||
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 % от полной шкалы аудио
|
||
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").
|
||
TWebSettings = record
|
||
Enabled: Boolean;
|
||
Port: Integer; // 1..65535
|
||
BindAddr: string; // e.g. '0.0.0.0' (все интерфейсы)
|
||
User: string;
|
||
Pass: string;
|
||
// Потолок точек в кадре спектра/водопада для веб-клиента (1024/2048/4096).
|
||
// Реально уходит min(этот потолок, разрешение анализатора = ширина
|
||
// панадаптера). Трафик линеен: 4096 точек — это ~650 КБ/с на клиента
|
||
// (два потока по 20 кадров/с), 1024 — ~160 КБ/с.
|
||
SpecPixels: Integer;
|
||
end;
|
||
|
||
// Transverter (XVTR) entry — один трансвертер.
|
||
// Видимая частота VFO в [FreqBegin..FreqEnd] транслируется в IF-частоту,
|
||
// которую слышит трансивер: f_IF = f_visible - LOOffset + LOError.
|
||
// Примеры: 2m XVTR с LO=116 MHz → visible 144.200 ⇒ IF 28.200 MHz.
|
||
TXvtrEntry = record
|
||
Enabled: Boolean;
|
||
ButtonText: string[15]; // подпись на band-кнопке (например, '2m', '70cm')
|
||
FreqBegin: Double; // Hz
|
||
FreqEnd: Double; // Hz
|
||
LOOffset: Double; // Hz, отнимается от видимой частоты для получения IF (RX)
|
||
LOError: Double; // Hz, корректировка LO (положительная — IF растёт) — RX/LNB дрейф
|
||
// QO-100 / split-LO транспондер: RX и TX идут на РАЗНЫХ LO. RX = visible-LOOffset
|
||
// (+LOError, дрейф LNB по маяку). TX = visible-TxLOOffset (без LOError — uplink-LO
|
||
// собственный у SDR). FullDuplexTx помечает слот как такой транспондер.
|
||
FullDuplexTx: Boolean; // True — раздельный TX-LO (QO-100); авто display-duplex
|
||
TxLOOffset: Double; // Hz, TX LO-offset (для QO-100 = транспондер-offset, ~8089.5 МГц)
|
||
// Hz, поправка к TX LO-offset (TO Error). Сам offset держим стандартным
|
||
// (8089.5 МГц у QO-100), а уход конкретного тракта правим здесь.
|
||
// Знак как у LOError: положительная — TX IF растёт.
|
||
TxLOError: Double;
|
||
RXGain: Double; // dB, компенсация усиления RX тракта
|
||
RXOnly: Boolean; // True — запрещает TX на этом трансверторе
|
||
TXPower: Integer; // 0..100 % (drive level специально для XVTR)
|
||
DisablePA: Boolean; // True — отключить внутренний PA при TX (XVTR имеет свой)
|
||
RXAntenna: Byte; // 0=по-умолчанию(основной band), 1=ANT1, 2=ANT2, 3=ANT3
|
||
LastFreq: Double; // Hz — последняя tuned visible частота VFO A (BandStack)
|
||
LastFreqB: Double; // Hz — последняя tuned visible частота VFO B
|
||
LastMode: Integer; // Mode (LSB=0,USB=1,…FM=5)
|
||
LastFilterIdx: Integer; // слот фильтра 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
|
||
// --- 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 DefaultWeb(out W: TWebSettings);
|
||
procedure LoadWebSettings(out W: TWebSettings);
|
||
procedure SaveWebSettings(const W: TWebSettings);
|
||
// Тема — глобальная настройка, не привязана к устройству
|
||
procedure SaveTheme(ALightTheme: Boolean);
|
||
function LoadTheme: Boolean;
|
||
// Таблица фильтров per-device (JSON-секция "filters" под MAC). Пишутся
|
||
// только режимы, отличающиеся от заводских, — конфиг не пухнет.
|
||
function LoadFilters(const MAC: array of Byte; out T: TFilterTable): Boolean;
|
||
procedure SaveFilters(const MAC: array of Byte; const T: TFilterTable);
|
||
// 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.AudioOutDevice := '';
|
||
G.AudioSampleRate := 48000;
|
||
G.AudioInDevice := '';
|
||
G.SendAudioToRadio := False;
|
||
G.RawFMDeviation := RAW_FM_DEV_DEFAULT;
|
||
G.ShowSpectrum := True;
|
||
G.ShowWaterfall := True;
|
||
G.ShowWideband := False;
|
||
G.WidebandFill := False;
|
||
G.SpectrumFill := True;
|
||
G.DisplayDuplex := False;
|
||
G.RxMuteOnTx := True;
|
||
G.DisplayFPS := 60;
|
||
G.FreqMhzDigits := 3;
|
||
// CAT defaults
|
||
for i := 0 to 3 do begin
|
||
G.CATSerialEnabled[i] := False;
|
||
{$IFDEF MSWINDOWS}
|
||
G.CATSerialPort[i] := 'COM' + IntToStr(i+1);
|
||
{$ELSE}
|
||
G.CATSerialPort[i] := '/dev/ttyS' + IntToStr(i);
|
||
{$ENDIF}
|
||
G.CATSerialBaud[i] := 9600;
|
||
G.CATSerialDataBits[i] := 8;
|
||
G.CATSerialStopBits[i] := 1;
|
||
G.CATSerialParity[i] := 0; // None
|
||
G.CATSerialAndromeda[i]:= False;
|
||
end;
|
||
G.CATTcpEnabled := False;
|
||
G.CATTcpPort := 19090;
|
||
for i := 0 to CFG_CAT_SLICE_COUNT-1 do begin
|
||
G.CATSliceEnabled[i] := False;
|
||
G.CATSlicePort[i] := CFG_CAT_SLICE_PORT0 + i;
|
||
G.CATSliceAutoTX[i] := False;
|
||
G.CATSliceOutDev[i] := '';
|
||
G.CATSliceInDev[i] := '';
|
||
end;
|
||
// ADC — включены по умолчанию: поддерживаются на Hermes, Angelia, Orion
|
||
G.DitherEnabled := True;
|
||
G.RandomEnabled := True;
|
||
DefaultCalibration(G.Cal);
|
||
end;
|
||
|
||
class procedure TSettingsManager.DefaultCalibration(out C: TCalibration);
|
||
var i: Integer;
|
||
begin
|
||
FillChar(C, SizeOf(C), 0);
|
||
C.PwrDetModel := 0; // ANAN-10/100 — совпадает с прежней зашитой формулой
|
||
for i := 0 to CFG_BAND_COUNT-1 do C.PADetCal[i] := 1.0;
|
||
C.SupplyVoltCal := 1.0;
|
||
C.AmpVoffMV := 360.0;
|
||
C.AmpSensMVA := 120.0;
|
||
// SMeterBandDB/SMeterXvtrDB — нули после FillChar (без коррекции).
|
||
end;
|
||
|
||
constructor TSettingsManager.Create(const FilePath: string);
|
||
begin
|
||
inherited Create;
|
||
if FilePath = '' then
|
||
FFilePath := GetAppCfgDir + SETTINGS_FILE
|
||
else
|
||
FFilePath := FilePath;
|
||
FRoot := TJSONObject.Create;
|
||
end;
|
||
|
||
destructor TSettingsManager.Destroy;
|
||
begin
|
||
FRoot.Free;
|
||
inherited;
|
||
end;
|
||
|
||
procedure TSettingsManager.Load;
|
||
var F: TFileStream; P: TJSONParser; D: TJSONData;
|
||
begin
|
||
if not FileExists(FFilePath) then Exit;
|
||
try
|
||
F := TFileStream.Create(FFilePath, fmOpenRead or fmShareDenyNone);
|
||
try
|
||
P := TJSONParser.Create(F, [joUTF8]);
|
||
try
|
||
D := P.Parse;
|
||
if (D <> nil) and (D is TJSONObject) then
|
||
begin FRoot.Free; FRoot := TJSONObject(D); end
|
||
else
|
||
D.Free;
|
||
finally P.Free; end;
|
||
finally F.Free; end;
|
||
except
|
||
FreeAndNil(FRoot);
|
||
FRoot := TJSONObject.Create;
|
||
end;
|
||
end;
|
||
|
||
procedure TSettingsManager.Save;
|
||
var
|
||
S: string;
|
||
F: TFileStream;
|
||
Buf: TBytes;
|
||
begin
|
||
try
|
||
S := FRoot.FormatJSON([], 2);
|
||
Buf := TEncoding.UTF8.GetBytes(S);
|
||
F := TFileStream.Create(FFilePath, fmCreate);
|
||
try
|
||
if Length(Buf) > 0 then
|
||
F.WriteBuffer(Buf[0], Length(Buf));
|
||
finally F.Free; end;
|
||
except end;
|
||
end;
|
||
|
||
function TSettingsManager.EnsureObj(P: TJSONObject; const K: string): TJSONObject;
|
||
var D: TJSONData; Idx: Integer;
|
||
begin
|
||
D := P.Find(K);
|
||
if (D <> nil) and (D is TJSONObject) then
|
||
Result := TJSONObject(D)
|
||
else
|
||
begin
|
||
if D <> nil then begin Idx := P.IndexOfName(K); if Idx >= 0 then P.Delete(Idx); end;
|
||
Result := TJSONObject.Create;
|
||
P.Add(K, Result);
|
||
end;
|
||
end;
|
||
|
||
function TSettingsManager.GetDevObj(const M: string): TJSONObject;
|
||
begin Result := EnsureObj(FRoot, M); end;
|
||
|
||
function TSettingsManager.GetGlobalObj(D: TJSONObject): TJSONObject;
|
||
begin Result := EnsureObj(D, 'global'); end;
|
||
|
||
function TSettingsManager.GetBandObj(D: TJSONObject; Idx: Integer): TJSONObject;
|
||
begin Result := EnsureObj(EnsureObj(D, 'bands'), IntToStr(Idx)); end;
|
||
|
||
function TSettingsManager.JI(O: TJSONObject; const K: string; Def: Integer): Integer;
|
||
var D: TJSONData;
|
||
begin D := O.Find(K); if D<>nil then try Result:=D.AsInteger; except Result:=Def; end else Result:=Def; end;
|
||
|
||
function TSettingsManager.JD(O: TJSONObject; const K: string; Def: Double): Double;
|
||
var D: TJSONData;
|
||
begin D := O.Find(K); if D<>nil then try Result:=D.AsFloat; except Result:=Def; end else Result:=Def; end;
|
||
|
||
function TSettingsManager.JB(O: TJSONObject; const K: string; Def: Boolean): Boolean;
|
||
var D: TJSONData;
|
||
begin D := O.Find(K); if D<>nil then try Result:=D.AsBoolean; except Result:=Def; end else Result:=Def; end;
|
||
|
||
procedure TSettingsManager.JW(O: TJSONObject; const K: string; V: Integer);
|
||
var Idx: Integer;
|
||
begin
|
||
Idx := O.IndexOfName(K); if Idx >= 0 then O.Delete(Idx);
|
||
O.Add(K, V);
|
||
end;
|
||
|
||
procedure TSettingsManager.JW(O: TJSONObject; const K: string; V: Double);
|
||
var Idx: Integer;
|
||
begin
|
||
Idx := O.IndexOfName(K); if Idx >= 0 then O.Delete(Idx);
|
||
O.Add(K, V);
|
||
end;
|
||
|
||
procedure TSettingsManager.JW(O: TJSONObject; const K: string; V: Boolean);
|
||
var Idx: Integer;
|
||
begin
|
||
Idx := O.IndexOfName(K); if Idx >= 0 then O.Delete(Idx);
|
||
O.Add(K, V);
|
||
end;
|
||
|
||
function TSettingsManager.JS(O: TJSONObject; const K: string; const Def: string): string;
|
||
var D: TJSONData;
|
||
begin D := O.Find(K); if D<>nil then try Result:=D.AsString; except Result:=Def; end else Result:=Def; end;
|
||
|
||
procedure TSettingsManager.JWS(O: TJSONObject; const K: string; const V: string);
|
||
var Idx: Integer;
|
||
begin
|
||
Idx := O.IndexOfName(K); if Idx >= 0 then O.Delete(Idx);
|
||
O.Add(K, V);
|
||
end;
|
||
|
||
function TSettingsManager.LoadDevice(const MAC: array of Byte;
|
||
out G: TGlobalSettings;
|
||
var Bands: array of TBandSettings): Boolean;
|
||
var MacStr: string; DevObj,GObj,BObj: TJSONObject; i: Integer; SMLegacy: Double;
|
||
begin
|
||
MacStr := MacToStr(MAC);
|
||
Result := FRoot.Find(MacStr) <> nil;
|
||
DevObj := GetDevObj(MacStr);
|
||
GObj := GetGlobalObj(DevObj);
|
||
|
||
G.Volume := JI(GObj,'volume',70);
|
||
G.TxMonVolume := JI(GObj,'tx_mon_volume',40);
|
||
G.DriveLevel := JI(GObj,'drive_level',50);
|
||
G.ActiveVfo := JI(GObj,'active_vfo',0);
|
||
G.NRMode := JI(GObj,'nr_mode',Ord(JB(GObj,'nr_enabled',False)));
|
||
G.NBMode := JI(GObj,'nb_mode',Ord(JB(GObj,'nb_enabled',False)));
|
||
G.SNBEnabled := JB(GObj,'snb_enabled',False);
|
||
G.ANFEnabled := JB(GObj,'anf_enabled',False);
|
||
G.AGCSlope := JI(GObj,'agc_slope',0);
|
||
G.AGCHangThreshold := JI(GObj,'agc_hang_threshold',100);
|
||
G.WfAGCEnabled := JB(GObj,'wf_agc_enabled',True);
|
||
G.WfNFEnabled := JB(GObj,'wf_nf_enabled',False);
|
||
G.LastBand := JI(GObj,'last_band',5);
|
||
G.LastXvtr := JI(GObj,'last_xvtr',-1);
|
||
G.SampleRate := JI(GObj,'sample_rate',192000);
|
||
G.RxGainMode := JI(GObj,'rx_gain_mode',2);
|
||
G.RxGainDb := JI(GObj,'rx_gain_db',40);
|
||
// Display settings
|
||
G.FFTSize := JI(GObj,'fft_size',131072);
|
||
G.WindowType := JI(GObj,'win_type',2);
|
||
G.SpecDetector := JI(GObj,'spec_detector',0);
|
||
G.SpecAvgMode := JI(GObj,'spec_avg_mode',3);
|
||
if GObj.Find('spec_avg_time_ms') <> nil then
|
||
G.SpecAvgTimeMS := JD(GObj,'spec_avg_time_ms',30.0)
|
||
else if (GObj.Find('spec_avg_count') <> nil) or
|
||
(GObj.Find('spec_backmult') <> nil) then
|
||
begin
|
||
G.SpecAvgTimeMS := LegacyAverageTimeMS(
|
||
JI(GObj,'display_fps',60),
|
||
G.SpecAvgMode,
|
||
JI(GObj,'spec_avg_count',1),
|
||
JD(GObj,'spec_backmult',0.0),
|
||
30.0);
|
||
end
|
||
else
|
||
G.SpecAvgTimeMS := 30.0;
|
||
G.WfDetector := JI(GObj,'wf_detector',0);
|
||
G.WfAvgMode := JI(GObj,'wf_avg_mode',3);
|
||
if GObj.Find('wf_avg_time_ms') <> nil then
|
||
G.WfAvgTimeMS := JD(GObj,'wf_avg_time_ms',120.0)
|
||
else if (GObj.Find('wf_avg_count') <> nil) or
|
||
(GObj.Find('wf_backmult') <> nil) then
|
||
begin
|
||
G.WfAvgTimeMS := LegacyAverageTimeMS(
|
||
JI(GObj,'display_fps',60),
|
||
G.WfAvgMode,
|
||
JI(GObj,'wf_avg_count',2),
|
||
JD(GObj,'wf_backmult',0.25),
|
||
120.0);
|
||
end
|
||
else
|
||
G.WfAvgTimeMS := 120.0;
|
||
G.WfManualHigh := JD(GObj,'wf_manual_high',-80.0);
|
||
G.WfManualLow := JD(GObj,'wf_manual_low',-130.0);
|
||
G.WfAGCOffset := JD(GObj,'wf_agc_offset',0.0);
|
||
G.WfPalette := JI(GObj,'wf_palette',0);
|
||
G.WfGamma := JD(GObj,'wf_gamma',1.0);
|
||
G.DisplayPixels := JI(GObj,'display_pixels',4096);
|
||
G.WfSpeedDiv := JI(GObj,'wf_speed_div',1);
|
||
G.SpecMSAA := JI(GObj,'spec_msaa',4);
|
||
G.SpecRefLevel := JD(GObj,'spec_ref_level',-20.0);
|
||
G.SpecRange := JD(GObj,'spec_range',110.0);
|
||
G.SpecGridStep := JD(GObj,'spec_grid_step',10.0);
|
||
// Audio settings
|
||
G.AudioOutDevice := JS(GObj,'audio_out_device','');
|
||
G.AudioSampleRate := JI(GObj,'audio_sample_rate',48000);
|
||
G.AudioInDevice := JS(GObj,'audio_in_device','');
|
||
G.SendAudioToRadio := JB(GObj,'send_audio_to_radio',False);
|
||
G.RawFMDeviation := EnsureRange(
|
||
JI(GObj,'raw_fm_deviation',RAW_FM_DEV_DEFAULT),
|
||
RAW_FM_DEV_MIN, RAW_FM_DEV_MAX);
|
||
// Visibility settings
|
||
G.ShowSpectrum := JB(GObj,'show_spectrum',True);
|
||
G.ShowWaterfall := JB(GObj,'show_waterfall',True);
|
||
G.ShowWideband := JB(GObj,'show_wideband',False);
|
||
G.WidebandFill := JB(GObj,'wideband_fill',False);
|
||
G.SpectrumFill := JB(GObj,'spectrum_fill',True);
|
||
G.DisplayDuplex := JB(GObj,'display_duplex',False);
|
||
G.RxMuteOnTx := JB(GObj,'rx_mute_on_tx',True);
|
||
G.DisplayFPS := JI(GObj,'display_fps',60);
|
||
G.LightTheme := JB(GObj,'light_theme',False);
|
||
G.FreqMhzDigits := EnsureRange(JI(GObj,'freq_mhz_digits',3), 3, 5);
|
||
// PA settings
|
||
G.PAMaxPower := JD(GObj,'pa_max_power',100.0);
|
||
G.PlutoTxMaxAttDb := JD(GObj,'pluto_tx_max_att',-10.0);
|
||
for i := 0 to CFG_BAND_COUNT-1 do
|
||
G.PABandCal[i] := JD(GObj,'pa_band_cal_'+IntToStr(i), CFG_PA_BAND_CAL_DEFAULT[i]);
|
||
for i := 0 to CFG_XVTR_COUNT-1 do
|
||
G.VHFBandCal[i] := JD(GObj,'vhf_cal_'+IntToStr(i), CFG_VHF_CAL_DEFAULT);
|
||
// CAT serial settings
|
||
for i := 0 to 3 do begin
|
||
G.CATSerialEnabled[i] := JB(GObj,'cat_serial_en_'+IntToStr(i), False);
|
||
{$IFDEF MSWINDOWS}
|
||
G.CATSerialPort[i] := JS(GObj,'cat_serial_port_'+IntToStr(i), 'COM'+IntToStr(i+1));
|
||
{$ELSE}
|
||
G.CATSerialPort[i] := JS(GObj,'cat_serial_port_'+IntToStr(i), '/dev/ttyS'+IntToStr(i));
|
||
{$ENDIF}
|
||
G.CATSerialBaud[i] := JI(GObj,'cat_serial_baud_'+IntToStr(i), 9600);
|
||
G.CATSerialDataBits[i] := JI(GObj,'cat_serial_dbits_'+IntToStr(i), 8);
|
||
G.CATSerialStopBits[i] := JI(GObj,'cat_serial_sbits_'+IntToStr(i), 1);
|
||
G.CATSerialParity[i] := JI(GObj,'cat_serial_parity_'+IntToStr(i), 0);
|
||
G.CATSerialAndromeda[i]:= JB(GObj,'cat_serial_androm_'+IntToStr(i), False);
|
||
end;
|
||
for i := 0 to CFG_CAT_SLICE_COUNT-1 do begin
|
||
G.CATSliceEnabled[i] := JB(GObj,'cat_slice_en_' +IntToStr(i), False);
|
||
G.CATSlicePort[i] := JI(GObj,'cat_slice_port_' +IntToStr(i), CFG_CAT_SLICE_PORT0 + i);
|
||
G.CATSliceAutoTX[i] := JB(GObj,'cat_slice_autotx_'+IntToStr(i), False);
|
||
G.CATSliceOutDev[i] := JS(GObj,'cat_slice_out_' +IntToStr(i), '');
|
||
G.CATSliceInDev[i] := JS(GObj,'cat_slice_in_' +IntToStr(i), '');
|
||
end;
|
||
G.CATTcpEnabled := JB(GObj,'cat_tcp_enabled', False);
|
||
G.CATTcpPort := JI(GObj,'cat_tcp_port', 19090);
|
||
// ADC settings
|
||
G.DitherEnabled := JB(GObj,'dither_enabled', True);
|
||
G.RandomEnabled := JB(GObj,'random_enabled', True);
|
||
// Calibration (детектор мощности/КСВ, напряжение/ток, S-метр)
|
||
G.Cal.PwrDetModel := EnsureRange(JI(GObj,'cal_pwr_det_model', 0), 0, 3);
|
||
for i := 0 to CFG_BAND_COUNT-1 do
|
||
G.Cal.PADetCal[i] := EnsureRange(JD(GObj,'cal_pa_det_'+IntToStr(i), 1.0), 0.5, 2.0);
|
||
G.Cal.SupplyVoltCal := EnsureRange(JD(GObj,'cal_supply_volt', 1.0), 0.1, 5.0);
|
||
G.Cal.AmpVoffMV := EnsureRange(JD(GObj,'cal_amp_voff_mv', 360.0), 0.0, 2000.0);
|
||
G.Cal.AmpSensMVA := EnsureRange(JD(GObj,'cal_amp_sens_mva', 120.0), 1.0, 1000.0);
|
||
// S-метр: офсет на диапазон/трансвертер. Легаси-ключ (одно значение на всё
|
||
// устройство) становится дефолтом всех слотов — показания не меняются.
|
||
SMLegacy := EnsureRange(JD(GObj,'cal_smeter_db', 0.0), -30.0, 30.0);
|
||
for i := 0 to CFG_BAND_COUNT-1 do
|
||
G.Cal.SMeterBandDB[i] :=
|
||
EnsureRange(JD(GObj,'cal_smeter_band_'+IntToStr(i), SMLegacy), -30.0, 30.0);
|
||
for i := 0 to CFG_XVTR_COUNT-1 do
|
||
G.Cal.SMeterXvtrDB[i] :=
|
||
EnsureRange(JD(GObj,'cal_smeter_xvtr_'+IntToStr(i), SMLegacy), -30.0, 30.0);
|
||
// Спектр/водопад: своя таблица офсетов (дефолт 0 — как было).
|
||
for i := 0 to CFG_BAND_COUNT-1 do
|
||
G.Cal.DispBandDB[i] :=
|
||
EnsureRange(JD(GObj,'cal_disp_band_'+IntToStr(i), 0.0), -30.0, 30.0);
|
||
for i := 0 to CFG_XVTR_COUNT-1 do
|
||
G.Cal.DispXvtrDB[i] :=
|
||
EnsureRange(JD(GObj,'cal_disp_xvtr_'+IntToStr(i), 0.0), -30.0, 30.0);
|
||
G.Cal.FreqCalPPM := EnsureRange(JD(GObj,'cal_freq_ppm', 0.0), -100.0, 100.0);
|
||
|
||
for i := 0 to CFG_BAND_COUNT-1 do
|
||
begin
|
||
DefaultBand(i, Bands[i]);
|
||
BObj := GetBandObj(DevObj, i);
|
||
Bands[i].VfoA := JD(BObj,'vfo_a', Bands[i].VfoA);
|
||
Bands[i].VfoB := JD(BObj,'vfo_b', Bands[i].VfoB);
|
||
Bands[i].Mode := EnsureRange(JI(BObj,'mode', Bands[i].Mode), CFG_MODE_MIN, CFG_MODE_MAX);
|
||
Bands[i].FilterIdx := JI(BObj,'filter_idx', Bands[i].FilterIdx);
|
||
Bands[i].FilterBW := JI(BObj,'filter_bw', Bands[i].FilterBW);
|
||
Bands[i].AGCMode := JI(BObj,'agc_mode', Bands[i].AGCMode);
|
||
Bands[i].AGCTop := JI(BObj,'agc_top', Bands[i].AGCTop);
|
||
Bands[i].CTun := JB(BObj,'ctun', Bands[i].CTun);
|
||
Bands[i].CenterHz := JD(BObj,'center_hz', Bands[i].CenterHz);
|
||
Bands[i].SpanHz := JD(BObj,'span_hz', Bands[i].SpanHz);
|
||
// FM squelch / CTCSS / step / repeater — как в LoadBand (раньше пропускались
|
||
// здесь → на старте сбрасывались в дефолт, хотя SaveBand их пишет).
|
||
Bands[i].FMSQOn := JB(BObj,'fmsq_on', Bands[i].FMSQOn);
|
||
Bands[i].FMSQLevel := JI(BObj,'fmsq_level', Bands[i].FMSQLevel);
|
||
Bands[i].CTCSSOn := JB(BObj,'ctcss_on', Bands[i].CTCSSOn);
|
||
Bands[i].CTCSSToneIdx := EnsureRange(JI(BObj,'ctcss_idx', Bands[i].CTCSSToneIdx), 0, 37);
|
||
Bands[i].FMStepOn := JB(BObj,'fmstep_on', Bands[i].FMStepOn);
|
||
Bands[i].FMStepIdx := EnsureRange(JI(BObj,'fmstep_idx', Bands[i].FMStepIdx), 0, 3);
|
||
Bands[i].FMRptDir := EnsureRange(JI(BObj,'fmrpt_dir', Bands[i].FMRptDir), 0, 2);
|
||
Bands[i].FMRptOffsetHz := JD(BObj,'fmrpt_offset', Bands[i].FMRptOffsetHz);
|
||
Bands[i].RxGainMode := EnsureRange(JI(BObj,'rx_gain_mode', Bands[i].RxGainMode), 0, 3);
|
||
Bands[i].RxGainDb := EnsureRange(JI(BObj,'rx_gain_db', Bands[i].RxGainDb), 0, 73);
|
||
Bands[i].AttenDB := EnsureRange(JI(BObj,'atten_db', Bands[i].AttenDB), 0, 31);
|
||
end;
|
||
end;
|
||
|
||
procedure TSettingsManager.SaveGlobal(const MAC: array of Byte;
|
||
const G: TGlobalSettings);
|
||
var O: TJSONObject; i: Integer;
|
||
begin
|
||
O := GetGlobalObj(GetDevObj(MacToStr(MAC)));
|
||
JW(O,'volume',G.Volume); JW(O,'tx_mon_volume',G.TxMonVolume); JW(O,'drive_level',G.DriveLevel);
|
||
JW(O,'active_vfo',G.ActiveVfo);
|
||
JW(O,'nr_mode',G.NRMode); JW(O,'nb_mode',G.NBMode);
|
||
JW(O,'nr_enabled',G.NRMode <> 0); JW(O,'nb_enabled',G.NBMode <> 0);
|
||
JW(O,'snb_enabled',G.SNBEnabled);
|
||
JW(O,'anf_enabled',G.ANFEnabled);
|
||
JW(O,'agc_slope',G.AGCSlope);
|
||
JW(O,'agc_hang_threshold',G.AGCHangThreshold);
|
||
JW(O,'wf_agc_enabled',G.WfAGCEnabled);
|
||
JW(O,'wf_nf_enabled',G.WfNFEnabled);
|
||
JW(O,'last_band',G.LastBand);
|
||
JW(O,'last_xvtr',G.LastXvtr);
|
||
JW(O,'sample_rate',G.SampleRate);
|
||
JW(O,'rx_gain_mode',G.RxGainMode);
|
||
JW(O,'rx_gain_db',G.RxGainDb);
|
||
// Display settings
|
||
JW(O,'fft_size',G.FFTSize);
|
||
JW(O,'win_type',G.WindowType);
|
||
JW(O,'spec_detector',G.SpecDetector);
|
||
JW(O,'spec_avg_mode',G.SpecAvgMode);
|
||
JW(O,'spec_avg_time_ms',G.SpecAvgTimeMS);
|
||
JW(O,'wf_detector',G.WfDetector);
|
||
JW(O,'wf_avg_mode',G.WfAvgMode);
|
||
JW(O,'wf_avg_time_ms',G.WfAvgTimeMS);
|
||
JW(O,'wf_manual_high',G.WfManualHigh);
|
||
JW(O,'wf_manual_low',G.WfManualLow);
|
||
JW(O,'wf_agc_offset',G.WfAGCOffset);
|
||
JW(O,'wf_palette',G.WfPalette);
|
||
JW(O,'wf_gamma',G.WfGamma);
|
||
JW(O,'display_pixels',G.DisplayPixels);
|
||
JW(O,'wf_speed_div',G.WfSpeedDiv);
|
||
JW(O,'spec_msaa',G.SpecMSAA);
|
||
JW(O,'spec_ref_level',G.SpecRefLevel);
|
||
JW(O,'spec_range',G.SpecRange);
|
||
JW(O,'spec_grid_step',G.SpecGridStep);
|
||
// Audio settings
|
||
JWS(O,'audio_out_device',G.AudioOutDevice);
|
||
JW(O,'audio_sample_rate',G.AudioSampleRate);
|
||
JWS(O,'audio_in_device',G.AudioInDevice);
|
||
JW(O,'send_audio_to_radio',G.SendAudioToRadio);
|
||
JW(O,'raw_fm_deviation',G.RawFMDeviation);
|
||
// Visibility settings
|
||
JW(O,'show_spectrum',G.ShowSpectrum);
|
||
JW(O,'show_waterfall',G.ShowWaterfall);
|
||
JW(O,'show_wideband',G.ShowWideband);
|
||
JW(O,'wideband_fill',G.WidebandFill);
|
||
JW(O,'spectrum_fill',G.SpectrumFill);
|
||
JW(O,'display_duplex',G.DisplayDuplex);
|
||
JW(O,'rx_mute_on_tx',G.RxMuteOnTx);
|
||
JW(O,'display_fps',G.DisplayFPS);
|
||
JW(O,'light_theme',G.LightTheme);
|
||
JW(O,'freq_mhz_digits',G.FreqMhzDigits);
|
||
// PA settings
|
||
JW(O,'pa_max_power',G.PAMaxPower);
|
||
JW(O,'pluto_tx_max_att',G.PlutoTxMaxAttDb);
|
||
for i := 0 to CFG_BAND_COUNT-1 do
|
||
JW(O,'pa_band_cal_'+IntToStr(i),G.PABandCal[i]);
|
||
for i := 0 to CFG_XVTR_COUNT-1 do
|
||
JW(O,'vhf_cal_'+IntToStr(i),G.VHFBandCal[i]);
|
||
// CAT serial settings
|
||
for i := 0 to 3 do begin
|
||
JW(O,'cat_serial_en_'+IntToStr(i), G.CATSerialEnabled[i]);
|
||
JWS(O,'cat_serial_port_'+IntToStr(i),G.CATSerialPort[i]);
|
||
JW(O,'cat_serial_baud_'+IntToStr(i), G.CATSerialBaud[i]);
|
||
JW(O,'cat_serial_dbits_'+IntToStr(i),G.CATSerialDataBits[i]);
|
||
JW(O,'cat_serial_sbits_'+IntToStr(i),G.CATSerialStopBits[i]);
|
||
JW(O,'cat_serial_parity_'+IntToStr(i),G.CATSerialParity[i]);
|
||
JW(O,'cat_serial_androm_'+IntToStr(i),G.CATSerialAndromeda[i]);
|
||
end;
|
||
for i := 0 to CFG_CAT_SLICE_COUNT-1 do begin
|
||
JW(O,'cat_slice_en_' +IntToStr(i), G.CATSliceEnabled[i]);
|
||
JW(O,'cat_slice_port_' +IntToStr(i), G.CATSlicePort[i]);
|
||
JW(O,'cat_slice_autotx_'+IntToStr(i), G.CATSliceAutoTX[i]);
|
||
JWS(O,'cat_slice_out_' +IntToStr(i), G.CATSliceOutDev[i]);
|
||
JWS(O,'cat_slice_in_' +IntToStr(i), G.CATSliceInDev[i]);
|
||
end;
|
||
JW(O,'cat_tcp_enabled',G.CATTcpEnabled);
|
||
JW(O,'cat_tcp_port',G.CATTcpPort);
|
||
// ADC settings
|
||
JW(O,'dither_enabled',G.DitherEnabled);
|
||
JW(O,'random_enabled',G.RandomEnabled);
|
||
// Calibration
|
||
JW(O,'cal_pwr_det_model',G.Cal.PwrDetModel);
|
||
for i := 0 to CFG_BAND_COUNT-1 do
|
||
JW(O,'cal_pa_det_'+IntToStr(i),G.Cal.PADetCal[i]);
|
||
JW(O,'cal_supply_volt',G.Cal.SupplyVoltCal);
|
||
JW(O,'cal_amp_voff_mv',G.Cal.AmpVoffMV);
|
||
JW(O,'cal_amp_sens_mva',G.Cal.AmpSensMVA);
|
||
for i := 0 to CFG_BAND_COUNT-1 do
|
||
JW(O,'cal_smeter_band_'+IntToStr(i),G.Cal.SMeterBandDB[i]);
|
||
for i := 0 to CFG_XVTR_COUNT-1 do
|
||
JW(O,'cal_smeter_xvtr_'+IntToStr(i),G.Cal.SMeterXvtrDB[i]);
|
||
for i := 0 to CFG_BAND_COUNT-1 do
|
||
JW(O,'cal_disp_band_'+IntToStr(i),G.Cal.DispBandDB[i]);
|
||
for i := 0 to CFG_XVTR_COUNT-1 do
|
||
JW(O,'cal_disp_xvtr_'+IntToStr(i),G.Cal.DispXvtrDB[i]);
|
||
JW(O,'cal_freq_ppm',G.Cal.FreqCalPPM);
|
||
end;
|
||
|
||
procedure TSettingsManager.SaveBand(const MAC: array of Byte; BandIdx: Integer;
|
||
const B: TBandSettings);
|
||
var O: TJSONObject;
|
||
begin
|
||
if (BandIdx < 0) or (BandIdx >= CFG_BAND_COUNT) then Exit;
|
||
O := GetBandObj(GetDevObj(MacToStr(MAC)), BandIdx);
|
||
JW(O,'vfo_a',B.VfoA); JW(O,'vfo_b',B.VfoB);
|
||
JW(O,'mode',B.Mode);
|
||
JW(O,'filter_idx',B.FilterIdx); JW(O,'filter_bw',B.FilterBW);
|
||
JW(O,'agc_mode',B.AGCMode); JW(O,'agc_top',B.AGCTop);
|
||
JW(O,'ctun',B.CTun); JW(O,'center_hz',B.CenterHz); JW(O,'span_hz',B.SpanHz);
|
||
JW(O,'fmsq_on',B.FMSQOn); JW(O,'fmsq_level',B.FMSQLevel);
|
||
JW(O,'ctcss_on',B.CTCSSOn); JW(O,'ctcss_idx',B.CTCSSToneIdx);
|
||
JW(O,'fmstep_on',B.FMStepOn); JW(O,'fmstep_idx',B.FMStepIdx);
|
||
JW(O,'fmrpt_dir',B.FMRptDir); JW(O,'fmrpt_offset',B.FMRptOffsetHz);
|
||
JW(O,'zoom_factor',B.ZoomFactor); JW(O,'pan_slider',B.PanSlider);
|
||
JW(O,'rx_gain_mode',B.RxGainMode); JW(O,'rx_gain_db',B.RxGainDb);
|
||
JW(O,'atten_db',B.AttenDB);
|
||
end;
|
||
|
||
function TSettingsManager.LoadBand(const MAC: array of Byte; BandIdx: Integer;
|
||
out B: TBandSettings): Boolean;
|
||
var MacStr: string; O: TJSONObject;
|
||
begin
|
||
DefaultBand(BandIdx, B);
|
||
MacStr := MacToStr(MAC);
|
||
Result := FRoot.Find(MacStr) <> nil;
|
||
if not Result then Exit;
|
||
O := GetBandObj(GetDevObj(MacStr), BandIdx);
|
||
B.VfoA := JD(O,'vfo_a', B.VfoA);
|
||
B.VfoB := JD(O,'vfo_b', B.VfoB);
|
||
B.Mode := EnsureRange(JI(O,'mode', B.Mode), CFG_MODE_MIN, CFG_MODE_MAX);
|
||
B.FilterIdx := JI(O,'filter_idx', B.FilterIdx);
|
||
B.FilterBW := JI(O,'filter_bw', B.FilterBW);
|
||
B.AGCMode := JI(O,'agc_mode', B.AGCMode);
|
||
B.AGCTop := JI(O,'agc_top', B.AGCTop);
|
||
B.CTun := JB(O,'ctun', B.CTun);
|
||
B.CenterHz := JD(O,'center_hz', B.CenterHz);
|
||
B.SpanHz := JD(O,'span_hz', B.SpanHz);
|
||
B.FMSQOn := JB(O,'fmsq_on', B.FMSQOn);
|
||
B.FMSQLevel := JI(O,'fmsq_level', B.FMSQLevel);
|
||
B.CTCSSOn := JB(O,'ctcss_on', B.CTCSSOn);
|
||
B.CTCSSToneIdx := EnsureRange(JI(O,'ctcss_idx', B.CTCSSToneIdx), 0, 37);
|
||
B.FMStepOn := JB(O,'fmstep_on', B.FMStepOn);
|
||
B.FMStepIdx := EnsureRange(JI(O,'fmstep_idx', B.FMStepIdx), 0, 3);
|
||
B.FMRptDir := EnsureRange(JI(O,'fmrpt_dir', B.FMRptDir), 0, 2);
|
||
B.FMRptOffsetHz := JD(O,'fmrpt_offset', B.FMRptOffsetHz);
|
||
B.RxGainMode := EnsureRange(JI(O,'rx_gain_mode', B.RxGainMode), 0, 3);
|
||
B.RxGainDb := EnsureRange(JI(O,'rx_gain_db', B.RxGainDb), 0, 73);
|
||
B.AttenDB := EnsureRange(JI(O,'atten_db', B.AttenDB), 0, 31);
|
||
B.ZoomFactor := EnsureRange(JD(O,'zoom_factor', B.ZoomFactor), 0.0, 1.0);
|
||
B.PanSlider := EnsureRange(JD(O,'pan_slider', B.PanSlider), 0.0, 1.0);
|
||
end;
|
||
|
||
procedure TSettingsManager.SaveWindowBounds(L, T, W, H, DPI: Integer; Maximized: Boolean);
|
||
var O: TJSONObject;
|
||
begin
|
||
O := EnsureObj(FRoot, 'window');
|
||
JW(O, 'left', L);
|
||
JW(O, 'top', T);
|
||
JW(O, 'width', W);
|
||
JW(O, 'height', H);
|
||
JW(O, 'dpi', DPI);
|
||
JW(O, 'maximized', Maximized);
|
||
end;
|
||
|
||
procedure TSettingsManager.LoadWindowBounds(out L, T, W, H, DPI: Integer; out Maximized: Boolean);
|
||
var O: TJSONObject;
|
||
begin
|
||
L := 80; T := 80; W := 1400; H := 900; DPI := 0; Maximized := False;
|
||
if FRoot.Find('window') = nil then Exit;
|
||
O := EnsureObj(FRoot, 'window');
|
||
L := JI(O, 'left', 80);
|
||
T := JI(O, 'top', 80);
|
||
W := JI(O, 'width', 1400);
|
||
H := JI(O, 'height', 900);
|
||
DPI := JI(O, 'dpi', 0);
|
||
Maximized := JB(O, 'maximized', False);
|
||
end;
|
||
|
||
procedure TSettingsManager.SaveStartupPreview(VfoA, VfoB: Double; SampleRate: Integer);
|
||
var O: TJSONObject;
|
||
begin
|
||
O := EnsureObj(FRoot, 'startup');
|
||
JW(O, 'vfo_a', VfoA);
|
||
JW(O, 'vfo_b', VfoB);
|
||
JW(O, 'sample_rate', SampleRate);
|
||
end;
|
||
|
||
function TSettingsManager.LoadStartupPreview(out VfoA, VfoB: Double; out SampleRate: Integer): Boolean;
|
||
var O: TJSONObject;
|
||
begin
|
||
Result := FRoot.Find('startup') <> nil;
|
||
if not Result then Exit;
|
||
|
||
O := EnsureObj(FRoot, 'startup');
|
||
VfoA := JD(O, 'vfo_a', 14200000);
|
||
VfoB := JD(O, 'vfo_b', 7100000);
|
||
SampleRate := JI(O, 'sample_rate', 192000);
|
||
end;
|
||
|
||
procedure TSettingsManager.SaveAudioBufferSize(BufferSize: Integer);
|
||
var O: TJSONObject;
|
||
begin
|
||
if BufferSize <= 128 then BufferSize := 128
|
||
else if BufferSize <= 256 then BufferSize := 256
|
||
else BufferSize := 512;
|
||
O := EnsureObj(FRoot, 'audio');
|
||
JW(O, 'output_buffer_size', BufferSize);
|
||
end;
|
||
|
||
function TSettingsManager.LoadAudioBufferSize: Integer;
|
||
var O: TJSONObject;
|
||
begin
|
||
if FRoot.Find('audio') = nil then Exit(128);
|
||
O := EnsureObj(FRoot, 'audio');
|
||
Result := JI(O, 'output_buffer_size', 128);
|
||
if Result <= 128 then Result := 128
|
||
else if Result <= 256 then Result := 256
|
||
else Result := 512;
|
||
end;
|
||
|
||
procedure TSettingsManager.SaveCATSettings(const G: TGlobalSettings);
|
||
var O: TJSONObject; i: Integer;
|
||
begin
|
||
O := EnsureObj(FRoot, 'cat');
|
||
JW(O, 'tcp_en', G.CATTcpEnabled);
|
||
JW(O, 'tcp_port', G.CATTcpPort);
|
||
for i := 0 to 3 do
|
||
begin
|
||
JW(O, 'ser_en_' +IntToStr(i), G.CATSerialEnabled[i]);
|
||
JWS(O, 'ser_port_' +IntToStr(i), G.CATSerialPort[i]);
|
||
JW(O, 'ser_baud_' +IntToStr(i), G.CATSerialBaud[i]);
|
||
JW(O, 'ser_dbits_' +IntToStr(i), G.CATSerialDataBits[i]);
|
||
JW(O, 'ser_sbits_' +IntToStr(i), G.CATSerialStopBits[i]);
|
||
JW(O, 'ser_parity_'+IntToStr(i), G.CATSerialParity[i]);
|
||
JW(O, 'ser_androm_'+IntToStr(i), G.CATSerialAndromeda[i]);
|
||
end;
|
||
for i := 0 to CFG_CAT_SLICE_COUNT-1 do
|
||
begin
|
||
JW(O, 'slice_en_' +IntToStr(i), G.CATSliceEnabled[i]);
|
||
JW(O, 'slice_port_' +IntToStr(i), G.CATSlicePort[i]);
|
||
JW(O, 'slice_autotx_'+IntToStr(i), G.CATSliceAutoTX[i]);
|
||
JWS(O,'slice_out_' +IntToStr(i), G.CATSliceOutDev[i]);
|
||
JWS(O,'slice_in_' +IntToStr(i), G.CATSliceInDev[i]);
|
||
end;
|
||
end;
|
||
|
||
procedure TSettingsManager.LoadCATSettings(var G: TGlobalSettings);
|
||
var O: TJSONObject; i: Integer;
|
||
begin
|
||
if FRoot.Find('cat') = nil then Exit;
|
||
O := EnsureObj(FRoot, 'cat');
|
||
G.CATTcpEnabled := JB(O, 'tcp_en', False);
|
||
G.CATTcpPort := JI(O, 'tcp_port', 19090);
|
||
for i := 0 to 3 do
|
||
begin
|
||
G.CATSerialEnabled[i] := JB(O, 'ser_en_' +IntToStr(i), False);
|
||
{$IFDEF MSWINDOWS}
|
||
G.CATSerialPort[i] := JS(O, 'ser_port_' +IntToStr(i), 'COM'+IntToStr(i+1));
|
||
{$ELSE}
|
||
G.CATSerialPort[i] := JS(O, 'ser_port_' +IntToStr(i), '/dev/ttyS'+IntToStr(i));
|
||
{$ENDIF}
|
||
G.CATSerialBaud[i] := JI(O, 'ser_baud_' +IntToStr(i), 9600);
|
||
G.CATSerialDataBits[i] := JI(O, 'ser_dbits_' +IntToStr(i), 8);
|
||
G.CATSerialStopBits[i] := JI(O, 'ser_sbits_' +IntToStr(i), 1);
|
||
G.CATSerialParity[i] := JI(O, 'ser_parity_'+IntToStr(i), 0);
|
||
G.CATSerialAndromeda[i]:= JB(O, 'ser_androm_'+IntToStr(i), False);
|
||
end;
|
||
for i := 0 to CFG_CAT_SLICE_COUNT-1 do
|
||
begin
|
||
G.CATSliceEnabled[i] := JB(O, 'slice_en_' +IntToStr(i), False);
|
||
G.CATSlicePort[i] := JI(O, 'slice_port_' +IntToStr(i), CFG_CAT_SLICE_PORT0 + i);
|
||
G.CATSliceAutoTX[i] := JB(O, 'slice_autotx_'+IntToStr(i), False);
|
||
G.CATSliceOutDev[i] := JS(O, 'slice_out_' +IntToStr(i), '');
|
||
G.CATSliceInDev[i] := JS(O, 'slice_in_' +IntToStr(i), '');
|
||
end;
|
||
end;
|
||
|
||
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.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);
|
||
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);
|
||
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.DefaultWeb(out W: TWebSettings);
|
||
begin
|
||
W.Enabled := True;
|
||
W.Port := 8080;
|
||
W.BindAddr := '0.0.0.0';
|
||
W.User := 'admin';
|
||
W.Pass := 'hpsdr';
|
||
W.SpecPixels := 1024; // экономный дефолт: столько же, сколько слал протокол раньше
|
||
end;
|
||
|
||
procedure TSettingsManager.LoadWebSettings(out W: TWebSettings);
|
||
var O: TJSONObject;
|
||
begin
|
||
DefaultWeb(W);
|
||
if FRoot.Find('web') = nil then Exit;
|
||
O := EnsureObj(FRoot, 'web');
|
||
W.Enabled := JB(O, 'enabled', True);
|
||
W.Port := EnsureRange(JI(O, 'port', 8080), 1, 65535);
|
||
W.BindAddr := JS(O, 'bind_addr', '0.0.0.0');
|
||
W.User := JS(O, 'user', 'admin');
|
||
W.Pass := JS(O, 'pass', 'hpsdr');
|
||
W.SpecPixels := EnsureRange(JI(O, 'spec_pixels', 1024), 256, 4096);
|
||
end;
|
||
|
||
procedure TSettingsManager.SaveWebSettings(const W: TWebSettings);
|
||
var O: TJSONObject;
|
||
begin
|
||
O := EnsureObj(FRoot, 'web');
|
||
JW(O, 'enabled', W.Enabled);
|
||
JW(O, 'port', W.Port);
|
||
JWS(O, 'bind_addr', W.BindAddr);
|
||
JWS(O, 'user', W.User);
|
||
JWS(O, 'pass', W.Pass);
|
||
JW(O, 'spec_pixels', W.SpecPixels);
|
||
Save;
|
||
end;
|
||
|
||
procedure TSettingsManager.SaveTheme(ALightTheme: Boolean);
|
||
var O: TJSONObject;
|
||
begin
|
||
O := EnsureObj(FRoot, 'preferences');
|
||
JW(O, 'light_theme', ALightTheme);
|
||
end;
|
||
|
||
function TSettingsManager.LoadTheme: Boolean;
|
||
var O: TJSONObject;
|
||
begin
|
||
Result := False;
|
||
if FRoot.Find('preferences') = nil then Exit;
|
||
O := EnsureObj(FRoot, 'preferences');
|
||
Result := JB(O, 'light_theme', False);
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Панадаптеры + слайсы (per-device, JSON-секция "pans" под MAC)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
class procedure TSettingsManager.DefaultPans(out P: TPansConfig);
|
||
var i: Integer;
|
||
begin
|
||
P.GridMode := False;
|
||
P.GridRowShare := 0.5;
|
||
for i := 0 to CFG_PANS_MAX-1 do
|
||
begin
|
||
P.Pans[i].Active := False;
|
||
P.Pans[i].FreqHz := 0.0;
|
||
P.Pans[i].RateKHz := 384;
|
||
P.Pans[i].ADC := 0;
|
||
P.Pans[i].Zoom := 0.0;
|
||
P.Pans[i].PanSlider := 0.5;
|
||
P.Pans[i].StackShare := 1.0;
|
||
P.Pans[i].ColShare := 1.0;
|
||
P.Pans[i].SplitRatio := 0.40; // = TPanafallPanel дефолт
|
||
P.Pans[i].HasDisplay := False;
|
||
P.Pans[i].DPalette := 0;
|
||
P.Pans[i].DGamma := 1.0;
|
||
P.Pans[i].DSpecRef := -20.0;
|
||
P.Pans[i].DSpecRange := 110.0;
|
||
P.Pans[i].DWfAGC := True;
|
||
P.Pans[i].DWfOffset := 0.0;
|
||
P.Pans[i].DWfHigh := -60.0;
|
||
P.Pans[i].DWfLow := -120.0;
|
||
SetLength(P.Pans[i].Slices, 0);
|
||
end;
|
||
end;
|
||
|
||
procedure TSettingsManager.SavePans(const MAC: array of Byte; const Ctx: string;
|
||
const P: TPansConfig);
|
||
var
|
||
Root, PanObj, SliceObj, DispObj: TJSONObject;
|
||
PanArr, SliceArr: TJSONArray;
|
||
i, j, Idx: Integer;
|
||
begin
|
||
Root := EnsureObj(EnsureObj(GetDevObj(MacToStr(MAC)), 'pans'), Ctx);
|
||
JW(Root, 'grid_mode', P.GridMode);
|
||
JW(Root, 'grid_row_share', P.GridRowShare);
|
||
// Список панов пересоздаётся целиком (проще, чем merge по индексам).
|
||
Idx := Root.IndexOfName('list');
|
||
if Idx >= 0 then Root.Delete(Idx);
|
||
PanArr := TJSONArray.Create;
|
||
Root.Add('list', PanArr);
|
||
for i := 0 to CFG_PANS_MAX-1 do
|
||
begin
|
||
// Пан 0 пишем всегда (ради его доли стека/слайсов), паны N — только активные.
|
||
if (i > 0) and not P.Pans[i].Active then Continue;
|
||
PanObj := TJSONObject.Create;
|
||
PanArr.Add(PanObj);
|
||
PanObj.Add('id', i);
|
||
PanObj.Add('active', P.Pans[i].Active);
|
||
PanObj.Add('freq', P.Pans[i].FreqHz);
|
||
PanObj.Add('rate_khz', P.Pans[i].RateKHz);
|
||
PanObj.Add('adc', P.Pans[i].ADC);
|
||
PanObj.Add('zoom', P.Pans[i].Zoom);
|
||
PanObj.Add('pan', P.Pans[i].PanSlider);
|
||
PanObj.Add('stack_share', P.Pans[i].StackShare);
|
||
PanObj.Add('col_share', P.Pans[i].ColShare);
|
||
PanObj.Add('split_ratio', P.Pans[i].SplitRatio);
|
||
// Per-pan дисплей: пишем блок только при override (иначе пан = глоб. дефолт).
|
||
if P.Pans[i].HasDisplay then
|
||
begin
|
||
DispObj := TJSONObject.Create;
|
||
PanObj.Add('display', DispObj);
|
||
DispObj.Add('palette', P.Pans[i].DPalette);
|
||
DispObj.Add('gamma', P.Pans[i].DGamma);
|
||
DispObj.Add('spec_ref', P.Pans[i].DSpecRef);
|
||
DispObj.Add('spec_range', P.Pans[i].DSpecRange);
|
||
DispObj.Add('wf_agc', P.Pans[i].DWfAGC);
|
||
DispObj.Add('wf_offset', P.Pans[i].DWfOffset);
|
||
DispObj.Add('wf_high', P.Pans[i].DWfHigh);
|
||
DispObj.Add('wf_low', P.Pans[i].DWfLow);
|
||
end;
|
||
SliceArr := TJSONArray.Create;
|
||
PanObj.Add('slices', SliceArr);
|
||
for j := 0 to High(P.Pans[i].Slices) do
|
||
begin
|
||
SliceObj := TJSONObject.Create;
|
||
SliceArr.Add(SliceObj);
|
||
SliceObj.Add('target_hz', P.Pans[i].Slices[j].TargetHz);
|
||
SliceObj.Add('mode', P.Pans[i].Slices[j].Mode);
|
||
SliceObj.Add('filter_lo', P.Pans[i].Slices[j].FilterLo);
|
||
SliceObj.Add('filter_hi', P.Pans[i].Slices[j].FilterHi);
|
||
SliceObj.Add('agc', P.Pans[i].Slices[j].AGC);
|
||
SliceObj.Add('volume', P.Pans[i].Slices[j].Volume);
|
||
SliceObj.Add('muted', P.Pans[i].Slices[j].Muted);
|
||
SliceObj.Add('rx_mute_on_tx', P.Pans[i].Slices[j].RxMuteOnTx);
|
||
SliceObj.Add('fmsq_on', P.Pans[i].Slices[j].FMSQOn);
|
||
SliceObj.Add('fmsq_level', P.Pans[i].Slices[j].FMSQLevel);
|
||
SliceObj.Add('dev_name', P.Pans[i].Slices[j].DevName);
|
||
SliceObj.Add('in_dev_name', P.Pans[i].Slices[j].InDevName);
|
||
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.
|