diff --git a/BeaconScopeForm.pas b/BeaconScopeForm.pas index dbc64bd..6e9365b 100644 --- a/BeaconScopeForm.pas +++ b/BeaconScopeForm.pas @@ -119,6 +119,8 @@ type procedure LayoutTunePanel; procedure PollZoom; // забрать кадр лупы у контроллера procedure ClampWindow; // окно не вылезает из полосы захвата + procedure RestoreZoomState; // окно лупы из настроек + procedure StoreZoomState; // окно лупы в настройки function DefaultCenterHz: Double; // куда смотреть, пока не выбрали function XToFreq(X, W: Integer): Double; function FreqToX(F: Double; W: Integer): Integer; @@ -511,6 +513,38 @@ begin FCenterHz := EnsureRange(FCenterHz, CapLo + Half, CapHi - Half); end; +procedure TBeaconScopeForm.RestoreZoomState; +// Окно лупы переживает перезапуск: ширина, положение и FOLLOW лежат в +// глобальных настройках (персист — общий SaveGlobalSettings при выходе). +// Положение хранится сдвигом от опорной частоты маяка, поэтому восстановление +// не зависит от накопленной калибровки LOError. +begin + if FCtl = nil then Exit; + FSpanHz := EnsureRange(FCtl.FBcnZoomSpanHz, BCN_SPAN_MIN, BCN_SPAN_MAX); + FFollow := FCtl.FBcnZoomFollow; + if FCtl.BeaconRefHz > 0 then + FCenterHz := FCtl.BeaconRefHz + FCtl.FBcnZoomOffsetHz + else + FCenterHz := DefaultCenterHz; + if FBtnFollow <> nil then FBtnFollow.Active := FFollow; + ClampWindow; + // Наведение декодера могло остаться за краем восстановленного окна — его + // подтянет FOLLOW на первом же тике (PollZoom), отдельной логики не нужно. + FZoomDirty := True; + FStripValid := False; +end; + +procedure TBeaconScopeForm.StoreZoomState; +// Зовётся каждый тик: так последнее состояние окна попадает в настройки, каким +// бы путём оно ни поменялось (колесо, drag, кнопки, авто-подтяжка FOLLOW). +begin + if FCtl = nil then Exit; + FCtl.FBcnZoomSpanHz := FSpanHz; + FCtl.FBcnZoomFollow := FFollow; + if FCtl.BeaconRefHz > 0 then + FCtl.FBcnZoomOffsetHz := FCenterHz - FCtl.BeaconRefHz; +end; + procedure TBeaconScopeForm.PollZoom; // Раз/тик: держим окно на месте (или тянем за декодером), просим у контроллера // свежий кадр лупы и отдаём водопаду. @@ -534,6 +568,7 @@ begin end; end; ClampWindow; + StoreZoomState; FCtl.SetBeaconZoomWindow(True, FCenterHz, FSpanHz); @@ -762,7 +797,7 @@ end; procedure TBeaconScopeForm.DoShow; begin inherited DoShow; - if FCenterHz <= 0 then FCenterHz := DefaultCenterHz; + RestoreZoomState; // Палитра/гамма водопада — те же, что выбраны для главного (настройки живут // в контроллере), иначе лупа выглядела бы чужой панелью. if (FWf <> nil) and (FCtl <> nil) then diff --git a/RadioController.pas b/RadioController.pas index 24ad03d..8cd5e47 100644 --- a/RadioController.pas +++ b/RadioController.pas @@ -332,6 +332,12 @@ type FBeaconDecHz: Double; // АБСОЛЮТНАЯ display-частота наведения декодера // (узкий фильтр, ставится кликом); 0 = ещё не наведён FBeaconDecInvert: Boolean; // зеркальный baseband (QO-100/LNB) — деф. True + // Лупа наведения (окно Beacon) — состояние окна, переживающее перезапуск. + // Центр хранится СДВИГОМ от FBeaconRefHz: так он не зависит ни от правки + // опорной частоты, ни от накопленной калибровки LOError. + FBcnZoomSpanHz: Double; // ширина окна лупы, Гц + FBcnZoomOffsetHz: Double; // центр = FBeaconRefHz + этот сдвиг + FBcnZoomFollow: Boolean; // держать наведение декодера в окне // ---- DMR 4FSK receive decoder ---- FDMRDec: TDMRDecoder; // 48 kHz FM discriminator -> DMR worker @@ -1317,6 +1323,9 @@ begin // и prom 500-800 при invert=ON против шума при OFF, на наведении точно на маяк). // Декодеру нужно сопряжение, чтобы наведённый offset доставлял маяк к DC. FBeaconDecInvert := True; + FBcnZoomSpanHz := 40000.0; + FBcnZoomOffsetHz := 0.0; + FBcnZoomFollow := True; FBeaconDec.SetInvert(FBeaconDecInvert); FDSPEngine.SetBeaconDecoder(FBeaconDec); @@ -3910,6 +3919,9 @@ begin Result.SpecRefLevel := FSpecRefLevel; Result.SpecRange := FSpecRange; Result.SpecGridStep := FSpecGridStep; + Result.BeaconZoomSpanHz := FBcnZoomSpanHz; + Result.BeaconZoomOffsetHz := FBcnZoomOffsetHz; + Result.BeaconZoomFollow := FBcnZoomFollow; Result.AudioSampleRate := FAudioOut.SampleRate; Result.ShowSpectrum := FShowSpectrum; Result.ShowWaterfall := FShowWaterfall; @@ -7303,6 +7315,9 @@ begin FDisplayPixels := EnsureRange(FLoadedGlobal.DisplayPixels, 64, 4096); FWfSpeedDiv := EnsureRange(FLoadedGlobal.WfSpeedDiv, 1, 8); FSpecMSAA := EnsureRange(FLoadedGlobal.SpecMSAA, 1, 8); + FBcnZoomSpanHz := EnsureRange(FLoadedGlobal.BeaconZoomSpanHz, 600.0, 200000.0); + FBcnZoomOffsetHz := EnsureRange(FLoadedGlobal.BeaconZoomOffsetHz, -500000.0, 500000.0); + FBcnZoomFollow := FLoadedGlobal.BeaconZoomFollow; if FWDSPReady then FDSPEngine.SetDisplayResLimit(FDisplayPixels); FShowSpectrum := FLoadedGlobal.ShowSpectrum; FShowWaterfall := FLoadedGlobal.ShowWaterfall; diff --git a/Settings.pas b/Settings.pas index d2af3f8..47d3e6b 100644 --- a/Settings.pas +++ b/Settings.pas @@ -614,6 +614,12 @@ type 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 + // --- Лупа наведения на маяк QO-100 (карточка BEACON TUNING) --- + // Центр хранится СДВИГОМ от опорной частоты маяка, а не абсолютом: так + // окно переживает и правку опорной частоты, и калибровку LOError. + BeaconZoomSpanHz: Double; // ширина окна лупы, Гц + BeaconZoomOffsetHz: Double; // центр окна = BeaconRefHz + этот сдвиг + BeaconZoomFollow: Boolean; // держать наведение декодера в окне // --- Audio settings --- AudioOutDevice: string; // PortAudio output device name ('' = default) AudioSampleRate: Integer; // PortAudio stream sample rate (e.g. 48000) @@ -979,6 +985,9 @@ begin G.SpecRefLevel := -20.0; G.SpecRange := 110.0; G.SpecGridStep := 10.0; + G.BeaconZoomSpanHz := 40000.0; + G.BeaconZoomOffsetHz := 0.0; + G.BeaconZoomFollow := True; G.AudioOutDevice := ''; G.AudioSampleRate := 48000; G.AudioInDevice := ''; @@ -1229,6 +1238,9 @@ begin 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); + G.BeaconZoomSpanHz := JD(GObj,'beacon_zoom_span',40000.0); + G.BeaconZoomOffsetHz := JD(GObj,'beacon_zoom_offset',0.0); + G.BeaconZoomFollow := JB(GObj,'beacon_zoom_follow',True); // Audio settings G.AudioOutDevice := JS(GObj,'audio_out_device',''); G.AudioSampleRate := JI(GObj,'audio_sample_rate',48000); @@ -1376,6 +1388,9 @@ begin JW(O,'spec_ref_level',G.SpecRefLevel); JW(O,'spec_range',G.SpecRange); JW(O,'spec_grid_step',G.SpecGridStep); + JW(O,'beacon_zoom_span',G.BeaconZoomSpanHz); + JW(O,'beacon_zoom_offset',G.BeaconZoomOffsetHz); + JW(O,'beacon_zoom_follow',G.BeaconZoomFollow); // Audio settings JWS(O,'audio_out_device',G.AudioOutDevice); JW(O,'audio_sample_rate',G.AudioSampleRate);