feat(beacon): состояние захвата видно прямо в лупе

Чтобы понять «попал или нет», приходилось переводить взгляд на плитки метрик
под графиком. Теперь это читается в самой лупе:

- Полоса приёма показывает захват начертанием: в поиске три линии пунктирные,
  на локе — сплошные и стянуты сверху скобой. Новых цветов не добавлено, та же
  семантика $0020D0FF, что и на главном спектре.
- Строка захвата под частотой центра: LOCK · 14.2 dB (зелёная) / SEARCH (янтарь)
  / BEACON OFF. Состояние берётся из BeaconState — это гистерезисный
  FBeaconLocked, которому доверяет сам контур коррекции, а не мгновенный
  CarrierLock: тот мерцал бы на границе.
- Δ от опорной рядом: расхождение измеренной частоты маяка с опорной, то есть
  расстояние между оранжевым и зелёным маркерами числом. Это невязка, которую
  лок стекает в LOError; в пределах дедбэнда (20 Гц) — зелёная, иначе янтарная.

Знак у Δ ставится вручную: флага '+' в Format Object Pascal нет (это printf'изм),
'%+.0f' молча съедается и в строку попадает голый хвост спецификатора — на
экране было «Δ .0f Hz» вместо значения.

Состояние и SNR снимаются раз в тик в PollZoom и кладутся в поля: копия
TBeaconScope в отрисовку не лезет, а SNR сравнивается с допуском 0.2 дБ —
иначе дрожание в сотых долях дБ пересобирало бы кадр каждый тик и dirty-флаг
потерял бы смысл.

Сборка ewsdr (--ws=qt6) — ОК.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-14 12:13:33 +03:00
co-authored by Claude Opus 5
parent e82f573313
commit a5daf33cc9
+66 -3
View File
@@ -96,6 +96,10 @@ type
FZoomDirty: Boolean;
FStripValid: Boolean; // в FStrip лежит кадр под текущий размер
FLastRefHz, FLastTrkHz, FLastDecHz: Double; // детект сдвига маркеров
// Состояние захвата, снятое в PollZoom: рисовать его надо в кадре лупы, а
// лезть за копией TBeaconScope на каждую отрисовку незачем.
FState: TBeaconState;
FSNRdB: Double;
FCursorX: Integer; // -1 = курсор вне лупы
FDragging: Boolean; // ЛКМ зажата в лупе
FDragMoved: Boolean; // увели мышь → это панорамирование, не клик
@@ -594,6 +598,14 @@ begin
else
FPixCount := 0;
// Лок и SNR — часть картинки: по ним видно, попал ли клик в маяк. SNR
// сравниваем с допуском, иначе дрожание в сотых долях дБ пересобирало бы
// кадр на каждом тике.
if FCtl.BeaconState <> FState then FZoomDirty := True;
if Abs(FCtl.BeaconSNRdB - FSNRdB) > 0.2 then FZoomDirty := True;
FState := FCtl.BeaconState;
FSNRdB := FCtl.BeaconSNRdB;
if FCtl.BeaconRefHz <> FLastRefHz then FZoomDirty := True;
if FCtl.BeaconTrackedFreqHz <> FLastTrkHz then FZoomDirty := True;
if FCtl.BeaconDecodeFreqHz <> FLastDecHz then FZoomDirty := True;
@@ -905,6 +917,8 @@ var
Sorted: array of Double;
TmpD: Double;
a, b: Integer;
Locked: Boolean;
Sign: string;
procedure SetFont(ASize: Integer; AStyle: TFontStyles = []);
begin
@@ -1057,9 +1071,27 @@ begin
if TrkHz > 0 then VLine(TrkHz, BCN_CLR_TRK, False);
if DecHz > 0 then
begin
VLine(DecHz - BCN_DEC_BW_HZ, BCN_CLR_DEC, False);
VLine(DecHz + BCN_DEC_BW_HZ, BCN_CLR_DEC, False);
VLine(DecHz, BCN_CLR_DEC, False);
// Полоса приёма показывает захват сама: в поиске грани пунктирные, на локе
// — сплошные и стянуты скобой. Так «попал/не попал» читается прямо здесь,
// не переводя взгляд на плитки метрик под графиком.
Locked := FState = bsLock;
VLine(DecHz - BCN_DEC_BW_HZ, BCN_CLR_DEC, not Locked);
VLine(DecHz + BCN_DEC_BW_HZ, BCN_CLR_DEC, not Locked);
VLine(DecHz, BCN_CLR_DEC, not Locked);
if Locked then
begin
a := EnsureRange(FreqToX(DecHz - BCN_DEC_BW_HZ, W), 0, W - 1);
b := EnsureRange(FreqToX(DecHz + BCN_DEC_BW_HZ, W), 0, W - 1);
if b > a then
begin
Y := DpiScale(40); // ниже строки захвата, чтобы не наехать на текст
C.Pen.Color := BCN_CLR_DEC;
C.Pen.Style := psSolid;
C.Line(a, Y, b, Y);
C.Line(a, Y, a, Y + DpiScale(4));
C.Line(b, Y, b, Y + DpiScale(4));
end;
end;
end;
// --- Линейка частот ---
@@ -1100,6 +1132,37 @@ begin
Txt := Format('span %.1f kHz', [Span / 1000]);
C.TextOut(W - DpiScale(6) - C.TextWidth(Txt), DpiScale(5), Txt);
// --- Строка захвата: состояние + SNR + расхождение с опорной ---
// Δ = измеренная частота маяка минус опорная, то есть ровно расстояние между
// оранжевым и зелёным маркерами. Это невязка, которую контур стекает в
// LOError: пока она больше дедбэнда, лок ещё подкручивает LO.
SetFont(7);
case FState of
bsLock: begin Txt := 'LOCK'; C.Font.Color := FTheme.MeterOn; end;
bsSearch: begin Txt := 'SEARCH'; C.Font.Color := FTheme.Amber; end;
else
Txt := 'BEACON OFF';
C.Font.Color := FTheme.TextDim;
end;
if (FState <> bsIdle) and (FSNRdB > -50) then
Txt := Txt + Format(' · %.1f dB', [FSNRdB]);
C.TextOut(DpiScale(6), DpiScale(21), Txt);
a := DpiScale(6) + C.TextWidth(Txt) + DpiScale(10);
if (FState <> bsIdle) and (TrkHz > 0) and (RefHz > 0) then
begin
T := TrkHz - RefHz;
// Знак ставим руками: флага '+' в Format Object Pascal нет (это printf'изм),
// '%+.0f' молча съедается и в строку попадает голый остаток спецификатора.
Sign := IfThen(T >= 0, '+', '');
if Abs(T) < 1000 then Txt := Format('Δ %s%.0f Hz', [Sign, T])
else Txt := Format('Δ %s%.2f kHz', [Sign, T / 1000]);
// Внутри дедбэнда контура (20 Гц) маяк считается стоящим на месте.
if Abs(T) <= 20.0 then C.Font.Color := FTheme.MeterOn
else C.Font.Color := FTheme.Amber;
C.TextOut(a, DpiScale(21), Txt);
end;
// Рамка карточки поверх всего
C.Brush.Style := bsClear;
C.Pen.Style := psSolid;