mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
feat: QO-100 central beacon BPSK decoder (Milestone 1 — stable demod)
Декодер центрального маяка QO-100 (10489.750, AO-40 BPSK 400 бод): фронтенд до мягких символов + констелляция, с устойчивым захватом несущей/символа. Цепочка (BeaconDecoder.pas): NCO-снос смещения → boxcar-децим до 9600 → RRC матч-фильтр → AGC-нормировка → Gardner timing → Costas BPSK с frequency offload в пред-RRC NCO. squaring-FFT оставлена индикатором несущей (prom). Ключевые решения по итогам эфирной отладки: - invert=True для тракта QO-100 (LNB+трансвертер зеркалит спектр). - Декодер развязан от beacon-lock (тот ретюнил железо, рвал IQ). - AGC-нормировка петель: сигнал ~-60 dBFS замораживал Costas/Gardner. - Frequency offload: накопл. снос Costas стекает в FResidInc (без потолка ±π/символ) → держит ошибку наведения (1пкс≈300Гц) + дрейф LNB. Интеграция: тап IQ в WDSPEngine.PushIQItemToDSP; RadioController владеет декодером, клик-наведение (ПКМ BEACON + ЛКМ/Shift+ЛКМ по спектру); узкий фильтр-маркер в SpectrumView/GL; окно констелляции BeaconScopeForm; файловый лог HOME/ewsdr_beacon.log для диагностики. Milestone 2 (далее): FEC — Viterbi k=7 + RS(160,128) + интерливер + дескремблер. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+162
-2
@@ -49,7 +49,7 @@ interface
|
||||
uses
|
||||
Classes, SysUtils, Math,
|
||||
HPSDRProtocol, HPSDRNetwork, RadioBackend, PlutoBackend, IIOBindings,
|
||||
WDSPEngine, AudioOutput, AudioInput,
|
||||
WDSPEngine, AudioOutput, AudioInput, BeaconDecoder,
|
||||
Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore;
|
||||
|
||||
type
|
||||
@@ -196,6 +196,14 @@ type
|
||||
FBeaconLocked: Boolean; // пик уверенно виден
|
||||
FBeaconCorrCooldown: Integer; // тиков паузы после коррекции (осадка пайплайна)
|
||||
FBeaconSaveCooldown: Integer; // троттлинг записи LOError на диск (тиков)
|
||||
// ---- QO-100 beacon DECODE (BPSK 400 бод, AO-40) — фронтенд+констелляция ----
|
||||
FBeaconDec: TBeaconDecoder; // владеет; тап IQ в WDSPEngine
|
||||
FBeaconDecOn: Boolean; // декодер включён пользователем
|
||||
FBeaconDecHz: Double; // АБСОЛЮТНАЯ display-частота наведения декодера
|
||||
// (узкий фильтр, ставится кликом); 0 = центр/DC
|
||||
FBeaconDecInvert: Boolean; // зеркальный baseband (QO-100/LNB) — деф. True
|
||||
FBeaconLogTick: Integer; // троттл периодического лога состояния декодера
|
||||
FBeaconLogScope: TBeaconScope; // буфер снимка для лога (не аллоцируем в контуре)
|
||||
|
||||
// ---- Аудио / TX ----
|
||||
FVolume: Integer;
|
||||
@@ -477,6 +485,7 @@ type
|
||||
// QO-100 beacon lock — стабилизация дрейфа LNB по опорному маяку.
|
||||
procedure SetBeaconLock(On_: Boolean);
|
||||
procedure ServiceBeaconLock; // одна итерация контура (MeterTimer @10 Гц)
|
||||
procedure BeaconLog(const Msg: string); // диагностика декодера → HOME/ewsdr_beacon.log
|
||||
procedure SetBeaconRefHz(Hz: Double); // опорная частота маяка (downlink)
|
||||
procedure BeaconSeedAtHz(DisplayHz: Double); // пользователь указал маяк кликом
|
||||
function BeaconState: TBeaconState;
|
||||
@@ -486,6 +495,15 @@ type
|
||||
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; // частота наведения (для маркера)
|
||||
procedure SetBeaconDecodeInvert(On_: Boolean); // зеркальный baseband вкл/выкл
|
||||
function BeaconDecodeInvert: Boolean;
|
||||
function GetBeaconScope(out S: TBeaconScope): Boolean;
|
||||
|
||||
// Отображение / приём
|
||||
procedure SetCTun(On_: Boolean);
|
||||
procedure SetDuplex(On_: Boolean);
|
||||
@@ -596,6 +614,16 @@ begin
|
||||
FBeaconLockHz := 0.0;
|
||||
FBeaconLockOn := False;
|
||||
|
||||
// QO-100 beacon DECODE: фронтенд BPSK 400 бод (тап IQ в движке).
|
||||
FBeaconDec := TBeaconDecoder.Create;
|
||||
FBeaconDecOn := False;
|
||||
// Тракт QO-100 (LNB+трансвертер) зеркалит спектр: проверено приборно (symMag ×4
|
||||
// и prom 500-800 при invert=ON против шума при OFF, на наведении точно на маяк).
|
||||
// Декодеру нужно сопряжение, чтобы наведённый offset доставлял маяк к DC.
|
||||
FBeaconDecInvert := True;
|
||||
FBeaconDec.SetInvert(FBeaconDecInvert);
|
||||
FDSPEngine.SetBeaconDecoder(FBeaconDec);
|
||||
|
||||
// Audio out/in — объекты создаём сейчас, Open вызывается позже (после показа
|
||||
// формы / при подключении устройства).
|
||||
FAudioOut := TAudioOutput.Create(48000);
|
||||
@@ -612,7 +640,13 @@ begin
|
||||
end;
|
||||
if Assigned(FAudioOut) then begin FAudioOut.Close; FreeAndNil(FAudioOut); end;
|
||||
if Assigned(FAudioIn) then begin FAudioIn.Close; FreeAndNil(FAudioIn); end;
|
||||
if Assigned(FDSPEngine) then begin FDSPEngine.Close; FreeAndNil(FDSPEngine); end;
|
||||
if Assigned(FDSPEngine) then
|
||||
begin
|
||||
FDSPEngine.SetBeaconDecoder(nil);
|
||||
FDSPEngine.Close;
|
||||
FreeAndNil(FDSPEngine);
|
||||
end;
|
||||
if Assigned(FBeaconDec) then FreeAndNil(FBeaconDec);
|
||||
end;
|
||||
|
||||
destructor TRadioController.Destroy;
|
||||
@@ -1200,6 +1234,45 @@ procedure TRadioController.ServiceBeaconLock;
|
||||
// лок не теряется; во время settle позицию не двигаем (FFT в транзиенте).
|
||||
var win, pk, prom, cand, 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
|
||||
|
||||
// Периодический снимок состояния (раз в ~1с при @10Гц-таймере) в лог-файл.
|
||||
Inc(FBeaconLogTick);
|
||||
if FBeaconLogTick >= 10 then
|
||||
begin
|
||||
FBeaconLogTick := 0;
|
||||
GetBeaconScope(FBeaconLogScope);
|
||||
BeaconLog(Format(
|
||||
'STATE decHz=%.1f center=%.1f off=%.1f | Fs=%.0f Fdec=%.0f sps=%.1f | ' +
|
||||
'resid=%.1f prom=%.1f snr=%.1f baud=%.1f decRMS=%.5f symMag=%.5f ' +
|
||||
'timErr=%.5f wsym=%.4f carHz=%.1f hold=%d | CAR=%d SYM=%d invert=%d lockOn=%d locked=%d',
|
||||
[FBeaconDecHz, FCenterFreq, FBeaconLogScope.OffsetHz,
|
||||
FBeaconLogScope.FsHz, FBeaconLogScope.FdecHz, FBeaconLogScope.Sps,
|
||||
FBeaconLogScope.ResidHz, FBeaconLogScope.CarProm, FBeaconLogScope.SNRdB,
|
||||
FBeaconLogScope.SymRate, FBeaconLogScope.DecRMS, FBeaconLogScope.SymMag,
|
||||
FBeaconLogScope.TimErr, FBeaconLogScope.Wsym, FBeaconLogScope.CarFreqHz,
|
||||
FBeaconLogScope.LockHold,
|
||||
Ord(FBeaconLogScope.CarrierLock), Ord(FBeaconLogScope.SymbolLock),
|
||||
Ord(FBeaconDecInvert), Ord(FBeaconLockOn), Ord(FBeaconLocked)]));
|
||||
end;
|
||||
end;
|
||||
|
||||
if not FBeaconLockOn then Exit;
|
||||
if FCurrentXvtr < 0 then Exit; // только в трансвертер-режиме
|
||||
if FBeaconSaveCooldown > 0 then Dec(FBeaconSaveCooldown);
|
||||
@@ -1310,6 +1383,93 @@ 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;
|
||||
BeaconLog(Format('AIM click: DisplayHz=%.1f CenterFreq=%.1f -> offset=%.1f invert=%d span=%.0f',
|
||||
[DisplayHz, FCenterFreq, FBeaconDecHz - FCenterFreq, Ord(FBeaconDecInvert), FSpanHz]));
|
||||
Changed(rfBeaconLock);
|
||||
end;
|
||||
|
||||
procedure TRadioController.BeaconLog(const Msg: string);
|
||||
// Диагностика декодера маяка: дописываем строку в HOME/ewsdr_beacon.log.
|
||||
// Дёшево и кросс-платформенно; файл потом скидывается на разбор.
|
||||
var
|
||||
f: TextFile;
|
||||
path, home: string;
|
||||
begin
|
||||
home := GetEnvironmentVariable('HOME');
|
||||
if home = '' then home := GetTempDir;
|
||||
path := IncludeTrailingPathDelimiter(home) + 'ewsdr_beacon.log';
|
||||
try
|
||||
AssignFile(f, path);
|
||||
if FileExists(path) then Append(f) else Rewrite(f);
|
||||
try
|
||||
WriteLn(f, FormatDateTime('hh:nn:ss.zzz', Now), ' ', Msg);
|
||||
finally
|
||||
CloseFile(f);
|
||||
end;
|
||||
except
|
||||
// лог не должен ронять контур — глотаем ошибки I/O
|
||||
end;
|
||||
end;
|
||||
|
||||
function TRadioController.BeaconDecodeEnabled: Boolean;
|
||||
begin
|
||||
Result := FBeaconDecOn;
|
||||
end;
|
||||
|
||||
function TRadioController.BeaconDecodeFreqHz: Double;
|
||||
begin
|
||||
Result := FBeaconDecHz;
|
||||
end;
|
||||
|
||||
procedure TRadioController.SetBeaconDecodeInvert(On_: Boolean);
|
||||
begin
|
||||
FBeaconDecInvert := On_;
|
||||
if Assigned(FBeaconDec) then FBeaconDec.SetInvert(On_);
|
||||
Changed(rfBeaconLock);
|
||||
end;
|
||||
|
||||
function TRadioController.BeaconDecodeInvert: Boolean;
|
||||
begin
|
||||
Result := FBeaconDecInvert;
|
||||
end;
|
||||
|
||||
function TRadioController.GetBeaconScope(out S: TBeaconScope): Boolean;
|
||||
begin
|
||||
Result := Assigned(FBeaconDec);
|
||||
if Result then FBeaconDec.GetScope(S)
|
||||
else FillChar(S, SizeOf(S), 0);
|
||||
end;
|
||||
|
||||
procedure TRadioController.ApplyModeFilter;
|
||||
var
|
||||
Lo, Hi, Half: Integer;
|
||||
|
||||
Reference in New Issue
Block a user