mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
PA calibration per band (HF and VHF): - Add VHFBandCal[0..7] in TGlobalSettings (saved as vhf_cal_0..7 in JSON) - Default HF cal updated to Thetis HERMES defaults (38.8-41.3 dB per band) - Default VHF cal = 56.2 dB (Thetis HERMES XVTR default) - SettingsForm: second group in PA tab "VHF Calibration (Transverters)" with 8 spinboxes VHF0..VHF7; labels dynamically show XVTR slot ButtonText when LoadXvtrSettings is called (e.g. "VHF0 (2m):") TX drive formula (CalcDriveByte) rewritten to match Thetis exactly: drive_byte = min(sqrt(10^((10*log10(pwr_W*1000) - Cal)/10) * 0.05) / 0.8, 1.0) * 1.02 * 255 Cal = PA gain in dB (not a percentage); matches Thetis verified output (Cal=63.1 + slider=100% + PAMax=100W → drive_byte=16, verified against Thetis) Old formula Pos/100*Cal/100*255 was wrong: treated dB gain as linear percent Fix stale FDriveLevel not updated on band/XVTR switch: - Startup: FCurrentBand must be set before CalcDriveByte (was reversed) - RestoreBand: recalculate FDriveLevel before ApplyVfoA network update - ActivateXvtrBand: recalculate after FCurrentXvtr changes - ApplyVfoA: recalculate when VFO crosses HF band boundary - FreqDispBChanged: same for VFO-B band changes - ApplyMOX(True): safety recalculate at PTT engage time Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1292 lines
54 KiB
ObjectPascal
1292 lines
54 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;
|
||
|
||
const
|
||
SETTINGS_FILE = 'hpsdr_settings.json';
|
||
CFG_BAND_COUNT = 11;
|
||
CFG_XVTR_COUNT = 8; // количество 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);
|
||
|
||
type
|
||
TBandSettings = record
|
||
VfoA: Double;
|
||
VfoB: Double;
|
||
Mode: Integer;
|
||
FilterIdx: Integer;
|
||
FilterBW: Integer;
|
||
AGCMode: Integer;
|
||
AGCTop: Integer;
|
||
CTun: Boolean;
|
||
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)
|
||
end;
|
||
|
||
// TX-настройки per-device. Хранятся в JSON-секции "tx" под MAC,
|
||
// отдельно от global/bands. Применяются к WDSP через SetTX* методы.
|
||
// (Тип TTXMicSource живёт в WDSPEngine — здесь храним как Integer,
|
||
// чтобы не тащить циклическую зависимость с WDSPEngine.)
|
||
TTXSettings = record
|
||
// Mic source select
|
||
MicSource: Integer; // 0=Radio, 1=SoundCard
|
||
// Radio mic options (DUC Specific byte 50 bits)
|
||
MicBoost: Boolean; // bit0: +20 dB
|
||
MicBias: Boolean; // bit4: электретный bias
|
||
MicLineIn: Boolean; // bit1: 1=Line in, 0=Mic in
|
||
MicPTTEnabled: Boolean; // bit2: Mic PTT enable
|
||
// 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
|
||
LOError: Double; // Hz, корректировка LO (положительная — IF растёт)
|
||
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 частота (BandStack)
|
||
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;
|
||
LastAGCMode: Integer;
|
||
LastAGCTop: Integer;
|
||
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; // глобальный — один для всех диапазонов
|
||
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)
|
||
// --- Visibility settings ---
|
||
ShowSpectrum: Boolean; // True = draw spectrum on main form
|
||
ShowWaterfall: Boolean; // True = draw waterfall on main form
|
||
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)
|
||
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);
|
||
public
|
||
constructor Create(const FilePath: string = SETTINGS_FILE);
|
||
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: Integer);
|
||
procedure LoadWindowBounds(out L, T, W, H: Integer);
|
||
procedure SaveStartupPreview(VfoA, VfoB: Double; SampleRate: Integer);
|
||
function LoadStartupPreview(out VfoA, VfoB: Double; out SampleRate: Integer): Boolean;
|
||
// 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.SpanHz := 192000;
|
||
B.FMSQOn := False;
|
||
B.FMSQLevel := 30;
|
||
B.CTCSSOn := False;
|
||
B.CTCSSToneIdx := 0;
|
||
B.FMStepOn := True;
|
||
B.FMStepIdx := 3; // 25 kHz
|
||
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.PAMaxPower := 100.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.ShowSpectrum := True;
|
||
G.ShowWaterfall := 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;
|
||
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);
|
||
// 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','');
|
||
// Visibility settings
|
||
G.ShowSpectrum := JB(GObj,'show_spectrum',True);
|
||
G.ShowWaterfall := JB(GObj,'show_waterfall',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);
|
||
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].SpanHz := JD(BObj,'span_hz', Bands[i].SpanHz);
|
||
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);
|
||
// 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);
|
||
// Visibility settings
|
||
JW(O,'show_spectrum',G.ShowSpectrum);
|
||
JW(O,'show_waterfall',G.ShowWaterfall);
|
||
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);
|
||
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,'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);
|
||
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.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);
|
||
end;
|
||
|
||
procedure TSettingsManager.SaveWindowBounds(L, T, W, H: Integer);
|
||
var O: TJSONObject;
|
||
begin
|
||
O := EnsureObj(FRoot, 'window');
|
||
JW(O, 'left', L);
|
||
JW(O, 'top', T);
|
||
JW(O, 'width', W);
|
||
JW(O, 'height', H);
|
||
end;
|
||
|
||
procedure TSettingsManager.LoadWindowBounds(out L, T, W, H: Integer);
|
||
var O: TJSONObject;
|
||
begin
|
||
L := 80; T := 80; W := 1400; H := 900;
|
||
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);
|
||
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.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);
|
||
// Mic source — Radio (HW). 0=Radio, 1=SoundCard (синхронно с TTXMicSource в WDSPEngine).
|
||
T.MicSource := 0;
|
||
T.MicBoost := False;
|
||
T.MicBias := True;
|
||
T.MicLineIn := False;
|
||
T.MicPTTEnabled := False;
|
||
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.MicSource := JI(O,'mic_source', T.MicSource);
|
||
T.MicBoost := JB(O,'mic_boost', T.MicBoost);
|
||
T.MicBias := JB(O,'mic_bias', T.MicBias);
|
||
T.MicLineIn := JB(O,'mic_line_in', T.MicLineIn);
|
||
T.MicPTTEnabled := JB(O,'mic_ptt_enabled', T.MicPTTEnabled);
|
||
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_source', T.MicSource);
|
||
JW(O,'mic_boost', T.MicBoost);
|
||
JW(O,'mic_bias', T.MicBias);
|
||
JW(O,'mic_line_in', T.MicLineIn);
|
||
JW(O,'mic_ptt_enabled', T.MicPTTEnabled);
|
||
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].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].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].LastAGCMode := 1;
|
||
X.Entries[i].LastAGCTop := 90;
|
||
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[1].ButtonText := '70cm';
|
||
X.Entries[1].FreqBegin := 430000000;
|
||
X.Entries[1].FreqEnd := 440000000;
|
||
X.Entries[1].LOOffset := 404000000;
|
||
X.Entries[1].LastFreq := 432200000;
|
||
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].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].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].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);
|
||
// Валидация: LastFreq должен быть в [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;
|
||
end;
|
||
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, '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_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_agc_mode', X.Entries[i].LastAGCMode);
|
||
JW(EObj, 'last_agc_top', X.Entries[i].LastAGCTop);
|
||
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.
|