feat(beacon): окно лупы переживает перезапуск

Ширина, положение и состояние 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 <noreply@anthropic.com>
This commit is contained in:
2026-08-14 12:03:13 +03:00
co-authored by Claude Opus 5
parent ac216fa332
commit e82f573313
3 changed files with 66 additions and 1 deletions
+36 -1
View File
@@ -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
+15
View File
@@ -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;
+15
View File
@@ -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);