mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
feat: QO-100 decoder-based beacon lock (replace FFT lock)
Replace the display-FFT centroid beacon lock with a decoder-based loop. The beacon decoder already tracks the carrier precisely via its NCO (squaring-FFT acquisition + Costas offload); its residual ResidHz+CarFreqHz is an exact measurement of the beacon's offset from the aim point. Feed that into LOError of the active transverter so the LO retunes to compensate LNB drift — the whole downlink stays put, calibration persists. RadioController: - Remove MeasureBeaconFFT and all display-FFT lock state/constants (lobe/search/track/slew/sym/shape/seed-gain/prom), fields FBeaconManualSeed/FBeaconPromDB/FBeaconDecOn, methods SetBeaconDecode/SetBeaconDecodeAtHz/BeaconDecodeEnabled. - SetBeaconLock is now the master switch (lock = decoder); BeaconSeedAtHz aims the decoder; ServiceBeaconLock measures beacon freq from the decoder scope and trims LOError (gain 0.5, deadband 20 Hz, step clamp 400 Hz, settle 8 ticks). Feed-forward of the aim keeps the decoder locked through the retune (net baseband ~0). Lock/SNR now come from the decoder. MainForm: single BEACON button — left-click toggles lock (+ opens the constellation/bulletin scope, arms the spectrum click for aiming), right-click shows/hides the scope. Spectrum click (armed or Shift) aims via BeaconSeedAtHz. Simplified status field and markers. Both targets build (lazbuild --ws=qt6 + build-ewsdrd.sh). Not yet verified on air. Risk: BEACON_RESID_SIGN=+1.0 — flip to -1.0 if the loop runs away. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+114
-274
@@ -182,25 +182,24 @@ type
|
||||
FCurrentBand: Integer;
|
||||
FCurrentXvtr: Integer;
|
||||
|
||||
// ---- QO-100 beacon lock (центральный BPSK-маяк 10489.750, по display-FFT) ----
|
||||
FBeaconLockOn: Boolean; // лок включён пользователем
|
||||
// ---- QO-100 beacon lock (декодер маяка 10489.750 = эталон частоты) ----
|
||||
// Один концепт: лок = декодер + трим LOError. Декодер точно ведёт несущую,
|
||||
// его измеренный дрейф стекаем в LOError → весь даунлинк стабилен.
|
||||
FBeaconLockOn: Boolean; // лок (=декодер) включён пользователем
|
||||
FBeaconLockHz: Double; // коррекция, накопленная за ТЕКУЩУЮ сессию лока
|
||||
// (только индикация на кнопке); сама поправка
|
||||
// живёт в LOError активного трансвертера
|
||||
FBeaconRefHz: Double; // опорная частота маяка (Hz, downlink), деф. 10489.750e6
|
||||
FBeaconManualSeed: Boolean; // пользователь указал маяк кликом → не автопоиск
|
||||
// FFT-трекинг маяка (робастный, по display-FFT):
|
||||
FBeaconTrackHz: Double; // отслеживаемая АБСОЛЮТНАЯ display-частота маяка
|
||||
// (не baseband-offset — иначе сдвиг центра/водопада ломает окно)
|
||||
FBeaconPromDB: Double; // «высота» пика над шумом окна (лок-метрика)
|
||||
FBeaconLocked: Boolean; // пик уверенно виден
|
||||
FBeaconCorrCooldown: Integer; // тиков паузы после коррекции (осадка пайплайна)
|
||||
FBeaconTrackHz: Double; // измеренная АБСОЛЮТНАЯ display-частота маяка
|
||||
// (наведение + остаток декодера) — для оранж. маркера
|
||||
FBeaconSNRdB: Double; // SNR из последнего снимка декодера (индикация лока)
|
||||
FBeaconLocked: Boolean; // декодер солидно залочен (гистерезис по SNR)
|
||||
FBeaconCorrCooldown: Integer; // тиков паузы после коррекции (осадка ретюна LO)
|
||||
FBeaconSaveCooldown: Integer; // троттлинг записи LOError на диск (тиков)
|
||||
// ---- QO-100 beacon DECODE (BPSK 400 бод, AO-40) — фронтенд+констелляция ----
|
||||
// ---- декодер маяка (BPSK 400 бод, AO-40) — фронтенд+констелляция+бюллетень ----
|
||||
FBeaconDec: TBeaconDecoder; // владеет; тап IQ в WDSPEngine
|
||||
FBeaconDecOn: Boolean; // декодер включён пользователем
|
||||
FBeaconDecHz: Double; // АБСОЛЮТНАЯ display-частота наведения декодера
|
||||
// (узкий фильтр, ставится кликом); 0 = центр/DC
|
||||
// (узкий фильтр, ставится кликом); 0 = ещё не наведён
|
||||
FBeaconDecInvert: Boolean; // зеркальный baseband (QO-100/LNB) — деф. True
|
||||
|
||||
// ---- Аудио / TX ----
|
||||
@@ -358,12 +357,6 @@ type
|
||||
// uplink-LO не подвержен дрейфу LNB).
|
||||
function XvtrTranslateTX(VisibleHz: Double): Double;
|
||||
function ActiveTXFreqHz: Double;
|
||||
// Измерение маяка по display-FFT в окне ±WinHz вокруг ExpectOff (baseband):
|
||||
// центроид лепестка → уточнённое смещение, «высота» пика над средним окна
|
||||
// (лок-метрика). False, если лепесток не похож на маяк (узкий/широкий/
|
||||
// несимметричный сосед) или вне span — тогда трек держим на месте.
|
||||
function MeasureBeaconFFT(ExpectOff, WinHz: Double;
|
||||
out PeakOff, PromDB: Double): Boolean;
|
||||
// Складывает коррекцию маяка dLock в LOError активного трансвертера (живая
|
||||
// калибровка LNB), ретюнит радио и троттлингом сохраняет на диск.
|
||||
procedure ApplyBeaconLOError(dLock: Double);
|
||||
@@ -481,22 +474,18 @@ type
|
||||
procedure SetSampleRate(Hz: Integer);
|
||||
|
||||
// QO-100 beacon lock — стабилизация дрейфа LNB по опорному маяку.
|
||||
procedure SetBeaconLock(On_: Boolean);
|
||||
// QO-100 beacon lock (= декодер + трим LOError; декодер = эталон частоты).
|
||||
procedure SetBeaconLock(On_: Boolean); // вкл/выкл лок (запускает декодер)
|
||||
procedure ServiceBeaconLock; // одна итерация контура (MeterTimer @10 Гц)
|
||||
procedure SetBeaconRefHz(Hz: Double); // опорная частота маяка (downlink)
|
||||
procedure BeaconSeedAtHz(DisplayHz: Double); // пользователь указал маяк кликом
|
||||
procedure BeaconSeedAtHz(DisplayHz: Double); // клик по маяку → наведение декодера
|
||||
function BeaconState: TBeaconState;
|
||||
function BeaconCorrectionHz: Double; // коррекция за сессию лока (индикация; в LOError)
|
||||
function BeaconSNRdB: Double;
|
||||
function BeaconLockEnabled: Boolean;
|
||||
function BeaconRefHz: Double; // опорная частота маяка (для маркера)
|
||||
function BeaconTrackedFreqHz: Double;// отслеживаемая display-частота маяка (центроид)
|
||||
|
||||
// QO-100 beacon DECODE (BPSK 400 бод, AO-40) — фронтенд + констелляция.
|
||||
procedure SetBeaconDecode(On_: Boolean);
|
||||
procedure SetBeaconDecodeAtHz(DisplayHz: Double); // навести узкий фильтр декодера
|
||||
function BeaconDecodeEnabled: Boolean;
|
||||
function BeaconDecodeFreqHz: Double; // частота наведения (для маркера)
|
||||
function BeaconTrackedFreqHz: Double;// измеренная display-частота маяка (наведение+остаток)
|
||||
function BeaconDecodeFreqHz: Double; // частота наведения (для узкого маркера)
|
||||
procedure SetBeaconDecodeInvert(On_: Boolean); // зеркальный baseband вкл/выкл
|
||||
function BeaconDecodeInvert: Boolean;
|
||||
function GetBeaconScope(out S: TBeaconScope): Boolean;
|
||||
@@ -607,14 +596,13 @@ begin
|
||||
// буфер вдвое сокращает построение downsampler в OpenChannel RXA.
|
||||
FDSPEngine := TWDSPEngine.Create(ASampleRate, 48000, 512);
|
||||
|
||||
// QO-100 beacon lock: измерение по display-FFT (см. ServiceBeaconLock).
|
||||
// QO-100 beacon lock: эталон частоты = декодер (см. ServiceBeaconLock).
|
||||
FBeaconRefHz := 10489750000.0; // центральный BPSK-маяк NB-транспондера (downlink)
|
||||
FBeaconLockHz := 0.0;
|
||||
FBeaconLockOn := False;
|
||||
|
||||
// QO-100 beacon DECODE: фронтенд BPSK 400 бод (тап IQ в движке).
|
||||
// Декодер маяка: фронтенд BPSK 400 бод (тап IQ в движке).
|
||||
FBeaconDec := TBeaconDecoder.Create;
|
||||
FBeaconDecOn := False;
|
||||
// Тракт QO-100 (LNB+трансвертер) зеркалит спектр: проверено приборно (symMag ×4
|
||||
// и prom 500-800 при invert=ON против шума при OFF, на наведении точно на маяк).
|
||||
// Декодеру нужно сопряжение, чтобы наведённый offset доставлял маяк к DC.
|
||||
@@ -1051,111 +1039,26 @@ end;
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const
|
||||
// --- контур коррекции ---
|
||||
BEACON_GAIN = 0.9; // гейн контура: feed-forward делает шаг точным,
|
||||
// <1 — запас от перелёта при шумной оценке центроида
|
||||
BEACON_SEED_GAIN = 0.95; // первая коррекция по клику агрессивнее (1 клик → лок)
|
||||
BEACON_DEADBAND_HZ = 75.0; // внутри — коррекции стоп (без вечного подкручивания)
|
||||
BEACON_SETTLE_TICKS = 8; // пауза (~0.8 с @10 Гц) после коррекции — осадка FFT-пайплайна
|
||||
// --- контур коррекции (измерение частоты — от ДЕКОДЕРА, не от display-FFT) ---
|
||||
// Декодер точно ведёт несущую маяка своим NCO (RunAcquisition + Costas-offload):
|
||||
// суммарный остаток ResidHz+CarFreqHz = текущее смещение маяка от точки наведения.
|
||||
// Это смещение медленно стекаем в LOError активного XVTR → ретюн LO компенсирует
|
||||
// дрейф LNB, весь даунлинк стоит. Внутренний NCO декодера держит маяк
|
||||
// декодированным во время плавного ретюна (наведение едет feed-forward за маяком).
|
||||
BEACON_GAIN = 0.5; // гейн контура: <1 — запас от перелёта (декодерная
|
||||
// оценка точная, но ретюн LO имеет латентность пайплайна)
|
||||
BEACON_DEADBAND_HZ = 20.0; // внутри — коррекции стоп (декодер точнее FFT → дедбэнд уже)
|
||||
BEACON_MAX_STEP_HZ = 400.0; // клам один шаг коррекции (защита от выброса оценки)
|
||||
BEACON_SETTLE_TICKS = 8; // пауза (~0.8 с @10 Гц) после коррекции — осадка ретюна LO
|
||||
BEACON_SAVE_TICKS = 30; // троттлинг записи LOError на диск (~3 с @10 Гц)
|
||||
// --- метрика лока (prominence = пик над шумовым полом окна) ---
|
||||
BEACON_LOCK_PROM_DB = 6.0; // загорается LOCK выше этого
|
||||
BEACON_UNLOCK_PROM_DB = 3.0; // гистерезис: гаснет ниже этого
|
||||
// --- окна поиска/слежения ---
|
||||
BEACON_SEARCH_HZ = 6000.0; // ШИРОКОЕ окно захвата (клик/включение/потеря лока)
|
||||
BEACON_TRACK_HZ = 2000.0; // УЗКИЙ gate слежения: сосед дальше — физически вне окна
|
||||
BEACON_MAX_SLEW_HZ = 500.0; // дрейф LNB ≪ этого/тик; больший скачок = чужой сигнал → отвергаем
|
||||
BEACON_TRACK_ALPHA = 0.25; // сглаживание позиции центроида за тик
|
||||
// --- форма лепестка BPSK-маяка (узнаём маяк, а не «самый громкий пик») ---
|
||||
BEACON_LOBE_DB = 10.0; // лепесток = пиксели в пределах этого ниже пика
|
||||
BEACON_LOBE_MIN_HZ = 700.0; // уже → CW-несущая, не BPSK-маяк
|
||||
BEACON_LOBE_MAX_HZ = 5000.0; // шире → слились два сигнала
|
||||
BEACON_SYM_MAX = 0.70; // |L−R|/(L+R) выше → односторонний сосед, не маяк
|
||||
BEACON_SHAPE_MAX_HZPP = 600.0; // форму лепестка проверяем лишь если бин ≤ этого
|
||||
|
||||
function TRadioController.MeasureBeaconFFT(ExpectOff, WinHz: Double;
|
||||
out PeakOff, PromDB: Double): Boolean;
|
||||
// Маяк по display-FFT в окне ±WinHz вокруг ExpectOff (baseband offset; центр =
|
||||
// пиксель n/2). Центральный маяк — BPSK с ПОДАВЛЕННОЙ несущей: в спектре не
|
||||
// линия, а широкая симметричная «полка» (~2.4 кГц), peak-finder по ней скачет.
|
||||
// Позицию берём как POWER-ВЗВЕШЕННЫЙ ЦЕНТРОИД лепестка (пиксели в пределах
|
||||
// BEACON_LOBE_DB от пика) — центр симметричной полки = несущая, стабилен.
|
||||
// PromDB — пик над ШУМОВЫМ ПОЛОМ окна (бины ниже лепестка), не над общим средним:
|
||||
// иначе на узком gate сам лепесток поднимал бы «средний» и занижал метрику.
|
||||
// Проверка ФОРМЫ (ширина + симметрия) отсеивает CW-несущую и одностороннего
|
||||
// соседа: False → лепесток не похож на маяк, вызывающий держит позицию.
|
||||
var
|
||||
pix: array[0..SPECTRUM_PIXELS - 1] of Single;
|
||||
n, i, best, cnt, noiseCnt: Integer;
|
||||
hzPerPix, lo, hi, off, thr, p, wsum, fsum, noiseSum: Double;
|
||||
minOff, maxOff, wLeft, wRight, widthHz, floorDb: Double;
|
||||
bestv: Single;
|
||||
begin
|
||||
Result := False; PeakOff := ExpectOff; PromDB := -99.0;
|
||||
if not FWDSPReady or not Assigned(FDSPEngine) then Exit;
|
||||
n := 0;
|
||||
FDSPEngine.GetSpectrumData(pix, n);
|
||||
if n < 8 then Exit;
|
||||
hzPerPix := FSpanHz / n;
|
||||
lo := ExpectOff - WinHz;
|
||||
hi := ExpectOff + WinHz;
|
||||
// Проход A — пик в окне.
|
||||
best := -1; bestv := -1e30; cnt := 0;
|
||||
for i := 0 to n - 1 do
|
||||
begin
|
||||
off := (i - n / 2) * hzPerPix;
|
||||
if (off < lo) or (off > hi) then Continue;
|
||||
Inc(cnt);
|
||||
if pix[i] > bestv then begin bestv := pix[i]; best := i; end;
|
||||
end;
|
||||
if (best < 0) or (cnt < 4) then Exit;
|
||||
// Проход B — центроид лепестка + протяжённость + шумовой пол (бины вне лепестка).
|
||||
thr := bestv - BEACON_LOBE_DB;
|
||||
wsum := 0; fsum := 0; minOff := 1e30; maxOff := -1e30;
|
||||
noiseSum := 0; noiseCnt := 0;
|
||||
for i := 0 to n - 1 do
|
||||
begin
|
||||
off := (i - n / 2) * hzPerPix;
|
||||
if (off < lo) or (off > hi) then Continue;
|
||||
if pix[i] >= thr then
|
||||
begin
|
||||
p := Power(10.0, (pix[i] - thr) / 10.0); // линейная мощность над порогом
|
||||
wsum := wsum + p;
|
||||
fsum := fsum + p * off;
|
||||
if off < minOff then minOff := off;
|
||||
if off > maxOff then maxOff := off;
|
||||
end
|
||||
else
|
||||
begin
|
||||
noiseSum := noiseSum + pix[i]; Inc(noiseCnt);
|
||||
end;
|
||||
end;
|
||||
if wsum <= 0 then Exit;
|
||||
PeakOff := fsum / wsum;
|
||||
if noiseCnt > 0 then floorDb := noiseSum / noiseCnt
|
||||
else floorDb := bestv - BEACON_LOBE_DB; // весь gate занят лепестком — fallback
|
||||
PromDB := bestv - floorDb;
|
||||
// Форма лепестка — оцениваем ТОЛЬКО при достаточном разрешении (лепесток
|
||||
// разрешается в несколько бинов). На крупном бине (широкий span) форму не
|
||||
// отличить от CW → пропускаем проверку, полагаемся на gate+slew в контуре.
|
||||
if hzPerPix > BEACON_SHAPE_MAX_HZPP then begin Result := True; Exit; end;
|
||||
// Ширина лепестка (BPSK ~2.4 кГц). Узкий = CW-несущая, широкий = слились два.
|
||||
widthHz := maxOff - minOff;
|
||||
if (widthHz < BEACON_LOBE_MIN_HZ) or (widthHz > BEACON_LOBE_MAX_HZ) then Exit;
|
||||
// Симметрия мощности вокруг центроида — односторонний сосед в окне ломает её.
|
||||
wLeft := 0; wRight := 0;
|
||||
for i := 0 to n - 1 do
|
||||
begin
|
||||
off := (i - n / 2) * hzPerPix;
|
||||
if (off < lo) or (off > hi) then Continue;
|
||||
if pix[i] < thr then Continue;
|
||||
p := Power(10.0, (pix[i] - thr) / 10.0);
|
||||
if off < PeakOff then wLeft := wLeft + p else wRight := wRight + p;
|
||||
end;
|
||||
if (wLeft + wRight) > 0 then
|
||||
if Abs(wLeft - wRight) / (wLeft + wRight) > BEACON_SYM_MAX then Exit;
|
||||
Result := True;
|
||||
end;
|
||||
// --- знак отображения остатка декодера ---
|
||||
// invert=True (QO-100/LNB) выравнивает частотную ось декодера с дисплеем, поэтому
|
||||
// display_маяка = наведение + (ResidHz+CarFreqHz). Если на эфире контур убегает —
|
||||
// ось перевёрнута, флипнуть на -1.0 (см. лог: e и дрейф должны иметь один знак).
|
||||
BEACON_RESID_SIGN = 1.0;
|
||||
// --- метрика лока (берём из снимка декодера: CarrierLock + SNR) ---
|
||||
BEACON_LOCK_SNR_DB = 3.0; // солидный лок: декодер несёт несущую и SNR выше этого
|
||||
BEACON_UNLOCK_SNR_DB = 0.0; // гистерезис: лок гаснет ниже этого
|
||||
|
||||
procedure TRadioController.SetBeaconRefHz(Hz: Double);
|
||||
begin
|
||||
@@ -1163,37 +1066,26 @@ begin
|
||||
end;
|
||||
|
||||
procedure TRadioController.BeaconSeedAtHz(DisplayHz: Double);
|
||||
// Пользователь кликнул по маяку → ручной режим (контур ведёт ИМЕННО этот пик,
|
||||
// без автопоиска, который в забитой полосе хватает соседей). Так как клик —
|
||||
// уверенное указание, делаем СРАЗУ почти полную (deadbeat) коррекцию: один клик
|
||||
// ставит маяк на опорную за одну перестройку, без многократного «тыкания».
|
||||
var clickOff, refined, prom, e, dLock: Double;
|
||||
// Пользователь кликнул по маяку → наводим узкий тракт декодера на эту частоту и
|
||||
// переинициализируем петли (RunAcquisition заново оценит несущую). Дальше декодер
|
||||
// сам ведёт маяк своим NCO, а ServiceBeaconLock стекает измеренный дрейф в LOError.
|
||||
begin
|
||||
if not FBeaconLockOn then SetBeaconLock(True);
|
||||
FBeaconManualSeed := True;
|
||||
clickOff := DisplayHz - FCenterFreq;
|
||||
if MeasureBeaconFFT(clickOff, BEACON_SEARCH_HZ, refined, prom) then
|
||||
FBeaconDecHz := DisplayHz;
|
||||
FBeaconTrackHz := DisplayHz;
|
||||
if Assigned(FBeaconDec) then
|
||||
begin
|
||||
FBeaconTrackHz := FCenterFreq + refined;
|
||||
FBeaconPromDB := prom;
|
||||
FBeaconLocked := prom >= BEACON_LOCK_PROM_DB;
|
||||
// deadbeat-коррекция + feed-forward: маяк уедет на −dLock, окно ведём туда же.
|
||||
e := FBeaconTrackHz - FBeaconRefHz;
|
||||
dLock := BEACON_SEED_GAIN * e;
|
||||
ApplyBeaconLOError(dLock); // складываем в LOError + сохраняем
|
||||
FBeaconTrackHz := FBeaconTrackHz - dLock; // ≈ FBeaconRefHz
|
||||
FBeaconCorrCooldown := BEACON_SETTLE_TICKS; // дать FFT отразить новый LO
|
||||
end
|
||||
else
|
||||
begin
|
||||
// маяк под кликом не распознан — просто ставим окно туда, ждём захвата.
|
||||
FBeaconTrackHz := DisplayHz;
|
||||
FBeaconCorrCooldown := BEACON_SETTLE_TICKS;
|
||||
FBeaconDec.SetOffsetHz(DisplayHz - FCenterFreq);
|
||||
FBeaconDec.Reseed; // одноразовый грубый захват частоты заново
|
||||
end;
|
||||
FBeaconCorrCooldown := BEACON_SETTLE_TICKS; // дать декодеру захватиться до 1-й коррекции
|
||||
Changed(rfBeaconLock);
|
||||
end;
|
||||
|
||||
procedure TRadioController.SetBeaconLock(On_: Boolean);
|
||||
var refined, prom: Double;
|
||||
// Один концепт: лок = декодер + трим LOError. Включение запускает декодер маяка;
|
||||
// пользователь кликает по маяку (BeaconSeedAtHz) для наведения. Когда декодер
|
||||
// солидно залочен, ServiceBeaconLock стекает его измеренный дрейф в LOError.
|
||||
begin
|
||||
if On_ = FBeaconLockOn then Exit;
|
||||
FBeaconLockOn := On_;
|
||||
@@ -1201,101 +1093,87 @@ begin
|
||||
begin
|
||||
FBeaconCorrCooldown := 0;
|
||||
FBeaconSaveCooldown := 0;
|
||||
FBeaconManualSeed := False; // авто до первого клика пользователя
|
||||
FBeaconLocked := False;
|
||||
FBeaconPromDB := 0.0;
|
||||
FBeaconLockHz := 0.0; // сессионный аккумулятор индикации (LOError уже несёт калибровку)
|
||||
// Начальная область — у опорной частоты (уточняем пик, если виден).
|
||||
if MeasureBeaconFFT(FBeaconRefHz - FCenterFreq, BEACON_SEARCH_HZ, refined, prom) then
|
||||
FBeaconTrackHz := FCenterFreq + refined
|
||||
else
|
||||
FBeaconTrackHz := FBeaconRefHz;
|
||||
FBeaconSNRdB := -99.0;
|
||||
FBeaconLockHz := 0.0; // сессионный аккумулятор индикации (LOError несёт калибровку)
|
||||
FBeaconDecHz := 0.0; // ещё не наведён — ждём клик по маяку
|
||||
FBeaconTrackHz := FBeaconRefHz;
|
||||
if Assigned(FBeaconDec) then
|
||||
begin
|
||||
FBeaconDec.SetOffsetHz(0);
|
||||
FBeaconDec.SetEnabled(True); // ResetDSP внутри
|
||||
end;
|
||||
end
|
||||
else
|
||||
PersistBeaconCalibration; // фиксируем накопленную коррекцию в LOError (см. метод)
|
||||
begin
|
||||
if Assigned(FBeaconDec) then FBeaconDec.SetEnabled(False);
|
||||
FBeaconDecHz := 0.0;
|
||||
PersistBeaconCalibration; // финальный flush LOError на диск
|
||||
end;
|
||||
Changed(rfBeaconLock);
|
||||
end;
|
||||
|
||||
procedure TRadioController.ServiceBeaconLock;
|
||||
// Одна итерация контура (UI-таймер @10 Гц, MeterTimer). Меряет маяк по
|
||||
// display-FFT и (после ручного клика) трим LOError так, чтобы маяк стоял на
|
||||
// опорной частоте → весь downlink стабилен. Коррекция складывается в LOError
|
||||
// (живая калибровка LNB: видна в настройках, сохраняется).
|
||||
//
|
||||
// Устойчивость к соседним сигналам (как в SDR Console):
|
||||
// • при локе окно УЗКОЕ (±BEACON_TRACK_HZ) — далёкий сосед вне окна;
|
||||
// • slew-отбраковка: дрейф LNB медленный, резкий скачок центроида = чужой
|
||||
// сигнал, измерение игнорируем;
|
||||
// • MeasureBeaconFFT проверяет форму лепестка (узнаём маяк, не «громкий пик»).
|
||||
// Быстрая сходимость при большой ошибке:
|
||||
// • feed-forward (FBeaconTrackHz −= dLock) — окно едет за маяком сквозь ретюн,
|
||||
// лок не теряется; во время settle позицию не двигаем (FFT в транзиенте).
|
||||
var win, pk, prom, cand, e, dLock: Double;
|
||||
// Одна итерация контура (UI-таймер @10 Гц, MeterTimer). Эталон частоты — ДЕКОДЕР:
|
||||
// он точно ведёт несущую маяка своим NCO (RunAcquisition + Costas-offload), и его
|
||||
// суммарный остаток ResidHz+CarFreqHz = текущее смещение маяка от точки наведения.
|
||||
// Измеренную АБСОЛЮТНУЮ частоту маяка (наведение + остаток) подтягиваем к опорной,
|
||||
// стекая ошибку в LOError активного XVTR → ретюн LO компенсирует дрейф LNB, весь
|
||||
// даунлинк стоит. Наведение декодера едет feed-forward за маяком (FBeaconDecHz),
|
||||
// так что внутренний NCO держит маяк декодированным сквозь плавный ретюн (нетто
|
||||
// baseband-смещение ≈ 0: маяк сдвинулся на −dLock железом, наведение тоже на −dLock).
|
||||
var
|
||||
S: TBeaconScope;
|
||||
residHz, measured, e, dLock: Double;
|
||||
begin
|
||||
// Beacon-декодер: ЦЕЛЬ — маяк на DC IQ-тапа (= центр RX-спектра, тот же сырой
|
||||
// DDC-IQ). По умолчанию offset=0: пользователь ставит маяк в центр спектра
|
||||
// (CTUN/перестройка), squaring-FFT acquisition доводит остаток. ТОЛЬКО когда
|
||||
// beacon-lock реально захвачен (зелёный) — доверяем его оценке и сносим
|
||||
// FBeaconTrackHz−FCenterFreq (авто-центровка). Развязано от сырого FBeaconTrackHz,
|
||||
// т.к. при выключенном/несошедшемся локе он недостоверен.
|
||||
// Декодер НЕ зависит от beacon-lock: lock физически ретюнит железо
|
||||
// (ApplyBeaconLOError → PushNetworkState), что рвёт IQ-поток и сбивает петли
|
||||
// декодера (Costas/FLL/Gardner), которым нужны секунды непрерывного сигнала.
|
||||
// У декодера своя петля несущей — она точнее, чем display-FFT-lock. Поэтому
|
||||
// offset = только явный клик-аим, иначе DC. Авто-offset от FBeaconTrackHz убран.
|
||||
if FBeaconDecOn and Assigned(FBeaconDec) then
|
||||
begin
|
||||
if FBeaconDecHz > 0 then
|
||||
FBeaconDec.SetOffsetHz(FBeaconDecHz - FCenterFreq) // наведённый узкий фильтр
|
||||
else
|
||||
FBeaconDec.SetOffsetHz(0); // иначе — центр/DC
|
||||
end;
|
||||
|
||||
if not FBeaconLockOn then Exit;
|
||||
if FCurrentXvtr < 0 then Exit; // только в трансвертер-режиме
|
||||
if not Assigned(FBeaconDec) then Exit;
|
||||
|
||||
// Держим наведение декодера через АКТУАЛЬНЫЙ FCenterFreq (панорама/CTUN не ломают
|
||||
// снос). До клика (FBeaconDecHz=0) — DC; acquisition попробует поймать у центра.
|
||||
if FBeaconDecHz > 0 then
|
||||
FBeaconDec.SetOffsetHz(FBeaconDecHz - FCenterFreq)
|
||||
else
|
||||
FBeaconDec.SetOffsetHz(0);
|
||||
|
||||
if FCurrentXvtr < 0 then Exit; // трим LOError только в XVTR-режиме
|
||||
if FBeaconSaveCooldown > 0 then Dec(FBeaconSaveCooldown);
|
||||
// Окно — вокруг ТЕКУЩЕЙ display-позиции маяка, пересчитанной в baseband через
|
||||
// актуальный FCenterFreq (сдвиг водопада/центра окно не ломает). Узкое при локе.
|
||||
if FBeaconLocked then win := BEACON_TRACK_HZ else win := BEACON_SEARCH_HZ;
|
||||
if not MeasureBeaconFFT(FBeaconTrackHz - FCenterFreq, win, pk, prom) then
|
||||
|
||||
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;
|
||||
|
||||
// Остаток несущей декодера (Гц, относительно наведения). invert выравнивает ось
|
||||
// декодера с дисплеем → display_маяка = наведение + остаток (знак BEACON_RESID_SIGN).
|
||||
residHz := BEACON_RESID_SIGN * (S.ResidHz + S.CarFreqHz);
|
||||
if FBeaconDecHz > 0 then
|
||||
begin
|
||||
Changed(rfBeaconLock); // лепесток не виден/не маяк — держим позицию и лок
|
||||
Exit;
|
||||
end;
|
||||
cand := FCenterFreq + pk;
|
||||
measured := FBeaconDecHz + residHz; // абсолютная display-частота маяка
|
||||
FBeaconTrackHz := measured; // для оранжевого маркера
|
||||
end
|
||||
else
|
||||
measured := FBeaconTrackHz;
|
||||
|
||||
// Slew-отбраковка (только при локе/узком окне): скачок больше дрейфа LNB =
|
||||
// чужой сигнал в окне → измерение игнорируем, остаёмся на маяке.
|
||||
if FBeaconLocked and (Abs(cand - FBeaconTrackHz) > BEACON_MAX_SLEW_HZ) then
|
||||
begin
|
||||
Changed(rfBeaconLock);
|
||||
Exit;
|
||||
end;
|
||||
|
||||
// Сопровождение центроида. Во время settle НЕ двигаем — FFT ещё отражает
|
||||
// старый LO (доверяем feed-forward-предсказанию позиции).
|
||||
if FBeaconCorrCooldown = 0 then
|
||||
FBeaconTrackHz := FBeaconTrackHz + BEACON_TRACK_ALPHA * (cand - FBeaconTrackHz);
|
||||
FBeaconPromDB := FBeaconPromDB + 0.2 * (prom - FBeaconPromDB);
|
||||
// Гистерезис лока: загорается при LOCK_PROM, гаснет ниже UNLOCK_PROM (без мерцания).
|
||||
if FBeaconPromDB >= BEACON_LOCK_PROM_DB then FBeaconLocked := True
|
||||
else if FBeaconPromDB < BEACON_UNLOCK_PROM_DB then FBeaconLocked := False;
|
||||
|
||||
// Коррекция: НЕ каждый тик (display-FFT отстаёт от ретюна → перелёт). После
|
||||
// коррекции ждём BEACON_SETTLE_TICKS. Только после ручного клика (manual seed).
|
||||
// Коррекция: не каждый тик (ретюн LO имеет латентность пайплайна). После коррекции
|
||||
// ждём BEACON_SETTLE_TICKS. Только при солидном локе и после наведения кликом.
|
||||
if FBeaconCorrCooldown > 0 then
|
||||
Dec(FBeaconCorrCooldown)
|
||||
else if FBeaconLocked and FBeaconManualSeed then
|
||||
else if FBeaconLocked and (FBeaconDecHz > 0) then
|
||||
begin
|
||||
// e = видимое_смещение_маяка − опорное. d(display)/d(LOError) = −1, поэтому
|
||||
// LOError += e тянет маяк к опорной; feed-forward предсказывает новую
|
||||
// display-позицию (track −= dLock), чтобы окно ехало за маяком.
|
||||
e := FBeaconTrackHz - FBeaconRefHz;
|
||||
e := measured - FBeaconRefHz;
|
||||
if Abs(e) > BEACON_DEADBAND_HZ then
|
||||
begin
|
||||
dLock := BEACON_GAIN * e;
|
||||
ApplyBeaconLOError(dLock); // в LOError + ретюн + троттл-сохранение
|
||||
FBeaconTrackHz := FBeaconTrackHz - dLock;
|
||||
if dLock > BEACON_MAX_STEP_HZ then dLock := BEACON_MAX_STEP_HZ;
|
||||
if dLock < -BEACON_MAX_STEP_HZ then dLock := -BEACON_MAX_STEP_HZ;
|
||||
ApplyBeaconLOError(dLock); // в LOError + ретюн + троттл-сохранение
|
||||
// Маяк уедет на −dLock (display); ведём наведение туда же feed-forward, чтобы
|
||||
// baseband декодера не дёрнулся и лок не потерялся.
|
||||
FBeaconDecHz := measured - dLock;
|
||||
FBeaconTrackHz := FBeaconDecHz;
|
||||
FBeaconDec.SetOffsetHz(FBeaconDecHz - FCenterFreq);
|
||||
FBeaconCorrCooldown := BEACON_SETTLE_TICKS;
|
||||
end;
|
||||
end;
|
||||
@@ -1343,7 +1221,7 @@ end;
|
||||
|
||||
function TRadioController.BeaconSNRdB: Double;
|
||||
begin
|
||||
Result := FBeaconPromDB; // «высота» пика над шумом окна (dB)
|
||||
Result := FBeaconSNRdB; // SNR последнего снимка декодера (dB)
|
||||
end;
|
||||
|
||||
function TRadioController.BeaconLockEnabled: Boolean;
|
||||
@@ -1361,44 +1239,6 @@ begin
|
||||
Result := FBeaconTrackHz;
|
||||
end;
|
||||
|
||||
procedure TRadioController.SetBeaconDecode(On_: Boolean);
|
||||
// Включает фронтенд-декодер маяка. Цель — маяк на DC (центр RX-спектра): стартуем
|
||||
// с offset=0 (или явного клик-аима), дальше частоту держит петля несущей декодера.
|
||||
// От beacon-lock декодер НЕ зависит (см. ServiceBeaconLock). Пользователь наводит
|
||||
// клик на маяк либо центрирует его в спектре, acquisition/Costas доводят.
|
||||
begin
|
||||
FBeaconDecOn := On_;
|
||||
if not Assigned(FBeaconDec) then Exit;
|
||||
if not On_ then FBeaconDecHz := 0; // сброс наведения при выключении
|
||||
if On_ then
|
||||
begin
|
||||
if FBeaconDecHz > 0 then
|
||||
FBeaconDec.SetOffsetHz(FBeaconDecHz - FCenterFreq)
|
||||
else
|
||||
FBeaconDec.SetOffsetHz(0);
|
||||
end;
|
||||
FBeaconDec.SetEnabled(On_);
|
||||
end;
|
||||
|
||||
procedure TRadioController.SetBeaconDecodeAtHz(DisplayHz: Double);
|
||||
// Пользователь навёл узкий фильтр на маяк (клик по спектру). Запоминаем абсолютную
|
||||
// частоту, сносим её к DC и переинициализируем петли (одноразовая грубая оценка
|
||||
// заново). Снос держится в ServiceBeaconLock при перестройке/панораме.
|
||||
begin
|
||||
FBeaconDecHz := DisplayHz;
|
||||
if Assigned(FBeaconDec) then
|
||||
begin
|
||||
FBeaconDec.SetOffsetHz(FBeaconDecHz - FCenterFreq);
|
||||
FBeaconDec.Reseed;
|
||||
end;
|
||||
Changed(rfBeaconLock);
|
||||
end;
|
||||
|
||||
function TRadioController.BeaconDecodeEnabled: Boolean;
|
||||
begin
|
||||
Result := FBeaconDecOn;
|
||||
end;
|
||||
|
||||
function TRadioController.BeaconDecodeFreqHz: Double;
|
||||
begin
|
||||
Result := FBeaconDecHz;
|
||||
|
||||
Reference in New Issue
Block a user