fix: persist CTun center + save VHF/UHF bands (Pluto); QO-100 transverter template

CTun position was lost on band restore / restart, and VHF/UHF bands were
never saved on Pluto.

- SaveCurrentBand: the >61 MHz guard (HPSDR HF-only) skipped saving native
  Pluto VHF/UHF bands → 2m/70cm reverted to defaults. Now HPSDR-only; Pluto
  saves VU-plan bands (FreqToBandIdx guards cross-band pollution).
- Persist DDC center for CTun: new TBandSettings.CenterHz (+ JSON center_hz)
  and TXvtrEntry.LastCenterHz (+ last_center_hz). RestoreBand / ActivateXvtr
  restore the saved center when CTun is on and the offset fits the span,
  instead of forcing center := VFO (which re-centered the receiver).
- UpdateFreqDisplayMax: freq-display upper bound from active transverter's
  FreqEnd (lets QO-100 show 10 GHz past the 6 GHz Pluto cap).
- QO-100 transverter template (slot 2, disabled): downlink 10489.5–10490.0,
  LO 9750. MigrateXvtrTemplate injects it into the first free slot for
  devices that already have a saved xvtr config (template was otherwise
  masked by the loaded slots).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 22:02:01 +03:00
co-authored by Claude Opus 4.8
parent 24f74357cd
commit 40dca01c40
3 changed files with 97 additions and 20 deletions
+45 -1
View File
@@ -54,6 +54,7 @@ type
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
@@ -172,6 +173,7 @@ type
LastFMStepOn: Boolean;
LastFMStepIdx: Integer;
LastCTun: Boolean;
LastCenterHz: Double; // Hz — DDC-центр при CTun (восстановление offset VFO)
LastAGCMode: Integer;
LastAGCTop: Integer;
LastFMRptDir: Integer;
@@ -293,6 +295,8 @@ type
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);
public
constructor Create(const FilePath: string = '');
destructor Destroy; override;
@@ -381,6 +385,7 @@ begin
B.AGCMode := 1;
B.AGCTop := 90;
B.CTun := False;
B.CenterHz := 0; // 0 = нет сохранённого центра → центрировать на VFO
B.SpanHz := 192000;
B.FMSQOn := False;
B.FMSQLevel := 30;
@@ -695,6 +700,7 @@ begin
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);
end;
end;
@@ -781,7 +787,7 @@ begin
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,'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);
@@ -805,6 +811,7 @@ begin
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);
@@ -1223,6 +1230,7 @@ begin
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;
@@ -1243,6 +1251,14 @@ begin
X.Entries[1].LOOffset := 404000000;
X.Entries[1].LastFreq := 432200000;
X.Entries[1].LastFreqB := 433000000;
// QO-100 (Es'hail-2) NB downlink: 10489.50010490.000 МГц, LO 9750 МГц.
// RX-трансвертер: tune = downlink LO. Калибровать LOError по маяку.
X.Entries[2].ButtonText := 'QO-100';
X.Entries[2].FreqBegin := 10489500000;
X.Entries[2].FreqEnd := 10490000000;
X.Entries[2].LOOffset := 9750000000;
X.Entries[2].LastFreq := 10489750000;
X.Entries[2].LastFreqB := 10489750000;
end;
function TSettingsManager.LoadXvtr(const MAC: array of Byte;
@@ -1286,6 +1302,7 @@ begin
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);
@@ -1300,6 +1317,32 @@ begin
(X.Entries[i].LastFreqB > X.Entries[i].FreqEnd) then
X.Entries[i].LastFreqB := (X.Entries[i].FreqBegin + X.Entries[i].FreqEnd) / 2.0;
end;
// Миграция: на устройствах с уже сохранённым xvtr-конфигом шаблон QO-100 из
// DefaultXvtr был перекрыт загруженными слотами. Если слота 'QO-100' нет —
// вписать шаблон (disabled) в первый свободный слот, чтобы он был в таблице.
MigrateXvtrTemplate(X, 'QO-100', 10489500000, 10490000000, 9750000000);
end;
procedure TSettingsManager.MigrateXvtrTemplate(var X: TXvtrSettings;
const AName: string; ABegin, AEnd, ALO: Double);
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].LastFreq := (ABegin + AEnd) / 2.0;
X.Entries[freeIdx].LastFreqB := (ABegin + AEnd) / 2.0;
end;
procedure TSettingsManager.SaveXvtr(const MAC: array of Byte;
@@ -1335,6 +1378,7 @@ begin
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);