mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 19:45:09 +00:00
LoadDevice (bulk startup band loader) parsed only freq/mode/filter/AGC/ CTun/center/span and skipped the FM fields, while SaveBand writes them and LoadBand reads them. So FM squelch, CTCSS, step and repeater shift were saved on exit but reset to defaults on the next start (band switching kept working — that path goes through the in-memory band cache, not disk). Added the missing fmsq_on/fmsq_level/ctcss_on/ctcss_idx/fmstep_on/ fmstep_idx/fmrpt_dir/fmrpt_offset reads to LoadDevice, mirroring LoadBand. Affects all backends, not just Pluto. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1492 lines
65 KiB
ObjectPascal
1492 lines
65 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;
|
||
|
||
const
|
||
SETTINGS_FILE = 'hpsdr_settings.json';
|
||
CFG_BAND_COUNT = 11;
|
||
CFG_XVTR_COUNT = 8; // количество XVTR-слотов (трансверторов)
|
||
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
|
||
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);
|
||
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);
|
||
CFG_BAND_DEFAULT_MODE: array[0..CFG_BAND_COUNT-1] of Integer = (
|
||
0, 0, 0, 0, 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
|
||
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
|
||
|
||
type
|
||
TBandSettings = record
|
||
VfoA: Double;
|
||
VfoB: Double;
|
||
Mode: Integer;
|
||
FilterIdx: Integer;
|
||
FilterBW: Integer;
|
||
AGCMode: Integer;
|
||
AGCTop: Integer;
|
||
CTun: Boolean;
|
||
CenterHz: Double; // DDC-центр при CTun (восстановление offset VFO в полосе)
|
||
SpanHz: Double;
|
||
FMSQOn: Boolean;
|
||
FMSQLevel: Integer; // 0..100 → WDSP threshold 0.0..0.5
|
||
CTCSSOn: Boolean;
|
||
CTCSSToneIdx: Integer; // 0..37
|
||
FMStepOn: Boolean;
|
||
FMStepIdx: Integer; // 0..3 (6.25/12.5/20/25 kHz)
|
||
FMRptDir: Integer; // RPT_NONE/RPT_MINUS/RPT_PLUS
|
||
FMRptOffsetHz: Double; // Hz, 0 = use band default
|
||
end;
|
||
|
||
// TX-настройки per-device. Хранятся в JSON-секции "tx" под MAC,
|
||
// отдельно от global/bands. Применяются к WDSP через SetTX* методы.
|
||
TTXSettings = record
|
||
// Radio mic options (DUC Specific byte 50/51)
|
||
MicLineIn: Boolean; // bit0: 1=Line In, 0=Mic In
|
||
MicBoost: Boolean; // bit1: +20 dB preamp (только когда Mic In)
|
||
MicPTTEnabled: Boolean; // bit2: Mic PTT enable (Orion)
|
||
MicTipRing: Boolean; // bit3: False=Tip, True=Ring (Orion/OrionMkII)
|
||
MicBias: Boolean; // bit4: электретный +5V bias (Orion)
|
||
LineInGainDB: Double; // byte51: -34.5..+12 dB (шаг 1.5; только когда Line In)
|
||
// Mic gain (dB, применяется в WDSP PanelGain1)
|
||
MicGainDB: Double; // -10..+30
|
||
// ATT on TX — ослабление RX-ADC во время передачи (защита от утечки TX).
|
||
// DUC Specific bytes 57/58/59 (StepAtten 0/1/2). Thetis default: 31 dB.
|
||
AttOnTX: Integer; // 0..31 dB
|
||
// Tune (кнопка TUN) — генерация тонального сигнала через WDSP PostGen.
|
||
// Уровень — отдельный от слайдера Drive, проходит через ту же
|
||
// per-band калибровку. Thetis defaults: level=10 %, freq=750 Hz.
|
||
TUNLevel: Integer; // 0..100 (% от полной шкалы Drive)
|
||
TUNFreq: Integer; // Hz, обычно 600..1500 для SSB tune
|
||
// TX bandpass filter
|
||
FilterLow: Integer; // Hz, для SSB обычно 100
|
||
FilterHigh: Integer; // Hz, для SSB обычно 2800
|
||
FilterNC: Integer; // FIR length (1024/2048/4096)
|
||
FilterMP: Integer; // 0=Linear Phase, 1=Low Latency
|
||
FilterWindow: Integer; // 0=Rect..6=BH7
|
||
// Compressor
|
||
CompressorOn: Boolean;
|
||
CompressorGain: Double; // 0..20 dB
|
||
// Leveler (TX AGC)
|
||
LevelerOn: Boolean;
|
||
LevelerTop: Double; // 0..10 dB
|
||
LevelerDecay: Integer; // ms
|
||
// ALC (Auto Level Control)
|
||
ALCOn: Boolean;
|
||
ALCMaxGain: Double; // -10..+10 dB
|
||
ALCDecay: Integer; // ms
|
||
// Phase Rotator
|
||
PhaseRotOn: Boolean;
|
||
PhaseRotStages: Integer; // 4/6/8
|
||
PhaseRotFreq: Double; // Hz, обычно 338
|
||
// EQ TX (10-band: [0]=preamp, [1..10]=bands)
|
||
EQOn: Boolean;
|
||
EQNumBands: Integer; // 3 или 10
|
||
EQGains: array[0..10] of Double; // dB, ±15
|
||
EQFreqs: array[0..10] of Double; // Hz
|
||
// AM modulator
|
||
AMCarrierLevel: Double; // 0..1
|
||
// FM modulator
|
||
FMDeviation: Double; // Hz, 2500..5000
|
||
FMLowCut: Integer; // AF filter low
|
||
FMHighCut: Integer; // AF filter high
|
||
FMEmphPosition: Integer; // 0=PreEQ, 1=PostEQ
|
||
// CTCSS
|
||
CTCSSOn: Boolean;
|
||
CTCSSFreq: Double; // Hz
|
||
// TX Display (отдельный analyzer для TX-спектра/водопада)
|
||
TXFFTSize: Integer; // 4096..262144
|
||
TXWindowType: Integer; // 0..6
|
||
TXSpecDetector: Integer; // 0=Peak,1=Rosenfell,2=Average,3=Sample,4=RMS
|
||
TXSpecAvgMode: Integer; // 0=None,1=Recursive,2=TimeWindow,3=LogRecursive
|
||
TXSpecAvgTimeMS: Double;
|
||
TXWfDetector: Integer;
|
||
TXWfAvgMode: Integer;
|
||
TXWfAvgTimeMS: Double;
|
||
// TX Grid (отдельные от RX — Thetis отрисовывает спектр TX в своих границах)
|
||
TXSpecRefLevel: Double; // dBm reference (top of scale), e.g. 0
|
||
TXSpecRange: Double; // dB range, e.g. 80
|
||
TXSpecGridStep: Double; // grid step in dB, e.g. 10
|
||
end;
|
||
|
||
// Web-сервер — глобальные настройки (не привязаны к устройству, секция "web").
|
||
TWebSettings = record
|
||
Enabled: Boolean;
|
||
Port: Integer; // 1..65535
|
||
BindAddr: string; // e.g. '0.0.0.0' (все интерфейсы)
|
||
User: string;
|
||
Pass: string;
|
||
end;
|
||
|
||
// Transverter (XVTR) entry — один трансвертер.
|
||
// Видимая частота VFO в [FreqBegin..FreqEnd] транслируется в IF-частоту,
|
||
// которую слышит трансивер: f_IF = f_visible - LOOffset + LOError.
|
||
// Примеры: 2m XVTR с LO=116 MHz → visible 144.200 ⇒ IF 28.200 MHz.
|
||
TXvtrEntry = record
|
||
Enabled: Boolean;
|
||
ButtonText: string[15]; // подпись на band-кнопке (например, '2m', '70cm')
|
||
FreqBegin: Double; // Hz
|
||
FreqEnd: Double; // Hz
|
||
LOOffset: Double; // Hz, отнимается от видимой частоты для получения IF (RX)
|
||
LOError: Double; // Hz, корректировка LO (положительная — IF растёт) — RX/LNB дрейф
|
||
// QO-100 / split-LO транспондер: RX и TX идут на РАЗНЫХ LO. RX = visible-LOOffset
|
||
// (+LOError, дрейф LNB по маяку). TX = visible-TxLOOffset (без LOError — uplink-LO
|
||
// собственный у SDR). FullDuplexTx помечает слот как такой транспондер.
|
||
FullDuplexTx: Boolean; // True — раздельный TX-LO (QO-100); авто display-duplex
|
||
TxLOOffset: Double; // Hz, TX LO-offset (для QO-100 = транспондер-offset, ~8089.5 МГц)
|
||
RXGain: Double; // dB, компенсация усиления RX тракта
|
||
RXOnly: Boolean; // True — запрещает TX на этом трансверторе
|
||
TXPower: Integer; // 0..100 % (drive level специально для XVTR)
|
||
DisablePA: Boolean; // True — отключить внутренний PA при TX (XVTR имеет свой)
|
||
RXAntenna: Byte; // 0=по-умолчанию(основной band), 1=ANT1, 2=ANT2, 3=ANT3
|
||
LastFreq: Double; // Hz — последняя tuned visible частота VFO A (BandStack)
|
||
LastFreqB: Double; // Hz — последняя tuned visible частота VFO B
|
||
LastMode: Integer; // Mode (LSB=0,USB=1,…FM=5)
|
||
LastFilterIdx: Integer; // FM: 0=NFM, 1=WFM; иначе 0..FILT_COUNT-1
|
||
LastFMSQOn: Boolean;
|
||
LastFMSQLevel: Integer;
|
||
LastCTCSSOn: Boolean;
|
||
LastCTCSSToneIdx: Integer;
|
||
LastFMStepOn: Boolean;
|
||
LastFMStepIdx: Integer;
|
||
LastCTun: Boolean;
|
||
LastCenterHz: Double; // Hz — DDC-центр при CTun (восстановление offset VFO)
|
||
LastAGCMode: Integer;
|
||
LastAGCTop: Integer;
|
||
LastFMRptDir: Integer;
|
||
LastFMRptOffsetHz: Double;
|
||
LastCTCSSAutoActive: Boolean; // CTCSS был включён каналом (авто)
|
||
LastFMRptAutoActive: Boolean; // RPT был включён каналом (авто)
|
||
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 для всех диапазонов, вторичный вход выключен.
|
||
TAlexSettings = record
|
||
// Per-band (11 bands: 0=160m..10=6m)
|
||
RxAnt: array[0..CFG_BAND_COUNT-1] of Byte; // 1..3 (ANT1/2/3)
|
||
TxAnt: array[0..CFG_BAND_COUNT-1] of Byte; // 1..3 (ANT1/2/3)
|
||
// Вторичный RX вход: 0=выкл, 1=BYPS(Ext1+Bypass), 2=EXT1(Ext2+Bypass), 3=XVTR(XvtrDDC+Bypass)
|
||
RxOnly: array[0..CFG_BAND_COUNT-1] of Byte;
|
||
DoNotTx: array[0..CFG_BAND_COUNT-1] of Boolean; // программный запрет TX на диапазоне
|
||
// Глобальные флаги в режиме TX
|
||
RxBypassOnTx: Boolean; // бит 11 (Bypass relay) во время TX
|
||
Ext1OnTx: Boolean; // бит 9 (Ext1) + бит 11 во время TX
|
||
GndBpf2OnTx: Boolean; // заземление BPF2/RX2 во время TX (Alex1 бит 8, ANAN-8000DLE)
|
||
EnableXvtrHf: Boolean; // режим XVTR HF
|
||
end;
|
||
|
||
TGlobalSettings = record
|
||
Volume: Integer;
|
||
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
|
||
SpecRefLevel: Double; // dBm reference (top of scale), e.g. -20
|
||
SpecRange: Double; // dB range, e.g. 110 (bottom = ref - range)
|
||
SpecGridStep: Double; // grid step in dB, e.g. 10
|
||
// --- Audio settings ---
|
||
AudioOutDevice: string; // PortAudio output device name ('' = default)
|
||
AudioSampleRate: Integer; // PortAudio stream sample rate (e.g. 48000)
|
||
AudioInDevice: string; // PortAudio input device name for TX mic ('' = none)
|
||
SendAudioToRadio: Boolean; // True = send RX audio stream to radio (built-in speaker)
|
||
// --- Visibility settings ---
|
||
ShowSpectrum: Boolean; // True = draw spectrum on main form
|
||
ShowWaterfall: Boolean; // True = draw waterfall on main form
|
||
ShowWideband: Boolean; // True = draw raw ADC wideband pane on main form
|
||
WidebandFill: Boolean; // True = fill wideband spectrum with gradient
|
||
SpectrumFill: Boolean; // True = fill main spectrum under the curve with gradient
|
||
DisplayDuplex: Boolean; // DUP: при TX показывать RX-водопад, TX-фильтр overlay'ем
|
||
// --- Display FPS ---
|
||
DisplayFPS: Integer; // spectrum/waterfall timer fps (1..100)
|
||
// --- Theme ---
|
||
LightTheme: Boolean; // False = dark (default), True = light
|
||
// --- VFO display digits ---
|
||
FreqMhzDigits: Integer; // minimum MHz digit count: 3=999MHz, 4=9.999GHz, 5=99.999GHz
|
||
// --- PA (Power Amplifier) settings ---
|
||
PAMaxPower: Double; // максимальная выходная мощность, Вт (5..200)
|
||
PlutoTxMaxAttDb: Double; // Pluto: TX-аттенюация на drive=100%, дБ<0 (дефолт -10)
|
||
PABandCal: array[0..CFG_BAND_COUNT-1] of Double; // калибровка на диапазон 38.8..100.0
|
||
VHFBandCal: array[0..CFG_XVTR_COUNT-1] of Double; // калибровка для XVTR слотов 38.8..100.0
|
||
// --- ADC settings ---
|
||
DitherEnabled: Boolean; // DDC Specific byte 5 — уменьшает интермодуляцию
|
||
RandomEnabled: Boolean; // DDC Specific byte 6 — рандомизация ADC
|
||
// --- CAT (Computer Aided Transceiver) settings ---
|
||
// До 4 последовательных портов (Linux: /dev/ttyS0, Windows: COM1 и т.д.)
|
||
CATSerialEnabled: array[0..3] of Boolean;
|
||
CATSerialPort: array[0..3] of string; // имя порта ОС
|
||
CATSerialBaud: array[0..3] of Integer; // скорость: 1200..115200
|
||
CATSerialDataBits: array[0..3] of Integer; // 7 или 8
|
||
CATSerialStopBits: array[0..3] of Integer; // 1 или 2
|
||
CATSerialParity: array[0..3] of Integer; // 0=None, 1=Odd, 2=Even
|
||
// TCP CAT сервер (один, многоклиентский)
|
||
CATTcpEnabled: Boolean;
|
||
CATTcpPort: Integer; // порт, по умолчанию 19090
|
||
end;
|
||
|
||
TSettingsManager = class
|
||
private
|
||
FFilePath: string;
|
||
FRoot: TJSONObject;
|
||
function EnsureObj(P: TJSONObject; const K: string): TJSONObject;
|
||
function GetDevObj(const M: string): TJSONObject;
|
||
function GetGlobalObj(D: TJSONObject): TJSONObject;
|
||
function GetBandObj(D: TJSONObject; Idx: Integer): TJSONObject;
|
||
function JI(O: TJSONObject; const K: string; Def: Integer): Integer;
|
||
function JD(O: TJSONObject; const K: string; Def: Double): Double;
|
||
function JB(O: TJSONObject; const K: string; Def: Boolean): Boolean;
|
||
function JS(O: TJSONObject; const K: string; const Def: string): string;
|
||
procedure JW(O: TJSONObject; const K: string; V: Integer); overload;
|
||
procedure JW(O: TJSONObject; const K: string; V: Double); overload;
|
||
procedure JW(O: TJSONObject; const K: string; V: Boolean); overload;
|
||
procedure JWS(O: TJSONObject; const K: string; const V: string);
|
||
procedure MigrateXvtrTemplate(var X: TXvtrSettings;
|
||
const AName: string; ABegin, AEnd, ALO: Double;
|
||
AFullDup: Boolean = False; ATxLO: Double = 0.0);
|
||
public
|
||
constructor Create(const FilePath: string = '');
|
||
destructor Destroy; override;
|
||
procedure Load;
|
||
procedure Save;
|
||
function LoadDevice(const MAC: array of Byte;
|
||
out G: TGlobalSettings;
|
||
var Bands: array of TBandSettings): Boolean;
|
||
procedure SaveGlobal(const MAC: array of Byte; const G: TGlobalSettings);
|
||
procedure SaveBand(const MAC: array of Byte; BandIdx: Integer;
|
||
const B: TBandSettings);
|
||
function LoadBand(const MAC: array of Byte; BandIdx: Integer;
|
||
out B: TBandSettings): Boolean;
|
||
// TX settings per-device (JSON-секция "tx" под MAC).
|
||
// LoadTX возвращает True если устройство уже зарегистрировано в JSON;
|
||
// в любом случае T заполняется (дефолтами или сохранёнными значениями).
|
||
function LoadTX(const MAC: array of Byte; out T: TTXSettings): Boolean;
|
||
procedure SaveTX(const MAC: array of Byte; const T: TTXSettings);
|
||
class function MacToStr(const MAC: array of Byte): string;
|
||
class procedure DefaultBand(BandIdx: Integer; out B: TBandSettings);
|
||
class procedure DefaultGlobal(out G: TGlobalSettings);
|
||
class procedure DefaultTX(out T: TTXSettings);
|
||
// Размер окна — не привязан к MAC, хранится в корне JSON
|
||
procedure SaveWindowBounds(L, T, W, H, DPI: Integer; Maximized: Boolean);
|
||
procedure LoadWindowBounds(out L, T, W, H, DPI: Integer; out Maximized: Boolean);
|
||
procedure SaveStartupPreview(VfoA, VfoB: Double; SampleRate: Integer);
|
||
function LoadStartupPreview(out VfoA, VfoB: Double; out SampleRate: Integer): Boolean;
|
||
procedure SaveAudioBufferSize(BufferSize: Integer);
|
||
function LoadAudioBufferSize: Integer;
|
||
// CAT settings — не привязаны к MAC, хранятся в корне JSON (доступны до подключения)
|
||
procedure SaveCATSettings(const G: TGlobalSettings);
|
||
procedure LoadCATSettings(var G: TGlobalSettings);
|
||
// Alex (antenna) settings per-device (JSON-секция "alex" под MAC).
|
||
class procedure DefaultAlex(out A: TAlexSettings);
|
||
function LoadAlex(const MAC: array of Byte; out A: TAlexSettings): Boolean;
|
||
procedure SaveAlex(const MAC: array of Byte; const A: TAlexSettings);
|
||
// XVTR (Transverter) settings per-device (JSON-секция "xvtr" под MAC).
|
||
class procedure DefaultXvtr(out X: TXvtrSettings);
|
||
function LoadXvtr(const MAC: array of Byte; out X: TXvtrSettings): Boolean;
|
||
procedure SaveXvtr(const MAC: array of Byte; const X: TXvtrSettings);
|
||
// Web-сервер — глобальные настройки (секция "web" в корне JSON).
|
||
class procedure DefaultWeb(out W: TWebSettings);
|
||
procedure LoadWebSettings(out W: TWebSettings);
|
||
procedure SaveWebSettings(const W: TWebSettings);
|
||
// Тема — глобальная настройка, не привязана к устройству
|
||
procedure SaveTheme(ALightTheme: Boolean);
|
||
function LoadTheme: Boolean;
|
||
end;
|
||
|
||
implementation
|
||
|
||
function LegacyAverageTimeMS(FPS, AvgMode, AvgCount: Integer;
|
||
Backmult, DefaultMS: Double): Double;
|
||
var
|
||
FPSv: Integer;
|
||
begin
|
||
if AvgMode <= 0 then
|
||
Exit(0.0);
|
||
|
||
FPSv := EnsureRange(FPS, 1, 100);
|
||
if (Backmult > 0.0) and (Backmult < 1.0) then
|
||
Result := -1000.0 / (FPSv * Ln(Backmult))
|
||
else
|
||
Result := (1000.0 * Max(1, AvgCount)) / FPSv;
|
||
|
||
if IsNan(Result) or IsInfinite(Result) or (Result <= 0.0) then
|
||
Result := DefaultMS;
|
||
|
||
Result := EnsureRange(Result, 10.0, 5000.0);
|
||
end;
|
||
|
||
class function TSettingsManager.MacToStr(const MAC: array of Byte): string;
|
||
begin
|
||
Result := Format('%02X:%02X:%02X:%02X:%02X:%02X',
|
||
[MAC[0],MAC[1],MAC[2],MAC[3],MAC[4],MAC[5]]);
|
||
end;
|
||
|
||
class procedure TSettingsManager.DefaultBand(BandIdx: Integer; out B: TBandSettings);
|
||
begin
|
||
FillChar(B, SizeOf(B), 0);
|
||
B.VfoA := CFG_BAND_DEFAULT_FREQ[BandIdx];
|
||
B.VfoB := CFG_BAND_DEFAULT_FREQ[BandIdx];
|
||
B.Mode := CFG_BAND_DEFAULT_MODE[BandIdx];
|
||
B.FilterIdx := 5;
|
||
B.FilterBW := 2700;
|
||
B.AGCMode := 1;
|
||
B.AGCTop := 90;
|
||
B.CTun := False;
|
||
B.CenterHz := 0; // 0 = нет сохранённого центра → центрировать на VFO
|
||
B.SpanHz := 192000;
|
||
B.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;
|
||
end;
|
||
|
||
class procedure TSettingsManager.DefaultGlobal(out G: TGlobalSettings);
|
||
var i: Integer;
|
||
begin
|
||
FillChar(G, SizeOf(G), 0);
|
||
G.Volume := 70; 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.SpecRefLevel := -20.0;
|
||
G.SpecRange := 110.0;
|
||
G.SpecGridStep := 10.0;
|
||
G.AudioOutDevice := '';
|
||
G.AudioSampleRate := 48000;
|
||
G.AudioInDevice := '';
|
||
G.SendAudioToRadio := False;
|
||
G.ShowSpectrum := True;
|
||
G.ShowWaterfall := True;
|
||
G.ShowWideband := False;
|
||
G.WidebandFill := False;
|
||
G.SpectrumFill := True;
|
||
G.DisplayDuplex := False;
|
||
G.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
|
||
end;
|
||
G.CATTcpEnabled := False;
|
||
G.CATTcpPort := 19090;
|
||
// ADC — включены по умолчанию: поддерживаются на Hermes, Angelia, Orion
|
||
G.DitherEnabled := True;
|
||
G.RandomEnabled := True;
|
||
end;
|
||
|
||
constructor TSettingsManager.Create(const FilePath: string);
|
||
begin
|
||
inherited Create;
|
||
if FilePath = '' then
|
||
FFilePath := GetAppCfgDir + SETTINGS_FILE
|
||
else
|
||
FFilePath := FilePath;
|
||
FRoot := TJSONObject.Create;
|
||
end;
|
||
|
||
destructor TSettingsManager.Destroy;
|
||
begin
|
||
FRoot.Free;
|
||
inherited;
|
||
end;
|
||
|
||
procedure TSettingsManager.Load;
|
||
var F: TFileStream; P: TJSONParser; D: TJSONData;
|
||
begin
|
||
if not FileExists(FFilePath) then Exit;
|
||
try
|
||
F := TFileStream.Create(FFilePath, fmOpenRead or fmShareDenyNone);
|
||
try
|
||
P := TJSONParser.Create(F, [joUTF8]);
|
||
try
|
||
D := P.Parse;
|
||
if (D <> nil) and (D is TJSONObject) then
|
||
begin FRoot.Free; FRoot := TJSONObject(D); end
|
||
else
|
||
D.Free;
|
||
finally P.Free; end;
|
||
finally F.Free; end;
|
||
except
|
||
FreeAndNil(FRoot);
|
||
FRoot := TJSONObject.Create;
|
||
end;
|
||
end;
|
||
|
||
procedure TSettingsManager.Save;
|
||
var
|
||
S: string;
|
||
F: TFileStream;
|
||
Buf: TBytes;
|
||
begin
|
||
try
|
||
S := FRoot.FormatJSON([], 2);
|
||
Buf := TEncoding.UTF8.GetBytes(S);
|
||
F := TFileStream.Create(FFilePath, fmCreate);
|
||
try
|
||
if Length(Buf) > 0 then
|
||
F.WriteBuffer(Buf[0], Length(Buf));
|
||
finally F.Free; end;
|
||
except end;
|
||
end;
|
||
|
||
function TSettingsManager.EnsureObj(P: TJSONObject; const K: string): TJSONObject;
|
||
var D: TJSONData; Idx: Integer;
|
||
begin
|
||
D := P.Find(K);
|
||
if (D <> nil) and (D is TJSONObject) then
|
||
Result := TJSONObject(D)
|
||
else
|
||
begin
|
||
if D <> nil then begin Idx := P.IndexOfName(K); if Idx >= 0 then P.Delete(Idx); end;
|
||
Result := TJSONObject.Create;
|
||
P.Add(K, Result);
|
||
end;
|
||
end;
|
||
|
||
function TSettingsManager.GetDevObj(const M: string): TJSONObject;
|
||
begin Result := EnsureObj(FRoot, M); end;
|
||
|
||
function TSettingsManager.GetGlobalObj(D: TJSONObject): TJSONObject;
|
||
begin Result := EnsureObj(D, 'global'); end;
|
||
|
||
function TSettingsManager.GetBandObj(D: TJSONObject; Idx: Integer): TJSONObject;
|
||
begin Result := EnsureObj(EnsureObj(D, 'bands'), IntToStr(Idx)); end;
|
||
|
||
function TSettingsManager.JI(O: TJSONObject; const K: string; Def: Integer): Integer;
|
||
var D: TJSONData;
|
||
begin D := O.Find(K); if D<>nil then try Result:=D.AsInteger; except Result:=Def; end else Result:=Def; end;
|
||
|
||
function TSettingsManager.JD(O: TJSONObject; const K: string; Def: Double): Double;
|
||
var D: TJSONData;
|
||
begin D := O.Find(K); if D<>nil then try Result:=D.AsFloat; except Result:=Def; end else Result:=Def; end;
|
||
|
||
function TSettingsManager.JB(O: TJSONObject; const K: string; Def: Boolean): Boolean;
|
||
var D: TJSONData;
|
||
begin D := O.Find(K); if D<>nil then try Result:=D.AsBoolean; except Result:=Def; end else Result:=Def; end;
|
||
|
||
procedure TSettingsManager.JW(O: TJSONObject; const K: string; V: Integer);
|
||
var Idx: Integer;
|
||
begin
|
||
Idx := O.IndexOfName(K); if Idx >= 0 then O.Delete(Idx);
|
||
O.Add(K, V);
|
||
end;
|
||
|
||
procedure TSettingsManager.JW(O: TJSONObject; const K: string; V: Double);
|
||
var Idx: Integer;
|
||
begin
|
||
Idx := O.IndexOfName(K); if Idx >= 0 then O.Delete(Idx);
|
||
O.Add(K, V);
|
||
end;
|
||
|
||
procedure TSettingsManager.JW(O: TJSONObject; const K: string; V: Boolean);
|
||
var Idx: Integer;
|
||
begin
|
||
Idx := O.IndexOfName(K); if Idx >= 0 then O.Delete(Idx);
|
||
O.Add(K, V);
|
||
end;
|
||
|
||
function TSettingsManager.JS(O: TJSONObject; const K: string; const Def: string): string;
|
||
var D: TJSONData;
|
||
begin D := O.Find(K); if D<>nil then try Result:=D.AsString; except Result:=Def; end else Result:=Def; end;
|
||
|
||
procedure TSettingsManager.JWS(O: TJSONObject; const K: string; const V: string);
|
||
var Idx: Integer;
|
||
begin
|
||
Idx := O.IndexOfName(K); if Idx >= 0 then O.Delete(Idx);
|
||
O.Add(K, V);
|
||
end;
|
||
|
||
function TSettingsManager.LoadDevice(const MAC: array of Byte;
|
||
out G: TGlobalSettings;
|
||
var Bands: array of TBandSettings): Boolean;
|
||
var MacStr: string; DevObj,GObj,BObj: TJSONObject; i: Integer;
|
||
begin
|
||
MacStr := MacToStr(MAC);
|
||
Result := FRoot.Find(MacStr) <> nil;
|
||
DevObj := GetDevObj(MacStr);
|
||
GObj := GetGlobalObj(DevObj);
|
||
|
||
G.Volume := JI(GObj,'volume',70);
|
||
G.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.SpecRefLevel := JD(GObj,'spec_ref_level',-20.0);
|
||
G.SpecRange := JD(GObj,'spec_range',110.0);
|
||
G.SpecGridStep := JD(GObj,'spec_grid_step',10.0);
|
||
// Audio settings
|
||
G.AudioOutDevice := JS(GObj,'audio_out_device','');
|
||
G.AudioSampleRate := JI(GObj,'audio_sample_rate',48000);
|
||
G.AudioInDevice := JS(GObj,'audio_in_device','');
|
||
G.SendAudioToRadio := JB(GObj,'send_audio_to_radio',False);
|
||
// Visibility settings
|
||
G.ShowSpectrum := JB(GObj,'show_spectrum',True);
|
||
G.ShowWaterfall := JB(GObj,'show_waterfall',True);
|
||
G.ShowWideband := JB(GObj,'show_wideband',False);
|
||
G.WidebandFill := JB(GObj,'wideband_fill',False);
|
||
G.SpectrumFill := JB(GObj,'spectrum_fill',True);
|
||
G.DisplayDuplex := JB(GObj,'display_duplex',False);
|
||
G.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);
|
||
end;
|
||
G.CATTcpEnabled := JB(GObj,'cat_tcp_enabled', False);
|
||
G.CATTcpPort := JI(GObj,'cat_tcp_port', 19090);
|
||
// ADC settings
|
||
G.DitherEnabled := JB(GObj,'dither_enabled', True);
|
||
G.RandomEnabled := JB(GObj,'random_enabled', True);
|
||
|
||
for i := 0 to CFG_BAND_COUNT-1 do
|
||
begin
|
||
DefaultBand(i, Bands[i]);
|
||
BObj := GetBandObj(DevObj, i);
|
||
Bands[i].VfoA := JD(BObj,'vfo_a', Bands[i].VfoA);
|
||
Bands[i].VfoB := JD(BObj,'vfo_b', Bands[i].VfoB);
|
||
Bands[i].Mode := JI(BObj,'mode', Bands[i].Mode);
|
||
Bands[i].FilterIdx := JI(BObj,'filter_idx', Bands[i].FilterIdx);
|
||
Bands[i].FilterBW := JI(BObj,'filter_bw', Bands[i].FilterBW);
|
||
Bands[i].AGCMode := JI(BObj,'agc_mode', Bands[i].AGCMode);
|
||
Bands[i].AGCTop := JI(BObj,'agc_top', Bands[i].AGCTop);
|
||
Bands[i].CTun := JB(BObj,'ctun', Bands[i].CTun);
|
||
Bands[i].CenterHz := JD(BObj,'center_hz', Bands[i].CenterHz);
|
||
Bands[i].SpanHz := JD(BObj,'span_hz', Bands[i].SpanHz);
|
||
// FM squelch / CTCSS / step / repeater — как в LoadBand (раньше пропускались
|
||
// здесь → на старте сбрасывались в дефолт, хотя SaveBand их пишет).
|
||
Bands[i].FMSQOn := JB(BObj,'fmsq_on', Bands[i].FMSQOn);
|
||
Bands[i].FMSQLevel := JI(BObj,'fmsq_level', Bands[i].FMSQLevel);
|
||
Bands[i].CTCSSOn := JB(BObj,'ctcss_on', Bands[i].CTCSSOn);
|
||
Bands[i].CTCSSToneIdx := EnsureRange(JI(BObj,'ctcss_idx', Bands[i].CTCSSToneIdx), 0, 37);
|
||
Bands[i].FMStepOn := JB(BObj,'fmstep_on', Bands[i].FMStepOn);
|
||
Bands[i].FMStepIdx := EnsureRange(JI(BObj,'fmstep_idx', Bands[i].FMStepIdx), 0, 3);
|
||
Bands[i].FMRptDir := EnsureRange(JI(BObj,'fmrpt_dir', Bands[i].FMRptDir), 0, 2);
|
||
Bands[i].FMRptOffsetHz := JD(BObj,'fmrpt_offset', Bands[i].FMRptOffsetHz);
|
||
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,'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,'spec_ref_level',G.SpecRefLevel);
|
||
JW(O,'spec_range',G.SpecRange);
|
||
JW(O,'spec_grid_step',G.SpecGridStep);
|
||
// Audio settings
|
||
JWS(O,'audio_out_device',G.AudioOutDevice);
|
||
JW(O,'audio_sample_rate',G.AudioSampleRate);
|
||
JWS(O,'audio_in_device',G.AudioInDevice);
|
||
JW(O,'send_audio_to_radio',G.SendAudioToRadio);
|
||
// Visibility settings
|
||
JW(O,'show_spectrum',G.ShowSpectrum);
|
||
JW(O,'show_waterfall',G.ShowWaterfall);
|
||
JW(O,'show_wideband',G.ShowWideband);
|
||
JW(O,'wideband_fill',G.WidebandFill);
|
||
JW(O,'spectrum_fill',G.SpectrumFill);
|
||
JW(O,'display_duplex',G.DisplayDuplex);
|
||
JW(O,'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]);
|
||
end;
|
||
JW(O,'cat_tcp_enabled',G.CATTcpEnabled);
|
||
JW(O,'cat_tcp_port',G.CATTcpPort);
|
||
// ADC settings
|
||
JW(O,'dither_enabled',G.DitherEnabled);
|
||
JW(O,'random_enabled',G.RandomEnabled);
|
||
end;
|
||
|
||
procedure TSettingsManager.SaveBand(const MAC: array of Byte; BandIdx: Integer;
|
||
const B: TBandSettings);
|
||
var O: TJSONObject;
|
||
begin
|
||
if (BandIdx < 0) or (BandIdx >= CFG_BAND_COUNT) then Exit;
|
||
O := GetBandObj(GetDevObj(MacToStr(MAC)), BandIdx);
|
||
JW(O,'vfo_a',B.VfoA); JW(O,'vfo_b',B.VfoB);
|
||
JW(O,'mode',B.Mode);
|
||
JW(O,'filter_idx',B.FilterIdx); JW(O,'filter_bw',B.FilterBW);
|
||
JW(O,'agc_mode',B.AGCMode); JW(O,'agc_top',B.AGCTop);
|
||
JW(O,'ctun',B.CTun); JW(O,'center_hz',B.CenterHz); JW(O,'span_hz',B.SpanHz);
|
||
JW(O,'fmsq_on',B.FMSQOn); JW(O,'fmsq_level',B.FMSQLevel);
|
||
JW(O,'ctcss_on',B.CTCSSOn); JW(O,'ctcss_idx',B.CTCSSToneIdx);
|
||
JW(O,'fmstep_on',B.FMStepOn); JW(O,'fmstep_idx',B.FMStepIdx);
|
||
JW(O,'fmrpt_dir',B.FMRptDir); JW(O,'fmrpt_offset',B.FMRptOffsetHz);
|
||
end;
|
||
|
||
function TSettingsManager.LoadBand(const MAC: array of Byte; BandIdx: Integer;
|
||
out B: TBandSettings): Boolean;
|
||
var MacStr: string; O: TJSONObject;
|
||
begin
|
||
DefaultBand(BandIdx, B);
|
||
MacStr := MacToStr(MAC);
|
||
Result := FRoot.Find(MacStr) <> nil;
|
||
if not Result then Exit;
|
||
O := GetBandObj(GetDevObj(MacStr), BandIdx);
|
||
B.VfoA := JD(O,'vfo_a', B.VfoA);
|
||
B.VfoB := JD(O,'vfo_b', B.VfoB);
|
||
B.Mode := JI(O,'mode', B.Mode);
|
||
B.FilterIdx := JI(O,'filter_idx', B.FilterIdx);
|
||
B.FilterBW := JI(O,'filter_bw', B.FilterBW);
|
||
B.AGCMode := JI(O,'agc_mode', B.AGCMode);
|
||
B.AGCTop := JI(O,'agc_top', B.AGCTop);
|
||
B.CTun := JB(O,'ctun', B.CTun);
|
||
B.CenterHz := JD(O,'center_hz', B.CenterHz);
|
||
B.SpanHz := JD(O,'span_hz', B.SpanHz);
|
||
B.FMSQOn := JB(O,'fmsq_on', B.FMSQOn);
|
||
B.FMSQLevel := JI(O,'fmsq_level', B.FMSQLevel);
|
||
B.CTCSSOn := JB(O,'ctcss_on', B.CTCSSOn);
|
||
B.CTCSSToneIdx := EnsureRange(JI(O,'ctcss_idx', B.CTCSSToneIdx), 0, 37);
|
||
B.FMStepOn := JB(O,'fmstep_on', B.FMStepOn);
|
||
B.FMStepIdx := EnsureRange(JI(O,'fmstep_idx', B.FMStepIdx), 0, 3);
|
||
B.FMRptDir := EnsureRange(JI(O,'fmrpt_dir', B.FMRptDir), 0, 2);
|
||
B.FMRptOffsetHz := JD(O,'fmrpt_offset', B.FMRptOffsetHz);
|
||
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]);
|
||
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);
|
||
end;
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// TX settings (per-device, JSON-секция "tx" под MAC)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
class procedure TSettingsManager.DefaultTX(out T: TTXSettings);
|
||
var
|
||
i: Integer;
|
||
const
|
||
// 10-полосный голосовой EQ (Hz). [0]=preamp slot, [1..10]=bands.
|
||
// Подобрано под голос, а не под музыку: больше точек в 100..3500 Hz.
|
||
DefaultEQFreqs: array[0..10] of Double = (
|
||
0, // [0] preamp slot — частота не используется
|
||
100, 200, 400, 700, 1100, 1500, 2000, 2500, 3000, 3500);
|
||
begin
|
||
FillChar(T, SizeOf(T), 0);
|
||
T.MicLineIn := False;
|
||
T.MicBoost := False;
|
||
T.MicPTTEnabled := False;
|
||
T.MicTipRing := False; // Tip (mic at Tip, PTT at Ring)
|
||
T.MicBias := True;
|
||
T.LineInGainDB := 0.0; // 0 dB
|
||
T.MicGainDB := 10.0;
|
||
// ATT on TX — Thetis default 31 dB (полная защита RX-ADC во время передачи)
|
||
T.AttOnTX := 31;
|
||
// Tune — Thetis defaults
|
||
T.TUNLevel := 10;
|
||
T.TUNFreq := 750;
|
||
// SSB-фильтр по умолчанию
|
||
T.FilterLow := 100;
|
||
T.FilterHigh := 2800;
|
||
T.FilterNC := 1024;
|
||
T.FilterMP := 1; // Low Latency (как в piHPSDR)
|
||
T.FilterWindow := 1; // BH4 / Blackman-Harris
|
||
// Compressor — выкл по умолчанию
|
||
T.CompressorOn := False;
|
||
T.CompressorGain:= 5.0;
|
||
// Leveler — вкл, типичные значения piHPSDR/Thetis
|
||
T.LevelerOn := True;
|
||
T.LevelerTop := 5.0;
|
||
T.LevelerDecay := 500;
|
||
// ALC — вкл всегда (защита от перегруза DAC)
|
||
T.ALCOn := True;
|
||
T.ALCMaxGain := 0.0;
|
||
T.ALCDecay := 10;
|
||
// Phase Rotator — выкл
|
||
T.PhaseRotOn := False;
|
||
T.PhaseRotStages:= 8;
|
||
T.PhaseRotFreq := 338.0;
|
||
// EQ — выкл, нулевые гейны
|
||
T.EQOn := False;
|
||
T.EQNumBands := 10;
|
||
for i := 0 to 10 do
|
||
begin
|
||
T.EQGains[i] := 0.0;
|
||
T.EQFreqs[i] := DefaultEQFreqs[i];
|
||
end;
|
||
// AM
|
||
T.AMCarrierLevel := 0.5;
|
||
// FM
|
||
T.FMDeviation := 5000.0;
|
||
T.FMLowCut := 300;
|
||
T.FMHighCut := 3000;
|
||
T.FMEmphPosition := 1; // PostEQ
|
||
// CTCSS
|
||
T.CTCSSOn := False;
|
||
T.CTCSSFreq := 100.0;
|
||
// TX Display defaults — те же что у RX (см. DefaultGlobal)
|
||
T.TXFFTSize := 16384; // меньше чем RX — TX-полоса узкая, FFT не нужен большой
|
||
T.TXWindowType := 2; // Hann
|
||
T.TXSpecDetector := 0; // Peak
|
||
T.TXSpecAvgMode := 3; // Log Recursive
|
||
T.TXSpecAvgTimeMS := 30.0;
|
||
T.TXWfDetector := 0;
|
||
T.TXWfAvgMode := 3;
|
||
T.TXWfAvgTimeMS := 120.0;
|
||
// TX Grid — потолок 0 dBm (TX-уровни выше RX), 80 dB динамики, шаг 10 dB
|
||
T.TXSpecRefLevel := 0.0;
|
||
T.TXSpecRange := 80.0;
|
||
T.TXSpecGridStep := 10.0;
|
||
end;
|
||
|
||
function TSettingsManager.LoadTX(const MAC: array of Byte;
|
||
out T: TTXSettings): Boolean;
|
||
var MacStr: string; DevObj, O: TJSONObject; i: Integer;
|
||
begin
|
||
DefaultTX(T);
|
||
MacStr := MacToStr(MAC);
|
||
Result := FRoot.Find(MacStr) <> nil;
|
||
if not Result then Exit;
|
||
DevObj := GetDevObj(MacStr);
|
||
// Если секции "tx" ещё нет — оставляем дефолты, не материализуем пустую секцию
|
||
if DevObj.Find('tx') = nil then Exit;
|
||
O := EnsureObj(DevObj, 'tx');
|
||
|
||
T.MicLineIn := JB(O,'mic_line_in', T.MicLineIn);
|
||
T.MicBoost := JB(O,'mic_boost', T.MicBoost);
|
||
T.MicPTTEnabled := JB(O,'mic_ptt_enabled', T.MicPTTEnabled);
|
||
T.MicTipRing := JB(O,'mic_tip_ring', T.MicTipRing);
|
||
T.MicBias := JB(O,'mic_bias', T.MicBias);
|
||
T.LineInGainDB := EnsureRange(JD(O,'line_in_gain_db', T.LineInGainDB), -34.5, 12.0);
|
||
T.MicGainDB := JD(O,'mic_gain_db', T.MicGainDB);
|
||
T.AttOnTX := EnsureRange(JI(O,'att_on_tx', T.AttOnTX), 0, 31);
|
||
T.TUNLevel := EnsureRange(JI(O,'tun_level', T.TUNLevel), 0, 100);
|
||
T.TUNFreq := EnsureRange(JI(O,'tun_freq', T.TUNFreq), 100, 5000);
|
||
T.FilterLow := JI(O,'filter_low', T.FilterLow);
|
||
T.FilterHigh := JI(O,'filter_high', T.FilterHigh);
|
||
T.FilterNC := JI(O,'filter_nc', T.FilterNC);
|
||
T.FilterMP := JI(O,'filter_mp', T.FilterMP);
|
||
T.FilterWindow := JI(O,'filter_window', T.FilterWindow);
|
||
T.CompressorOn := JB(O,'compressor_on', T.CompressorOn);
|
||
T.CompressorGain := JD(O,'compressor_gain', T.CompressorGain);
|
||
T.LevelerOn := JB(O,'leveler_on', T.LevelerOn);
|
||
T.LevelerTop := JD(O,'leveler_top', T.LevelerTop);
|
||
T.LevelerDecay := JI(O,'leveler_decay', T.LevelerDecay);
|
||
T.ALCOn := JB(O,'alc_on', T.ALCOn);
|
||
T.ALCMaxGain := JD(O,'alc_max_gain', T.ALCMaxGain);
|
||
T.ALCDecay := JI(O,'alc_decay', T.ALCDecay);
|
||
T.PhaseRotOn := JB(O,'phase_rot_on', T.PhaseRotOn);
|
||
T.PhaseRotStages := JI(O,'phase_rot_stages',T.PhaseRotStages);
|
||
T.PhaseRotFreq := JD(O,'phase_rot_freq', T.PhaseRotFreq);
|
||
T.EQOn := JB(O,'eq_on', T.EQOn);
|
||
T.EQNumBands := JI(O,'eq_num_bands', T.EQNumBands);
|
||
for i := 0 to 10 do
|
||
begin
|
||
T.EQGains[i] := JD(O,'eq_g_'+IntToStr(i), T.EQGains[i]);
|
||
T.EQFreqs[i] := JD(O,'eq_f_'+IntToStr(i), T.EQFreqs[i]);
|
||
end;
|
||
T.AMCarrierLevel := JD(O,'am_carrier_level',T.AMCarrierLevel);
|
||
T.FMDeviation := JD(O,'fm_deviation', T.FMDeviation);
|
||
T.FMLowCut := JI(O,'fm_low_cut', T.FMLowCut);
|
||
T.FMHighCut := JI(O,'fm_high_cut', T.FMHighCut);
|
||
T.FMEmphPosition := JI(O,'fm_emph_position',T.FMEmphPosition);
|
||
T.CTCSSOn := JB(O,'ctcss_on', T.CTCSSOn);
|
||
T.CTCSSFreq := JD(O,'ctcss_freq', T.CTCSSFreq);
|
||
T.TXFFTSize := JI(O,'tx_fft_size', T.TXFFTSize);
|
||
T.TXWindowType := JI(O,'tx_win_type', T.TXWindowType);
|
||
T.TXSpecDetector := JI(O,'tx_spec_detector', T.TXSpecDetector);
|
||
T.TXSpecAvgMode := JI(O,'tx_spec_avg_mode', T.TXSpecAvgMode);
|
||
T.TXSpecAvgTimeMS := JD(O,'tx_spec_avg_time_ms', T.TXSpecAvgTimeMS);
|
||
T.TXWfDetector := JI(O,'tx_wf_detector', T.TXWfDetector);
|
||
T.TXWfAvgMode := JI(O,'tx_wf_avg_mode', T.TXWfAvgMode);
|
||
T.TXWfAvgTimeMS := JD(O,'tx_wf_avg_time_ms', T.TXWfAvgTimeMS);
|
||
T.TXSpecRefLevel := JD(O,'tx_spec_ref_level', T.TXSpecRefLevel);
|
||
T.TXSpecRange := JD(O,'tx_spec_range', T.TXSpecRange);
|
||
T.TXSpecGridStep := JD(O,'tx_spec_grid_step', T.TXSpecGridStep);
|
||
end;
|
||
|
||
procedure TSettingsManager.SaveTX(const MAC: array of Byte;
|
||
const T: TTXSettings);
|
||
var O: TJSONObject; i: Integer;
|
||
begin
|
||
O := EnsureObj(GetDevObj(MacToStr(MAC)), 'tx');
|
||
JW(O,'mic_line_in', T.MicLineIn);
|
||
JW(O,'mic_boost', T.MicBoost);
|
||
JW(O,'mic_ptt_enabled', T.MicPTTEnabled);
|
||
JW(O,'mic_tip_ring', T.MicTipRing);
|
||
JW(O,'mic_bias', T.MicBias);
|
||
JW(O,'line_in_gain_db', T.LineInGainDB);
|
||
JW(O,'mic_gain_db', T.MicGainDB);
|
||
JW(O,'att_on_tx', T.AttOnTX);
|
||
JW(O,'tun_level', T.TUNLevel);
|
||
JW(O,'tun_freq', T.TUNFreq);
|
||
JW(O,'filter_low', T.FilterLow);
|
||
JW(O,'filter_high', T.FilterHigh);
|
||
JW(O,'filter_nc', T.FilterNC);
|
||
JW(O,'filter_mp', T.FilterMP);
|
||
JW(O,'filter_window', T.FilterWindow);
|
||
JW(O,'compressor_on', T.CompressorOn);
|
||
JW(O,'compressor_gain', T.CompressorGain);
|
||
JW(O,'leveler_on', T.LevelerOn);
|
||
JW(O,'leveler_top', T.LevelerTop);
|
||
JW(O,'leveler_decay', T.LevelerDecay);
|
||
JW(O,'alc_on', T.ALCOn);
|
||
JW(O,'alc_max_gain', T.ALCMaxGain);
|
||
JW(O,'alc_decay', T.ALCDecay);
|
||
JW(O,'phase_rot_on', T.PhaseRotOn);
|
||
JW(O,'phase_rot_stages',T.PhaseRotStages);
|
||
JW(O,'phase_rot_freq', T.PhaseRotFreq);
|
||
JW(O,'eq_on', T.EQOn);
|
||
JW(O,'eq_num_bands', T.EQNumBands);
|
||
for i := 0 to 10 do
|
||
begin
|
||
JW(O,'eq_g_'+IntToStr(i), T.EQGains[i]);
|
||
JW(O,'eq_f_'+IntToStr(i), T.EQFreqs[i]);
|
||
end;
|
||
JW(O,'am_carrier_level',T.AMCarrierLevel);
|
||
JW(O,'fm_deviation', T.FMDeviation);
|
||
JW(O,'fm_low_cut', T.FMLowCut);
|
||
JW(O,'fm_high_cut', T.FMHighCut);
|
||
JW(O,'fm_emph_position',T.FMEmphPosition);
|
||
JW(O,'ctcss_on', T.CTCSSOn);
|
||
JW(O,'ctcss_freq', T.CTCSSFreq);
|
||
JW(O,'tx_fft_size', T.TXFFTSize);
|
||
JW(O,'tx_win_type', T.TXWindowType);
|
||
JW(O,'tx_spec_detector', T.TXSpecDetector);
|
||
JW(O,'tx_spec_avg_mode', T.TXSpecAvgMode);
|
||
JW(O,'tx_spec_avg_time_ms', T.TXSpecAvgTimeMS);
|
||
JW(O,'tx_wf_detector', T.TXWfDetector);
|
||
JW(O,'tx_wf_avg_mode', T.TXWfAvgMode);
|
||
JW(O,'tx_wf_avg_time_ms', T.TXWfAvgTimeMS);
|
||
JW(O,'tx_spec_ref_level', T.TXSpecRefLevel);
|
||
JW(O,'tx_spec_range', T.TXSpecRange);
|
||
JW(O,'tx_spec_grid_step', T.TXSpecGridStep);
|
||
end;
|
||
|
||
class procedure TSettingsManager.DefaultAlex(out A: TAlexSettings);
|
||
var i: Integer;
|
||
begin
|
||
FillChar(A, SizeOf(A), 0);
|
||
for i := 0 to CFG_BAND_COUNT - 1 do
|
||
begin
|
||
A.RxAnt[i] := 1; // ANT1 — безопасный дефолт
|
||
A.TxAnt[i] := 1; // ANT1
|
||
A.RxOnly[i] := 0; // вторичный вход выключен
|
||
A.DoNotTx[i] := False;
|
||
end;
|
||
A.RxBypassOnTx := False;
|
||
A.Ext1OnTx := False;
|
||
A.GndBpf2OnTx := False;
|
||
A.EnableXvtrHf := False;
|
||
end;
|
||
|
||
function TSettingsManager.LoadAlex(const MAC: array of Byte;
|
||
out A: TAlexSettings): Boolean;
|
||
var MacStr: string; DevObj, O: TJSONObject; i: Integer;
|
||
begin
|
||
DefaultAlex(A);
|
||
MacStr := MacToStr(MAC);
|
||
Result := FRoot.Find(MacStr) <> nil;
|
||
if not Result then Exit;
|
||
DevObj := GetDevObj(MacStr);
|
||
if DevObj.Find('alex') = nil then Exit;
|
||
O := EnsureObj(DevObj, 'alex');
|
||
for i := 0 to CFG_BAND_COUNT - 1 do
|
||
begin
|
||
A.RxAnt[i] := EnsureRange(JI(O, 'rx_ant_' + IntToStr(i), 1), 1, 3);
|
||
A.TxAnt[i] := EnsureRange(JI(O, 'tx_ant_' + IntToStr(i), 1), 1, 3);
|
||
A.RxOnly[i] := EnsureRange(JI(O, 'rx_only_' + IntToStr(i), 0), 0, 3);
|
||
A.DoNotTx[i] := JB(O, 'do_not_tx_' + IntToStr(i), False);
|
||
end;
|
||
A.RxBypassOnTx := JB(O, 'rx_bypass_on_tx', False);
|
||
A.Ext1OnTx := JB(O, 'ext1_on_tx', False);
|
||
A.GndBpf2OnTx := JB(O, 'gnd_bpf2_on_tx', False);
|
||
A.EnableXvtrHf := JB(O, 'enable_xvtr_hf', False);
|
||
end;
|
||
|
||
procedure TSettingsManager.SaveAlex(const MAC: array of Byte;
|
||
const A: TAlexSettings);
|
||
var O: TJSONObject; i: Integer;
|
||
begin
|
||
O := EnsureObj(GetDevObj(MacToStr(MAC)), 'alex');
|
||
for i := 0 to CFG_BAND_COUNT - 1 do
|
||
begin
|
||
JW(O, 'rx_ant_' + IntToStr(i), Integer(A.RxAnt[i]));
|
||
JW(O, 'tx_ant_' + IntToStr(i), Integer(A.TxAnt[i]));
|
||
JW(O, 'rx_only_' + IntToStr(i), Integer(A.RxOnly[i]));
|
||
JW(O, 'do_not_tx_' + IntToStr(i), A.DoNotTx[i]);
|
||
end;
|
||
JW(O, 'rx_bypass_on_tx', A.RxBypassOnTx);
|
||
JW(O, 'ext1_on_tx', A.Ext1OnTx);
|
||
JW(O, 'gnd_bpf2_on_tx', A.GndBpf2OnTx);
|
||
JW(O, 'enable_xvtr_hf', A.EnableXvtrHf);
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// XVTR settings (per-device, JSON-секция "xvtr" под MAC)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
class procedure TSettingsManager.DefaultXvtr(out X: TXvtrSettings);
|
||
var i: Integer;
|
||
begin
|
||
FillChar(X, SizeOf(X), 0);
|
||
X.UseXVTRTunePower := False;
|
||
// Все слоты по умолчанию disabled. Первые два предзаполнены типичными
|
||
// значениями (2m с LO=116MHz, 70cm с LO=404MHz) — чтобы пользователь
|
||
// мог быстро включить.
|
||
for i := 0 to CFG_XVTR_COUNT - 1 do
|
||
begin
|
||
X.Entries[i].Enabled := False;
|
||
X.Entries[i].ButtonText := '';
|
||
X.Entries[i].FreqBegin := 0.0;
|
||
X.Entries[i].FreqEnd := 0.0;
|
||
X.Entries[i].LOOffset := 0.0;
|
||
X.Entries[i].LOError := 0.0;
|
||
X.Entries[i].FullDuplexTx := False;
|
||
X.Entries[i].TxLOOffset := 0.0;
|
||
X.Entries[i].RXGain := 0.0;
|
||
X.Entries[i].RXOnly := False;
|
||
X.Entries[i].TXPower := 100;
|
||
X.Entries[i].DisablePA := False;
|
||
X.Entries[i].RXAntenna := 0;
|
||
X.Entries[i].LastFreq := 0.0;
|
||
X.Entries[i].LastFreqB := 0.0;
|
||
X.Entries[i].LastMode := 1; // USB — VHF SSB calling default
|
||
X.Entries[i].LastFilterIdx := 0; // NFM
|
||
X.Entries[i].LastFMSQOn := False;
|
||
X.Entries[i].LastFMSQLevel := 30;
|
||
X.Entries[i].LastCTCSSOn := False;
|
||
X.Entries[i].LastCTCSSToneIdx := 0;
|
||
X.Entries[i].LastFMStepOn := True;
|
||
X.Entries[i].LastFMStepIdx := 3; // 25 kHz
|
||
X.Entries[i].LastCTun := False;
|
||
X.Entries[i].LastCenterHz := 0.0;
|
||
X.Entries[i].LastAGCMode := 1;
|
||
X.Entries[i].LastAGCTop := 90;
|
||
X.Entries[i].LastFMRptDir := 0;
|
||
X.Entries[i].LastFMRptOffsetHz := 0.0;
|
||
X.Entries[i].LastCTCSSAutoActive := False;
|
||
X.Entries[i].LastFMRptAutoActive := False;
|
||
end;
|
||
// Шаблоны (пользователь должен сам Enable, чтобы трансверторы появились)
|
||
X.Entries[0].ButtonText := '2m';
|
||
X.Entries[0].FreqBegin := 144000000;
|
||
X.Entries[0].FreqEnd := 148000000;
|
||
X.Entries[0].LOOffset := 116000000;
|
||
X.Entries[0].LastFreq := 144300000;
|
||
X.Entries[0].LastFreqB := 145500000;
|
||
X.Entries[1].ButtonText := '70cm';
|
||
X.Entries[1].FreqBegin := 430000000;
|
||
X.Entries[1].FreqEnd := 440000000;
|
||
X.Entries[1].LOOffset := 404000000;
|
||
X.Entries[1].LastFreq := 432200000;
|
||
X.Entries[1].LastFreqB := 433000000;
|
||
// QO-100 (Es'hail-2) NB downlink: 10489.500–10490.000 МГц, LO 9750 МГц.
|
||
// RX-трансвертер: tune = downlink − LO. Калибровать LOError по маяку.
|
||
// QO-100 — в зарезервированном последнем слоте (отдельная страница настроек).
|
||
X.Entries[QO100_SLOT].ButtonText := 'QO-100';
|
||
X.Entries[QO100_SLOT].FreqBegin := 10489500000;
|
||
X.Entries[QO100_SLOT].FreqEnd := 10490000000;
|
||
X.Entries[QO100_SLOT].LOOffset := 9750000000; // LNB LO
|
||
X.Entries[QO100_SLOT].FullDuplexTx := True; // раздельный TX-LO + full-duplex
|
||
X.Entries[QO100_SLOT].TxLOOffset := 8089500000; // транспондер-offset (downlink-uplink)
|
||
X.Entries[QO100_SLOT].LastFreq := 10489750000;
|
||
X.Entries[QO100_SLOT].LastFreqB := 10489750000;
|
||
end;
|
||
|
||
function TSettingsManager.LoadXvtr(const MAC: array of Byte;
|
||
out X: TXvtrSettings): Boolean;
|
||
var
|
||
MacStr: string;
|
||
DevObj, Root, EObj: TJSONObject;
|
||
i: Integer;
|
||
begin
|
||
DefaultXvtr(X);
|
||
MacStr := MacToStr(MAC);
|
||
Result := FRoot.Find(MacStr) <> nil;
|
||
if not Result then Exit;
|
||
DevObj := GetDevObj(MacStr);
|
||
if DevObj.Find('xvtr') = nil then Exit;
|
||
Root := EnsureObj(DevObj, 'xvtr');
|
||
X.UseXVTRTunePower := JB(Root, 'use_tun_power', False);
|
||
for i := 0 to CFG_XVTR_COUNT - 1 do
|
||
begin
|
||
if Root.Find('e' + IntToStr(i)) = nil then Continue;
|
||
EObj := EnsureObj(Root, 'e' + IntToStr(i));
|
||
X.Entries[i].Enabled := JB(EObj, 'en', X.Entries[i].Enabled);
|
||
X.Entries[i].ButtonText := JS(EObj, 'name', X.Entries[i].ButtonText);
|
||
X.Entries[i].FreqBegin := JD(EObj, 'fbeg', X.Entries[i].FreqBegin);
|
||
X.Entries[i].FreqEnd := JD(EObj, 'fend', X.Entries[i].FreqEnd);
|
||
X.Entries[i].LOOffset := JD(EObj, 'lo_off', X.Entries[i].LOOffset);
|
||
X.Entries[i].LOError := JD(EObj, 'lo_err', X.Entries[i].LOError);
|
||
X.Entries[i].FullDuplexTx := JB(EObj, 'full_dup', X.Entries[i].FullDuplexTx);
|
||
X.Entries[i].TxLOOffset := JD(EObj, 'tx_lo_off', X.Entries[i].TxLOOffset);
|
||
X.Entries[i].RXGain := JD(EObj, 'rx_gain', X.Entries[i].RXGain);
|
||
X.Entries[i].RXOnly := JB(EObj, 'rx_only', X.Entries[i].RXOnly);
|
||
X.Entries[i].TXPower := EnsureRange(JI(EObj, 'tx_pwr', X.Entries[i].TXPower), 0, 100);
|
||
X.Entries[i].DisablePA := JB(EObj, 'dis_pa', X.Entries[i].DisablePA);
|
||
X.Entries[i].RXAntenna := EnsureRange(JI(EObj, 'rx_ant', X.Entries[i].RXAntenna), 0, 3);
|
||
X.Entries[i].LastFreq := JD(EObj, 'last_freq', X.Entries[i].LastFreq);
|
||
X.Entries[i].LastFreqB := JD(EObj, 'last_freq_b', X.Entries[i].LastFreqB);
|
||
X.Entries[i].LastMode := EnsureRange(JI(EObj, 'last_mode', X.Entries[i].LastMode), 0, 7);
|
||
X.Entries[i].LastFilterIdx := EnsureRange(JI(EObj, 'last_filt', X.Entries[i].LastFilterIdx), 0, 9);
|
||
X.Entries[i].LastFMSQOn := JB(EObj, 'last_fmsq_on', X.Entries[i].LastFMSQOn);
|
||
X.Entries[i].LastFMSQLevel := EnsureRange(JI(EObj, 'last_fmsq_lvl', X.Entries[i].LastFMSQLevel), 0, 100);
|
||
X.Entries[i].LastCTCSSOn := JB(EObj, 'last_ctcss_on', X.Entries[i].LastCTCSSOn);
|
||
X.Entries[i].LastCTCSSToneIdx := EnsureRange(JI(EObj, 'last_ctcss_idx', X.Entries[i].LastCTCSSToneIdx), 0, 37);
|
||
X.Entries[i].LastFMStepOn := JB(EObj, 'last_fmstep_on', X.Entries[i].LastFMStepOn);
|
||
X.Entries[i].LastFMStepIdx := EnsureRange(JI(EObj, 'last_fmstep_idx', X.Entries[i].LastFMStepIdx), 0, 3);
|
||
X.Entries[i].LastCTun := JB(EObj, 'last_ctun', X.Entries[i].LastCTun);
|
||
X.Entries[i].LastCenterHz := JD(EObj, 'last_center_hz', X.Entries[i].LastCenterHz);
|
||
X.Entries[i].LastAGCMode := EnsureRange(JI(EObj, 'last_agc_mode', X.Entries[i].LastAGCMode), 0, 4);
|
||
X.Entries[i].LastAGCTop := EnsureRange(JI(EObj, 'last_agc_top', X.Entries[i].LastAGCTop), 0, 120);
|
||
X.Entries[i].LastFMRptDir := EnsureRange(JI(EObj, 'last_fmrpt_dir', X.Entries[i].LastFMRptDir), 0, 2);
|
||
X.Entries[i].LastFMRptOffsetHz := JD(EObj, 'last_fmrpt_offset', X.Entries[i].LastFMRptOffsetHz);
|
||
X.Entries[i].LastCTCSSAutoActive := JB(EObj, 'last_ctcss_auto', X.Entries[i].LastCTCSSAutoActive);
|
||
X.Entries[i].LastFMRptAutoActive := JB(EObj, 'last_fmrpt_auto', X.Entries[i].LastFMRptAutoActive);
|
||
// Валидация: Last freqs должны быть в [FreqBegin..FreqEnd]
|
||
if (X.Entries[i].LastFreq < X.Entries[i].FreqBegin) or
|
||
(X.Entries[i].LastFreq > X.Entries[i].FreqEnd) then
|
||
X.Entries[i].LastFreq := (X.Entries[i].FreqBegin + X.Entries[i].FreqEnd) / 2.0;
|
||
if (X.Entries[i].LastFreqB < X.Entries[i].FreqBegin) or
|
||
(X.Entries[i].LastFreqB > X.Entries[i].FreqEnd) then
|
||
X.Entries[i].LastFreqB := (X.Entries[i].FreqBegin + X.Entries[i].FreqEnd) / 2.0;
|
||
end;
|
||
// QO-100 закреплён за зарезервированным слотом QO100_SLOT. Восстанавливаем
|
||
// шаблон, если слот не размечен как split-LO транспондер ИЛИ значения пустые
|
||
// (старый конфиг сохранял слот как обычный пустой XVTR с нулями — тогда
|
||
// FullDuplexTx по умолчанию True, но LO/offset == 0). Реальный QO-100 всегда
|
||
// имеет положительный транспондер-offset, так что валидный конфиг не трогаем.
|
||
if (not X.Entries[QO100_SLOT].FullDuplexTx)
|
||
or (X.Entries[QO100_SLOT].TxLOOffset <= 0)
|
||
or (X.Entries[QO100_SLOT].LOOffset <= 0) then
|
||
begin
|
||
X.Entries[QO100_SLOT].Enabled := False;
|
||
X.Entries[QO100_SLOT].ButtonText := 'QO-100';
|
||
X.Entries[QO100_SLOT].FreqBegin := 10489500000;
|
||
X.Entries[QO100_SLOT].FreqEnd := 10490000000;
|
||
X.Entries[QO100_SLOT].LOOffset := 9750000000;
|
||
X.Entries[QO100_SLOT].FullDuplexTx := True;
|
||
X.Entries[QO100_SLOT].TxLOOffset := 8089500000;
|
||
X.Entries[QO100_SLOT].LastFreq := 10489750000;
|
||
X.Entries[QO100_SLOT].LastFreqB := 10489750000;
|
||
end;
|
||
end;
|
||
|
||
procedure TSettingsManager.MigrateXvtrTemplate(var X: TXvtrSettings;
|
||
const AName: string; ABegin, AEnd, ALO: Double;
|
||
AFullDup: Boolean = False; ATxLO: Double = 0.0);
|
||
var
|
||
i, freeIdx: Integer;
|
||
begin
|
||
freeIdx := -1;
|
||
for i := 0 to CFG_XVTR_COUNT - 1 do
|
||
begin
|
||
if SameText(X.Entries[i].ButtonText, AName) then Exit; // уже есть — ничего не делаем
|
||
if (freeIdx < 0) and (not X.Entries[i].Enabled) and
|
||
(Trim(X.Entries[i].ButtonText) = '') then
|
||
freeIdx := i;
|
||
end;
|
||
if freeIdx < 0 then Exit; // нет свободных слотов
|
||
X.Entries[freeIdx].ButtonText := AName;
|
||
X.Entries[freeIdx].FreqBegin := ABegin;
|
||
X.Entries[freeIdx].FreqEnd := AEnd;
|
||
X.Entries[freeIdx].LOOffset := ALO;
|
||
X.Entries[freeIdx].FullDuplexTx := AFullDup;
|
||
X.Entries[freeIdx].TxLOOffset := ATxLO;
|
||
X.Entries[freeIdx].LastFreq := (ABegin + AEnd) / 2.0;
|
||
X.Entries[freeIdx].LastFreqB := (ABegin + AEnd) / 2.0;
|
||
end;
|
||
|
||
procedure TSettingsManager.SaveXvtr(const MAC: array of Byte;
|
||
const X: TXvtrSettings);
|
||
var
|
||
Root, EObj: TJSONObject;
|
||
i: Integer;
|
||
begin
|
||
Root := EnsureObj(GetDevObj(MacToStr(MAC)), 'xvtr');
|
||
JW(Root, 'use_tun_power', X.UseXVTRTunePower);
|
||
for i := 0 to CFG_XVTR_COUNT - 1 do
|
||
begin
|
||
EObj := EnsureObj(Root, 'e' + IntToStr(i));
|
||
JW(EObj, 'en', X.Entries[i].Enabled);
|
||
JWS(EObj, 'name', X.Entries[i].ButtonText);
|
||
JW(EObj, 'fbeg', X.Entries[i].FreqBegin);
|
||
JW(EObj, 'fend', X.Entries[i].FreqEnd);
|
||
JW(EObj, 'lo_off', X.Entries[i].LOOffset);
|
||
JW(EObj, 'lo_err', X.Entries[i].LOError);
|
||
JW(EObj, 'full_dup', X.Entries[i].FullDuplexTx);
|
||
JW(EObj, 'tx_lo_off', X.Entries[i].TxLOOffset);
|
||
JW(EObj, 'rx_gain', X.Entries[i].RXGain);
|
||
JW(EObj, 'rx_only', X.Entries[i].RXOnly);
|
||
JW(EObj, 'tx_pwr', X.Entries[i].TXPower);
|
||
JW(EObj, 'dis_pa', X.Entries[i].DisablePA);
|
||
JW(EObj, 'rx_ant', Integer(X.Entries[i].RXAntenna));
|
||
JW(EObj, 'last_freq', X.Entries[i].LastFreq);
|
||
JW(EObj, 'last_freq_b', X.Entries[i].LastFreqB);
|
||
JW(EObj, 'last_mode', X.Entries[i].LastMode);
|
||
JW(EObj, 'last_filt', X.Entries[i].LastFilterIdx);
|
||
JW(EObj, 'last_fmsq_on', X.Entries[i].LastFMSQOn);
|
||
JW(EObj, 'last_fmsq_lvl', X.Entries[i].LastFMSQLevel);
|
||
JW(EObj, 'last_ctcss_on', X.Entries[i].LastCTCSSOn);
|
||
JW(EObj, 'last_ctcss_idx',X.Entries[i].LastCTCSSToneIdx);
|
||
JW(EObj, 'last_fmstep_on', X.Entries[i].LastFMStepOn);
|
||
JW(EObj, 'last_fmstep_idx',X.Entries[i].LastFMStepIdx);
|
||
JW(EObj, 'last_ctun', X.Entries[i].LastCTun);
|
||
JW(EObj, 'last_center_hz', X.Entries[i].LastCenterHz);
|
||
JW(EObj, 'last_agc_mode', X.Entries[i].LastAGCMode);
|
||
JW(EObj, 'last_agc_top', X.Entries[i].LastAGCTop);
|
||
JW(EObj, 'last_fmrpt_dir', X.Entries[i].LastFMRptDir);
|
||
JW(EObj, 'last_fmrpt_offset', X.Entries[i].LastFMRptOffsetHz);
|
||
JW(EObj, 'last_ctcss_auto', X.Entries[i].LastCTCSSAutoActive);
|
||
JW(EObj, 'last_fmrpt_auto', X.Entries[i].LastFMRptAutoActive);
|
||
end;
|
||
end;
|
||
|
||
class procedure TSettingsManager.DefaultWeb(out W: TWebSettings);
|
||
begin
|
||
W.Enabled := True;
|
||
W.Port := 8080;
|
||
W.BindAddr := '0.0.0.0';
|
||
W.User := 'admin';
|
||
W.Pass := 'hpsdr';
|
||
end;
|
||
|
||
procedure TSettingsManager.LoadWebSettings(out W: TWebSettings);
|
||
var O: TJSONObject;
|
||
begin
|
||
DefaultWeb(W);
|
||
if FRoot.Find('web') = nil then Exit;
|
||
O := EnsureObj(FRoot, 'web');
|
||
W.Enabled := JB(O, 'enabled', True);
|
||
W.Port := EnsureRange(JI(O, 'port', 8080), 1, 65535);
|
||
W.BindAddr := JS(O, 'bind_addr', '0.0.0.0');
|
||
W.User := JS(O, 'user', 'admin');
|
||
W.Pass := JS(O, 'pass', 'hpsdr');
|
||
end;
|
||
|
||
procedure TSettingsManager.SaveWebSettings(const W: TWebSettings);
|
||
var O: TJSONObject;
|
||
begin
|
||
O := EnsureObj(FRoot, 'web');
|
||
JW(O, 'enabled', W.Enabled);
|
||
JW(O, 'port', W.Port);
|
||
JWS(O, 'bind_addr', W.BindAddr);
|
||
JWS(O, 'user', W.User);
|
||
JWS(O, 'pass', W.Pass);
|
||
Save;
|
||
end;
|
||
|
||
procedure TSettingsManager.SaveTheme(ALightTheme: Boolean);
|
||
var O: TJSONObject;
|
||
begin
|
||
O := EnsureObj(FRoot, 'preferences');
|
||
JW(O, 'light_theme', ALightTheme);
|
||
end;
|
||
|
||
function TSettingsManager.LoadTheme: Boolean;
|
||
var O: TJSONObject;
|
||
begin
|
||
Result := False;
|
||
if FRoot.Find('preferences') = nil then Exit;
|
||
O := EnsureObj(FRoot, 'preferences');
|
||
Result := JB(O, 'light_theme', False);
|
||
end;
|
||
|
||
end.
|