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