mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
fix(beacon): контур принимал за маяк любую узкую несущую
Наводишься на соседнюю станцию — и контур честно берёт её за маяк и тащит 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 <noreply@anthropic.com>
This commit is contained in:
+41
-4
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user