From e82f5733131a790fd6ebace25907082bc449fbd4 Mon Sep 17 00:00:00 2001 From: Uladzimir Karpenka Date: Fri, 14 Aug 2026 12:03:13 +0300 Subject: [PATCH] =?UTF-8?q?feat(beacon):=20=D0=BE=D0=BA=D0=BD=D0=BE=20?= =?UTF-8?q?=D0=BB=D1=83=D0=BF=D1=8B=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B6=D0=B8?= =?UTF-8?q?=D0=B2=D0=B0=D0=B5=D1=82=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B7=D0=B0?= =?UTF-8?q?=D0=BF=D1=83=D1=81=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ширина, положение и состояние FOLLOW уезжали в дефолт при каждом открытии окна: настроил 5 кГц вокруг маяка, закрыл — в следующий раз снова 40 кГц. Положение хранится СДВИГОМ от опорной частоты маяка, а не абсолютом. Абсолют протух бы от любой правки опорной частоты, а главное — калибровка LOError копится от сеанса к сеансу и двигает шкалу, так что сохранённая абсолютная частота через неделю указывала бы уже не туда. - Settings: BeaconZoomSpanHz/OffsetHz/Follow в TGlobalSettings (ключи beacon_zoom_span/offset/follow), дефолты и чтение/запись по общему пути. - RadioController: FBcnZoom* с клампом при загрузке (span 600..200000 Гц, сдвиг ±500 кГц) — руками испорченный JSON окно не сломает. - BeaconScopeForm: RestoreZoomState в DoShow, StoreZoomState каждый тик из PollZoom. Раз состояние пишется в одном месте, оно не разъедется, каким бы путём ни менялось — колесом, drag'ом, кнопками или автоподтяжкой FOLLOW. Наведение декодера за краем восстановленного окна подтянет сам FOLLOW на первом тике, отдельной логики не потребовалось. Сборка ewsdr (--ws=qt6) и ewsdrd — ОК. Co-Authored-By: Claude Opus 5 --- BeaconScopeForm.pas | 37 ++++++++++++++++++++++++++++++++++++- RadioController.pas | 15 +++++++++++++++ Settings.pas | 15 +++++++++++++++ 3 files changed, 66 insertions(+), 1 deletion(-) 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);