From a5daf33cc9c9ad9f13528f73f007fe3c9e2b35b7 Mon Sep 17 00:00:00 2001 From: Uladzimir Karpenka Date: Fri, 14 Aug 2026 12:13:33 +0300 Subject: [PATCH] =?UTF-8?q?feat(beacon):=20=D1=81=D0=BE=D1=81=D1=82=D0=BE?= =?UTF-8?q?=D1=8F=D0=BD=D0=B8=D0=B5=20=D0=B7=D0=B0=D1=85=D0=B2=D0=B0=D1=82?= =?UTF-8?q?=D0=B0=20=D0=B2=D0=B8=D0=B4=D0=BD=D0=BE=20=D0=BF=D1=80=D1=8F?= =?UTF-8?q?=D0=BC=D0=BE=20=D0=B2=20=D0=BB=D1=83=D0=BF=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Чтобы понять «попал или нет», приходилось переводить взгляд на плитки метрик под графиком. Теперь это читается в самой лупе: - Полоса приёма показывает захват начертанием: в поиске три линии пунктирные, на локе — сплошные и стянуты сверху скобой. Новых цветов не добавлено, та же семантика $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 --- BeaconScopeForm.pas | 69 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 3 deletions(-) diff --git a/BeaconScopeForm.pas b/BeaconScopeForm.pas index 6e9365b..7208760 100644 --- a/BeaconScopeForm.pas +++ b/BeaconScopeForm.pas @@ -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;