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_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;
  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;
    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
    // --- Display FPS ---
    DisplayFPS:        Integer;  // spectrum/waterfall timer fps (1..100)
    // --- PA (Power Amplifier) settings ---
    PAMaxPower:        Double;   // максимальная выходная мощность, Вт (5..200)
    PABandCal:         array[0..CFG_BAND_COUNT-1] of Double; // калибровка на диапазон 38.8..100.0
    // --- 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;
    class function MacToStr(const MAC: array of Byte): string;
    class procedure DefaultBand(BandIdx: Integer; out B: TBandSettings);
    class procedure DefaultGlobal(out G: TGlobalSettings);
    // Размер окна — не привязан к 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);
  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;
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.WfAGCEnabled := True;
  G.WfNFEnabled := False;
  G.SampleRate := 192000;
  G.PAMaxPower := 100.0;
  for i := 0 to CFG_BAND_COUNT-1 do G.PABandCal[i] := 100.0;
  // 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.DisplayFPS       := 60;
  // 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;
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.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.DisplayFPS    := JI(GObj,'display_fps',60);
  // 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),100.0);
  // 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);

  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,'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_fps',G.DisplayFPS);
  // 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]);
  // 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);
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);
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);
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;

end.
