mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
Merge feature/qo100-decoder-lock: decoder-based QO-100 beacon lock
Replace the display-FFT centroid beacon lock with a decoder-based loop that trims the active transverter's LOError from the beacon decoder's precise NCO carrier estimate, compensating LNB drift so the whole downlink (incl. FT8) stays put. Single BEACON button (lock = decoder). Also unifies the beacon decoder filter marker between the CPU and OpenGL spectrum renderers. Verified on air: lock holds, FT8 decodes cleanly.
This commit is contained in:
+27
-39
@@ -135,7 +135,6 @@ type
|
||||
FSpecDragX0: Integer; // X при нажатии
|
||||
FSpecDragFreq: Double; // FCenterFreq при нажатии
|
||||
FBeaconArming: Boolean; // ждём клик по маяку (после включения BEACON)
|
||||
FBeaconDecArming: Boolean; // ждём клик: навести узкий фильтр декодера на маяк
|
||||
FSpectrumDirty: Boolean; // таймер должен перерисовать спектр/водопад
|
||||
// TX DUC-очередь (FDUCPending*) переехала в TRadioController (OnTXIQ).
|
||||
FSMeterPeak: Double; // верхняя граница светлой зоны
|
||||
@@ -3278,15 +3277,12 @@ function TMainForm.BeaconStatusText: string;
|
||||
// Компактный статус QO-100 beacon lock для поля 7 статус-бара (Pluto). Courier 8pt,
|
||||
// поле ~200px → держим ≤ ~24 символов.
|
||||
begin
|
||||
if FBeaconDecArming then Exit('DEC: click beacon');
|
||||
if FController.BeaconDecodeEnabled and (FController.BeaconDecodeFreqHz > 0) then
|
||||
Exit('DEC: shift-click=re-aim');
|
||||
if not FController.BeaconLockEnabled then Exit('BCN: off');
|
||||
if FBeaconArming then Exit('BCN: click beacon');
|
||||
if FController.BeaconDecodeFreqHz <= 0 then Exit('BCN: click beacon');
|
||||
case FController.BeaconState of
|
||||
bsLock: Result := Format('BCN LOCK %.0fHz /%.0f',
|
||||
bsLock: Result := Format('BCN LOCK %.0fHz /%.0fdB',
|
||||
[FController.BeaconCorrectionHz, FController.BeaconSNRdB]);
|
||||
bsSearch: Result := Format('BCN search /%.0f', [FController.BeaconSNRdB]);
|
||||
bsSearch: Result := Format('BCN sync /%.0fdB', [FController.BeaconSNRdB]);
|
||||
else Result := 'BCN --';
|
||||
end;
|
||||
end;
|
||||
@@ -3989,34 +3985,34 @@ begin
|
||||
end;
|
||||
|
||||
procedure TMainForm.BtnBeaconClick(Sender: TObject);
|
||||
// ЛКМ по BEACON: вкл/выкл лок маяка (= декодер + трим LOError). При включении
|
||||
// «вооружаем» клик (следующий ЛКМ по спектру наведёт декодер на маяк) и открываем
|
||||
// окно констелляции/бюллетеня. Декодер ведёт несущую, контур стекает дрейф в LOError.
|
||||
begin
|
||||
FController.SetBeaconLock(not FController.BeaconLockEnabled);
|
||||
// При включении — «вооружаем» клик: следующий ЛКМ по спектру укажет маяк
|
||||
// (надёжнее автопоиска в забитой полосе). Авто-сид работает до клика.
|
||||
FBeaconArming := FController.BeaconLockEnabled;
|
||||
if FController.BeaconLockEnabled then
|
||||
begin
|
||||
if FBeaconScopeForm = nil then
|
||||
FBeaconScopeForm := TBeaconScopeForm.CreateWith(Self, FController);
|
||||
FBeaconScopeForm.ApplyTheme(DarkTheme);
|
||||
FBeaconScopeForm.Show;
|
||||
end;
|
||||
UpdateBeaconButton;
|
||||
end;
|
||||
|
||||
procedure TMainForm.BtnBeaconMouseDown(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
// ПКМ по BEACON: включает фронтенд-декодер маяка (BPSK 400 бод) и открывает окно
|
||||
// констелляции для диагностики демодуляции (Milestone 1 — до подключения FEC).
|
||||
// ПКМ по BEACON: показать/скрыть окно констелляции+бюллетеня (диагностика декодера).
|
||||
// Лок не трогает — он на ЛКМ.
|
||||
begin
|
||||
if Button <> mbRight then Exit;
|
||||
FController.SetBeaconDecode(not FController.BeaconDecodeEnabled);
|
||||
if FController.BeaconDecodeEnabled then
|
||||
begin
|
||||
// «Вооружаем» наведение: следующий ЛКМ по спектру поставит узкий фильтр
|
||||
// декодера на маяк. Потом перенавести — Shift+ЛКМ по спектру.
|
||||
FBeaconDecArming := True;
|
||||
if FBeaconScopeForm = nil then
|
||||
FBeaconScopeForm := TBeaconScopeForm.CreateWith(Self, FController);
|
||||
FBeaconScopeForm.ApplyTheme(DarkTheme);
|
||||
FBeaconScopeForm.Show;
|
||||
end
|
||||
else
|
||||
FBeaconDecArming := False;
|
||||
UpdateBeaconButton;
|
||||
if not FController.BeaconLockEnabled then Exit;
|
||||
if FBeaconScopeForm = nil then
|
||||
FBeaconScopeForm := TBeaconScopeForm.CreateWith(Self, FController);
|
||||
FBeaconScopeForm.ApplyTheme(DarkTheme);
|
||||
if FBeaconScopeForm.Visible then FBeaconScopeForm.Hide
|
||||
else FBeaconScopeForm.Show;
|
||||
end;
|
||||
|
||||
procedure TMainForm.UpdateBeaconButton;
|
||||
@@ -4033,7 +4029,7 @@ begin
|
||||
FController.BeaconTrackedFreqHz);
|
||||
// Узкий фильтр-маркер декодера (±450 Гц ≈ полоса BPSK-маяка).
|
||||
FSpecView.SetBeaconDecMarker(
|
||||
FController.BeaconDecodeEnabled and (FController.BeaconDecodeFreqHz > 0),
|
||||
FController.BeaconLockEnabled and (FController.BeaconDecodeFreqHz > 0),
|
||||
FController.BeaconDecodeFreqHz, 450.0);
|
||||
end;
|
||||
UpdateStatusField7; // моментальный апдейт статус-бара (не ждём MeterTimer)
|
||||
@@ -4223,19 +4219,11 @@ begin
|
||||
|
||||
if Button = mbLeft then
|
||||
begin
|
||||
// Наведение узкого фильтра ДЕКОДЕРА на маяк: после ПКМ BEACON (вооружено)
|
||||
// либо Shift+ЛКМ в любой момент при включённом декодере. Не трогает VFO.
|
||||
if FController.BeaconDecodeEnabled and (PbSpectrum.Width > 0) and
|
||||
(FBeaconDecArming or (ssShift in Shift)) then
|
||||
begin
|
||||
FController.SetBeaconDecodeAtHz(
|
||||
PixelToFreq(X, PbSpectrum.Width, FController.FCenterFreq, FController.FSpanHz));
|
||||
FBeaconDecArming := False;
|
||||
UpdateBeaconButton;
|
||||
Exit;
|
||||
end;
|
||||
// Наведение beacon-lock: ЛКМ указывает на маяк (вместо перестройки VFO).
|
||||
if FBeaconArming and (PbSpectrum.Width > 0) then
|
||||
// Наведение маяка: ЛКМ указывает на маяк (после включения BEACON «вооружено»,
|
||||
// либо Shift+ЛКМ в любой момент при включённом локе для перенаведения). Декодер
|
||||
// наводится на эту частоту, дальше сам ведёт несущую. Не трогает VFO.
|
||||
if FController.BeaconLockEnabled and (PbSpectrum.Width > 0) and
|
||||
(FBeaconArming or (ssShift in Shift)) then
|
||||
begin
|
||||
FController.BeaconSeedAtHz(
|
||||
PixelToFreq(X, PbSpectrum.Width, FController.FCenterFreq, FController.FSpanHz));
|
||||
|
||||
+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;
|
||||
|
||||
+5
-9
@@ -526,23 +526,19 @@ procedure TSpectrumView.DrawBeaconMarkers(C: TCanvas; W, H: Integer);
|
||||
C.Pen.Color := Col; C.Pen.Width := 1; C.Pen.Style := Style;
|
||||
C.MoveTo(x, 0); C.LineTo(x, H);
|
||||
end;
|
||||
var
|
||||
xc, x1, x2: Integer;
|
||||
begin
|
||||
if FBeaconMarkActive then
|
||||
begin
|
||||
Vline(FBeaconRefFreq, clLime, psDot); // где маяк ДОЛЖЕН быть
|
||||
Vline(FBeaconTrkFreq, TColor($000AA5FF), psSolid); // отслеживаемый центроид (оранж)
|
||||
end;
|
||||
// Узкий фильтр-маркер декодера: жёлтые грани полосы + центральная линия.
|
||||
// Узкий фильтр-маркер декодера: грани полосы + центральная линия. Три линии без
|
||||
// полупрозрачной заливки (как в GL-рендере) — единый вид и дешевле для CPU.
|
||||
if FBeaconDecActive and (FSpanHz > 0) then
|
||||
begin
|
||||
xc := Round((FBeaconDecFreq - (FCenterFreq - FSpanHz / 2)) / FSpanHz * W);
|
||||
x1 := Round((FBeaconDecFreq - FBeaconDecHalf - (FCenterFreq - FSpanHz / 2)) / FSpanHz * W);
|
||||
x2 := Round((FBeaconDecFreq + FBeaconDecHalf - (FCenterFreq - FSpanHz / 2)) / FSpanHz * W);
|
||||
BlendBand(x1, x2, H, $FF, $D0, $20, 48); // полупрозрачная полоса
|
||||
C.Pen.Color := TColor($0020D0FF); C.Pen.Width := 1; C.Pen.Style := psSolid;
|
||||
if (xc >= 0) and (xc < W) then begin C.MoveTo(xc, 0); C.LineTo(xc, H); end;
|
||||
Vline(FBeaconDecFreq - FBeaconDecHalf, TColor($0020D0FF), psSolid); // грань фильтра
|
||||
Vline(FBeaconDecFreq + FBeaconDecHalf, TColor($0020D0FF), psSolid);
|
||||
Vline(FBeaconDecFreq, TColor($0020D0FF), psSolid); // центр наведения
|
||||
end;
|
||||
C.Pen.Style := psSolid;
|
||||
end;
|
||||
|
||||
Reference in New Issue
Block a user