diff --git a/MainForm.pas b/MainForm.pas index 9a02bdc..d06f051 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -534,6 +534,7 @@ type WidebandFill, SpectrumFill: Boolean); procedure ApplyFPS(FPS: Integer); procedure ApplyFreqMhzDigits(Digits: Integer); + procedure UpdateFreqDisplayMax; // верхняя граница FreqDisplay (XVTR/Pluto/HF) published // Обработчики событий формы — должны быть в published для RTTI/LFM @@ -1188,6 +1189,7 @@ begin for i := 0 to CFG_XVTR_COUNT - 1 do if BtnXvtrBand[i] <> nil then StyleButton(BtnXvtrBand[i], i = FController.FCurrentXvtr); + UpdateFreqDisplayMax; // трансвертер может быть >6 ГГц (QO-100) // Wideband-вид + web-зеркало xvtr (ранее в обёртках Activate/Deactivate). // rfXvtr эмитят ТОЛЬКО ActivateXvtr/DeactivateXvtr — состояние финальное. UpdateWidebandFrequencyView; @@ -5539,30 +5541,39 @@ begin end; procedure TMainForm.ApplyFreqMhzDigits(Digits: Integer); -const - MaxFreqs: array[3..5] of Int64 = (999999999, 9999999999, 99999999999); begin Digits := EnsureRange(Digits, 3, 5); FFreqMhzDigits := Digits; FController.FLoadedGlobal.FreqMhzDigits := Digits; // зеркало в persist-буфер контроллера FreqDispA.MinMhzDigits := Digits; FreqDispB.MinMhzDigits := Digits; - // Pluto/VHF/UHF: верхняя граница из Caps (до 6 ГГц), иначе HF по числу цифр. - if FController.IsPluto then - begin - FreqDispA.MaxFreq := Round(FController.BackendCaps.MaxFreqHz); - FreqDispB.MaxFreq := Round(FController.BackendCaps.MaxFreqHz); - end - else - begin - FreqDispA.MaxFreq := MaxFreqs[Digits]; - FreqDispB.MaxFreq := MaxFreqs[Digits]; - end; + UpdateFreqDisplayMax; // верхняя граница: активный XVTR / Pluto Caps / HF по цифрам if Assigned(FWebServer) then FWebServer.FreqMhzDigits := Digits; LayoutTopVfoBlock; end; +procedure TMainForm.UpdateFreqDisplayMax; +// Верхняя граница FreqDisplay. Активный трансвертер (XVTR/QO-100) может показывать +// частоты выше диапазона железа (напр. QO-100 — 10.5 ГГц) → берём его FreqEnd. +// Иначе Pluto — из Caps (до 6 ГГц), HPSDR — по числу MHz-цифр. +const + MaxFreqs: array[3..5] of Int64 = (999999999, 9999999999, 99999999999); +var + hi: Int64; xi: Integer; +begin + xi := FController.FCurrentXvtr; + if (xi >= 0) and (xi < CFG_XVTR_COUNT) then + hi := Round(FController.FXvtrSettings.Entries[xi].FreqEnd) + 1000000 // +1 МГц запас + else if FController.IsPluto then + hi := Round(FController.BackendCaps.MaxFreqHz) + else + hi := MaxFreqs[EnsureRange(FFreqMhzDigits, 3, 5)]; + if hi < 1000000 then hi := MaxFreqs[3]; + if FreqDispA <> nil then FreqDispA.MaxFreq := hi; + if FreqDispB <> nil then FreqDispB.MaxFreq := hi; +end; + procedure TMainForm.BtnSettingsClick(Sender: TObject); var SF: TSettingsForm; diff --git a/RadioController.pas b/RadioController.pas index c3d4684..3a0bba3 100644 --- a/RadioController.pas +++ b/RadioController.pas @@ -1050,6 +1050,7 @@ begin Result.AGCMode := FAGCMode; Result.AGCTop := FAGCTop; Result.CTun := FCTun; + Result.CenterHz := FCenterFreq; // DDC-центр → восстановим offset CTun при загрузке Result.SpanHz := FSpanHz; Result.FMSQOn := FFMSQOn; Result.FMSQLevel := FFMSQLevel; @@ -1088,6 +1089,7 @@ begin FXvtrSettings.Entries[i].LastFMRptDir := FFMRptDir; FXvtrSettings.Entries[i].LastFMRptOffsetHz := FFMRptOffsetHz; FXvtrSettings.Entries[i].LastCTun := FCTun; + FXvtrSettings.Entries[i].LastCenterHz := FCenterFreq; FXvtrSettings.Entries[i].LastAGCMode := FAGCMode; FXvtrSettings.Entries[i].LastAGCTop := FAGCTop; FXvtrSettings.Entries[i].LastCTCSSAutoActive := FFMCTCSSAutoActive; @@ -1096,9 +1098,11 @@ begin FSettings.SaveXvtr(FDevMAC, FXvtrSettings); Exit; end; - // Не сохраняем, если VFO ушёл в VHF/UHF (например, после канала на 2м без - // XVTR) — иначе HF-кэш диапазона перезапишется VHF-частотой. - if FVfoA > 61000000 then Exit; + // HPSDR: приёмник только HF — если VFO ушёл в VHF/UHF (напр. канал на 2м без + // XVTR), не сохраняем, иначе HF-кэш диапазона перезапишется VHF-частотой. + // Pluto: VHF/UHF — нативные диапазоны (VU-план), сохранять обязательно; + // от перезаписи чужим диапазоном защищает FreqToBandIdx-проверка ниже. + if (not IsPluto) and (FVfoA > 61000000) then Exit; B := MakeBandSettings; // Bandstack: VFO A и B независимы и могут стоять на разных бэндах. В кэш // ЭТОГО бэнда пишем только ту частоту VFO, что реально в нём — иначе чужая @@ -1262,7 +1266,14 @@ begin ApplyBandDSP(B); // --- VFO / drive (только RestoreBand — bandstack-путь VFO не трогает) --- - FCenterFreq := B.VfoA; + // При CTun восстанавливаем сохранённый DDC-центр, чтобы VFO остался на своём + // месте в полосе (а не прыгал в центр). Берём только если offset влезает в + // текущий span — иначе (сменился sample rate / битый конфиг) центрируем на VFO. + // ApplyTuneCore в SetVfoA при необходимости сам прокрутит центр к краю. + if FCTun and (B.CenterHz > 0) and (Abs(B.VfoA - B.CenterHz) < FSpanHz / 2) then + FCenterFreq := B.CenterHz + else + FCenterFreq := B.VfoA; FVfoB := B.VfoB; FVfoA := B.VfoA; FDriveLevel := CalcDriveByte; @@ -1808,8 +1819,19 @@ begin // VFO: B — своя сохранённая частота слота (клэмп в диапазон); A = visible. FVfoB := EnsureRange(E.LastFreqB, E.FreqBegin, E.FreqEnd); FVfoA := Vis; - FCenterFreq := Vis; - if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(0.0); + // При CTun восстанавливаем сохранённый DDC-центр, чтобы VFO остался на своём + // месте в полосе (а не прыгал в центр). Берём только если offset влезает в + // текущий span — иначе центрируемся на видимой частоте. + if FCTun and (E.LastCenterHz > 0) and (Abs(Vis - E.LastCenterHz) < FSpanHz / 2) then + begin + FCenterFreq := E.LastCenterHz; + if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(Vis - FCenterFreq); + end + else + begin + FCenterFreq := Vis; + if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(0.0); + end; FDriveLevel := CalcDriveByte; PushNetworkState; diff --git a/Settings.pas b/Settings.pas index 9e16758..c732b25 100644 --- a/Settings.pas +++ b/Settings.pas @@ -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.500–10490.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);