From 51599b2457443662e51eb7eaeea26c616515459c Mon Sep 17 00:00:00 2001 From: Uladzimir Karpenka Date: Fri, 14 Aug 2026 12:59:46 +0300 Subject: [PATCH] =?UTF-8?q?fix(beacon):=20=D0=BA=D0=BE=D0=BD=D1=82=D1=83?= =?UTF-8?q?=D1=80=20=D0=BF=D1=80=D0=B8=D0=BD=D0=B8=D0=BC=D0=B0=D0=BB=20?= =?UTF-8?q?=D0=B7=D0=B0=20=D0=BC=D0=B0=D1=8F=D0=BA=20=D0=BB=D1=8E=D0=B1?= =?UTF-8?q?=D1=83=D1=8E=20=D1=83=D0=B7=D0=BA=D1=83=D1=8E=20=D0=BD=D0=B5?= =?UTF-8?q?=D1=81=D1=83=D1=89=D1=83=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Наводишься на соседнюю станцию — и контур честно берёт её за маяк и тащит LOError, подтягивая чужой сигнал к 10489.750. Решение принималось по CarrierLock и SNR, а CarrierLock = FCarPresent and FFreqLocked. FCarPresent — prominence сильнейшей линии в squaring-FFT: возведение в квадрат схлопывает модуляцию BPSK ±180° в одну линию на 2·fc, но такую же линию даёт любой сигнал с узкой составляющей — телеграф, пораженка, остаток несущей у цифры. FFreqLocked — Costas держит фазу, а на немодулированную несущую он садится идеально: ровный тон это BPSK без переходов. SNR считается как (E|I|)²/E[Q²], и захваченная Costas'ом несущая лежит целиком на оси I — Q пустой, SNR выходит отличный. То есть чужой сигнал не просто проходил порог, он выглядел лучше маяка. Различаем по констелляции: у настоящего BPSK точки ходят между двумя сгустками ±I (данные после свёрточного кодера равновероятны), у несущей все в одном. BeaconIQBalance считает долю меньшинства по знаку I, EMA сглаживает, порог 0.15 на взятие лока и 0.08 на сброс — гистерезис, как у SNR. Кольцо в 256 точек при 400 Bd набирается за ~0.6 с, EMA добавляет столько же: чужой сигнал отваливается примерно за секунду, а не за десяток, как ждать кадр FEC. FBeaconBal обнуляется вместе с FBeaconLocked при каждом наведении и при включении лока — после клика BPSK-ность доказывается заново, унаследовать её от прошлой цели нельзя. Скорость символов признаком служить не может: FWsym зажат в ±0.02, поэтому S.SymRate конструктивно всегда 392..408 Bd, что бы декодер ни слушал. Непрерывный цифровой сигнал с настоящей модуляцией баланс пройдёт — от него защитит только декодированный кадр AO-40. Сборка ewsdr (--ws=qt6) и ewsdrd — ОК. На эфире не проверено. Co-Authored-By: Claude Opus 5 --- RadioController.pas | 45 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/RadioController.pas b/RadioController.pas index 8cd5e47..ea65100 100644 --- a/RadioController.pas +++ b/RadioController.pas @@ -324,6 +324,7 @@ type FBeaconTrackHz: Double; // измеренная АБСОЛЮТНАЯ display-частота маяка // (наведение + остаток декодера) — для оранж. маркера FBeaconSNRdB: Double; // SNR из последнего снимка декодера (индикация лока) + FBeaconBal: Double; // EMA доли меньшинства знаков I (BPSK-ность) FBeaconLocked: Boolean; // декодер солидно залочен (гистерезис по SNR) FBeaconCorrCooldown: Integer; // тиков паузы после коррекции (осадка ретюна LO) FBeaconSaveCooldown: Integer; // троттлинг записи LOError на диск (тиков) @@ -3300,6 +3301,17 @@ const // --- метрика лока (берём из снимка декодера: CarrierLock + SNR) --- BEACON_LOCK_SNR_DB = 3.0; // солидный лок: декодер несёт несущую и SNR выше этого BEACON_UNLOCK_SNR_DB = 0.0; // гистерезис: лок гаснет ниже этого + // --- «это точно BPSK, а не голая несущая» --- + // CarrierLock+SNR подтверждают лишь то, что в полосе есть узкая линия и Costas + // держит фазу. Ровная несущая (телеграф, пораженка, остаток несущей у цифры) даёт + // ровно это, причём с ОТЛИЧНЫМ SNR: захваченная Costas'ом, она лежит целиком на + // оси I, Q пустой. Контур тогда честно уводил LOError, подтягивая чужой сигнал к + // опорной частоте маяка. Различаем по констелляции: у настоящего BPSK точки ходят + // между двумя сгустками ±I (данные после свёрточного кодера равновероятны), у + // несущей все точки в одном. Меряем долю меньшинства по знаку I. + BEACON_BAL_MIN = 0.15; // ниже — на BPSK не похоже, лок не даём + BEACON_BAL_DROP = 0.08; // гистерезис: уже взятый лок гаснет ниже этого + BEACON_BAL_MIN_PTS = 64; // меньше точек в кольце — судить рано procedure TRadioController.SetBeaconRefHz(Hz: Double); begin @@ -3319,6 +3331,8 @@ begin FBeaconDec.SetOffsetHz(DisplayHz - FCenterFreq); FBeaconDec.Reseed; // одноразовый грубый захват частоты заново end; + FBeaconBal := 0.0; // новое наведение — BPSK-ность доказывается заново + FBeaconLocked := False; FBeaconCorrCooldown := BEACON_SETTLE_TICKS; // дать декодеру захватиться до 1-й коррекции Changed(rfBeaconLock); end; @@ -3335,6 +3349,7 @@ begin FBeaconCorrCooldown := 0; FBeaconSaveCooldown := 0; FBeaconLocked := False; + FBeaconBal := 0.0; FBeaconSNRdB := -99.0; FBeaconLockHz := 0.0; // сессионный аккумулятор индикации (LOError несёт калибровку) FBeaconDecHz := 0.0; // ещё не наведён — ждём клик по маяку @@ -3354,6 +3369,20 @@ begin Changed(rfBeaconLock); end; +function BeaconIQBalance(const S: TBeaconScope): Double; +// Доля меньшинства среди знаков I в кольце констелляции: 0.5 — идеально +// сбалансированный BPSK, 0 — все точки в одном сгустке (немодулированная +// несущая). -1 = точек мало, судить рано. +var + i, Neg: Integer; +begin + if S.Count < BEACON_BAL_MIN_PTS then Exit(-1.0); + Neg := 0; + for i := 0 to S.Count - 1 do + if S.PtI[i] < 0 then Inc(Neg); + Result := Min(Neg, S.Count - Neg) / S.Count; +end; + procedure TRadioController.ServiceBeaconLock; // Одна итерация контура (UI-таймер @10 Гц, MeterTimer). Эталон частоты — ДЕКОДЕР: // он точно ведёт несущую маяка своим NCO (RunAcquisition + Costas-offload), и его @@ -3365,7 +3394,7 @@ procedure TRadioController.ServiceBeaconLock; // baseband-смещение ≈ 0: маяк сдвинулся на −dLock железом, наведение тоже на −dLock). var S: TBeaconScope; - residHz, measured, e, dLock: Double; + residHz, measured, e, dLock, Bal: Double; begin if not FBeaconLockOn then Exit; if not Assigned(FBeaconDec) then Exit; @@ -3382,9 +3411,17 @@ begin FBeaconDec.GetScope(S); FBeaconSNRdB := FBeaconSNRdB + 0.2 * (S.SNRdB - FBeaconSNRdB); - // Солидный лок: декодер несёт несущую И SNR выше порога (гистерезис против мерцания). - if S.CarrierLock and (FBeaconSNRdB >= BEACON_LOCK_SNR_DB) then FBeaconLocked := True - else if FBeaconSNRdB < BEACON_UNLOCK_SNR_DB then FBeaconLocked := False; + // Доля меньшинства по знаку I: у BPSK ≈0.5, у голой несущей ≈0. Кольцо в 256 + // точек при 400 Bd набирается за ~0.6 с, EMA добавляет ещё столько же — то есть + // чужой сигнал отваливается примерно за секунду, а не за десяток (кадр FEC). + Bal := BeaconIQBalance(S); + if Bal >= 0 then FBeaconBal := FBeaconBal + 0.2 * (Bal - FBeaconBal); + // Солидный лок: декодер несёт несущую, SNR выше порога И картинка похожа на + // BPSK (гистерезис по обоим — против мерцания). + if S.CarrierLock and (FBeaconSNRdB >= BEACON_LOCK_SNR_DB) + and (FBeaconBal >= BEACON_BAL_MIN) then FBeaconLocked := True + else if (FBeaconSNRdB < BEACON_UNLOCK_SNR_DB) + or (FBeaconBal < BEACON_BAL_DROP) then FBeaconLocked := False; // Остаток несущей декодера (Гц, относительно наведения). invert выравнивает ось // декодера с дисплеем → display_маяка = наведение + остаток (знак BEACON_RESID_SIGN).