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-частота маяка
|
FBeaconTrackHz: Double; // измеренная АБСОЛЮТНАЯ display-частота маяка
|
||||||
// (наведение + остаток декодера) — для оранж. маркера
|
// (наведение + остаток декодера) — для оранж. маркера
|
||||||
FBeaconSNRdB: Double; // SNR из последнего снимка декодера (индикация лока)
|
FBeaconSNRdB: Double; // SNR из последнего снимка декодера (индикация лока)
|
||||||
|
FBeaconBal: Double; // EMA доли меньшинства знаков I (BPSK-ность)
|
||||||
FBeaconLocked: Boolean; // декодер солидно залочен (гистерезис по SNR)
|
FBeaconLocked: Boolean; // декодер солидно залочен (гистерезис по SNR)
|
||||||
FBeaconCorrCooldown: Integer; // тиков паузы после коррекции (осадка ретюна LO)
|
FBeaconCorrCooldown: Integer; // тиков паузы после коррекции (осадка ретюна LO)
|
||||||
FBeaconSaveCooldown: Integer; // троттлинг записи LOError на диск (тиков)
|
FBeaconSaveCooldown: Integer; // троттлинг записи LOError на диск (тиков)
|
||||||
@@ -3300,6 +3301,17 @@ const
|
|||||||
// --- метрика лока (берём из снимка декодера: CarrierLock + SNR) ---
|
// --- метрика лока (берём из снимка декодера: CarrierLock + SNR) ---
|
||||||
BEACON_LOCK_SNR_DB = 3.0; // солидный лок: декодер несёт несущую и SNR выше этого
|
BEACON_LOCK_SNR_DB = 3.0; // солидный лок: декодер несёт несущую и SNR выше этого
|
||||||
BEACON_UNLOCK_SNR_DB = 0.0; // гистерезис: лок гаснет ниже этого
|
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);
|
procedure TRadioController.SetBeaconRefHz(Hz: Double);
|
||||||
begin
|
begin
|
||||||
@@ -3319,6 +3331,8 @@ begin
|
|||||||
FBeaconDec.SetOffsetHz(DisplayHz - FCenterFreq);
|
FBeaconDec.SetOffsetHz(DisplayHz - FCenterFreq);
|
||||||
FBeaconDec.Reseed; // одноразовый грубый захват частоты заново
|
FBeaconDec.Reseed; // одноразовый грубый захват частоты заново
|
||||||
end;
|
end;
|
||||||
|
FBeaconBal := 0.0; // новое наведение — BPSK-ность доказывается заново
|
||||||
|
FBeaconLocked := False;
|
||||||
FBeaconCorrCooldown := BEACON_SETTLE_TICKS; // дать декодеру захватиться до 1-й коррекции
|
FBeaconCorrCooldown := BEACON_SETTLE_TICKS; // дать декодеру захватиться до 1-й коррекции
|
||||||
Changed(rfBeaconLock);
|
Changed(rfBeaconLock);
|
||||||
end;
|
end;
|
||||||
@@ -3335,6 +3349,7 @@ begin
|
|||||||
FBeaconCorrCooldown := 0;
|
FBeaconCorrCooldown := 0;
|
||||||
FBeaconSaveCooldown := 0;
|
FBeaconSaveCooldown := 0;
|
||||||
FBeaconLocked := False;
|
FBeaconLocked := False;
|
||||||
|
FBeaconBal := 0.0;
|
||||||
FBeaconSNRdB := -99.0;
|
FBeaconSNRdB := -99.0;
|
||||||
FBeaconLockHz := 0.0; // сессионный аккумулятор индикации (LOError несёт калибровку)
|
FBeaconLockHz := 0.0; // сессионный аккумулятор индикации (LOError несёт калибровку)
|
||||||
FBeaconDecHz := 0.0; // ещё не наведён — ждём клик по маяку
|
FBeaconDecHz := 0.0; // ещё не наведён — ждём клик по маяку
|
||||||
@@ -3354,6 +3369,20 @@ begin
|
|||||||
Changed(rfBeaconLock);
|
Changed(rfBeaconLock);
|
||||||
end;
|
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;
|
procedure TRadioController.ServiceBeaconLock;
|
||||||
// Одна итерация контура (UI-таймер @10 Гц, MeterTimer). Эталон частоты — ДЕКОДЕР:
|
// Одна итерация контура (UI-таймер @10 Гц, MeterTimer). Эталон частоты — ДЕКОДЕР:
|
||||||
// он точно ведёт несущую маяка своим NCO (RunAcquisition + Costas-offload), и его
|
// он точно ведёт несущую маяка своим NCO (RunAcquisition + Costas-offload), и его
|
||||||
@@ -3365,7 +3394,7 @@ procedure TRadioController.ServiceBeaconLock;
|
|||||||
// baseband-смещение ≈ 0: маяк сдвинулся на −dLock железом, наведение тоже на −dLock).
|
// baseband-смещение ≈ 0: маяк сдвинулся на −dLock железом, наведение тоже на −dLock).
|
||||||
var
|
var
|
||||||
S: TBeaconScope;
|
S: TBeaconScope;
|
||||||
residHz, measured, e, dLock: Double;
|
residHz, measured, e, dLock, Bal: Double;
|
||||||
begin
|
begin
|
||||||
if not FBeaconLockOn then Exit;
|
if not FBeaconLockOn then Exit;
|
||||||
if not Assigned(FBeaconDec) then Exit;
|
if not Assigned(FBeaconDec) then Exit;
|
||||||
@@ -3382,9 +3411,17 @@ begin
|
|||||||
|
|
||||||
FBeaconDec.GetScope(S);
|
FBeaconDec.GetScope(S);
|
||||||
FBeaconSNRdB := FBeaconSNRdB + 0.2 * (S.SNRdB - FBeaconSNRdB);
|
FBeaconSNRdB := FBeaconSNRdB + 0.2 * (S.SNRdB - FBeaconSNRdB);
|
||||||
// Солидный лок: декодер несёт несущую И SNR выше порога (гистерезис против мерцания).
|
// Доля меньшинства по знаку I: у BPSK ≈0.5, у голой несущей ≈0. Кольцо в 256
|
||||||
if S.CarrierLock and (FBeaconSNRdB >= BEACON_LOCK_SNR_DB) then FBeaconLocked := True
|
// точек при 400 Bd набирается за ~0.6 с, EMA добавляет ещё столько же — то есть
|
||||||
else if FBeaconSNRdB < BEACON_UNLOCK_SNR_DB then FBeaconLocked := False;
|
// чужой сигнал отваливается примерно за секунду, а не за десяток (кадр 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 выравнивает ось
|
// Остаток несущей декодера (Гц, относительно наведения). invert выравнивает ось
|
||||||
// декодера с дисплеем → display_маяка = наведение + остаток (знак BEACON_RESID_SIGN).
|
// декодера с дисплеем → display_маяка = наведение + остаток (знак BEACON_RESID_SIGN).
|
||||||
|
|||||||
Reference in New Issue
Block a user