Files
ewsdr/BeaconScopeForm.pas
T
ew8bakandClaude Opus 4.8 afc8775ddb feat: QO-100 beacon AO-40 FEC decode end-to-end (Milestone 2)
Full chain now decodes the QO-100 central beacon to 256-byte AO-40 frames
on-air (frames>0, RS errors=0). Builds on M1 (stable BPSK demod).

FEC backend (BeaconFEC.pas, TBeaconFEC):
- AO-40 FEC: distributed sync (65 bit, step 80) -> 80x65 deinterleave ->
  Viterbi r=1/2 k=7 -> CCSDS descramble -> 2x RS(160,128). Links system
  libfec (Karn) load-time (libfec.so/.dll/.dylib). Viterbi AO-40 [0x4F,-0x6D]
  remapped to libfec [0x6D,0x4F]. Validated against gr-satellites reference
  vectors (fectest2: exact 256-byte frame, 0 mismatches).

DBPSK Manchester frontend (BeaconDecoder.pas):
- Chip-rate (800) processing: RRC matched filter -> ML signal-times-slope
  timing recovery -> Costas -> Manchester combine (block phase select) ->
  differential decode -> soft symbols to FEC.
- Carrier acquisition: squaring-FFT estimates the residual (carrier at +-pi/chip
  defeats decision-directed Costas), one-shot pulls it to DC via residual NCO.
- Costas frequency offload into pre-RRC NCO (with deadband) tracks LNB drift
  without hitting the +-pi Costas clamp.
- ML TED replaces Gardner, which degenerates on the Manchester chip stream.

Temporary on-air diagnostics retained (IQ dump + STATE log) pending wider
signal validation; to be removed before final cleanup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 10:21:33 +03:00

172 lines
5.5 KiB
ObjectPascal

unit BeaconScopeForm;
{ TBeaconScopeForm — окно диагностики QO-100 beacon-декодера (Milestone 1).
Показывает BPSK-констелляцию демодулированного маяка + метрики захвата:
carrier-lock, symbol-lock, SNR, измеренную скорость, снос NCO.
Это смотровое окно фронтенда ДО подключения FEC: если констелляция сжимается
в два чётких сгустка по оси I и горят оба лока — демодуляция корректна, можно
цеплять Viterbi/RS (libfec). Открывается из RX-блока MainForm (ПКМ по BEACON).
Данные тянутся из TRadioController.GetBeaconScope по таймеру (~25 Гц). }
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, Math, StrUtils,
Forms, Controls, Graphics, ExtCtrls,
AppTheme, BeaconDecoder, RadioController;
type
TBeaconScopeForm = class(TForm)
private
FCtl: TRadioController;
FBox: TPaintBox;
FTimer: TTimer;
FTheme: TAppTheme;
procedure DoTick(Sender: TObject);
procedure DoPaint(Sender: TObject);
public
constructor CreateWith(AOwner: TComponent; ACtl: TRadioController);
procedure ApplyTheme(const T: TAppTheme);
end;
implementation
constructor TBeaconScopeForm.CreateWith(AOwner: TComponent; ACtl: TRadioController);
begin
inherited CreateNew(AOwner);
FCtl := ACtl;
FTheme := DarkTheme;
Caption := 'QO-100 Beacon — Constellation';
BorderStyle := bsSizeable;
Width := 360;
Height := 560;
Position := poScreenCenter;
FBox := TPaintBox.Create(Self);
FBox.Parent := Self;
FBox.Align := alClient;
FBox.OnPaint := @DoPaint;
FTimer := TTimer.Create(Self);
FTimer.Interval := 40; // ~25 Гц
FTimer.OnTimer := @DoTick;
FTimer.Enabled := True;
end;
procedure TBeaconScopeForm.ApplyTheme(const T: TAppTheme);
begin
FTheme := T;
if FBox <> nil then FBox.Invalidate;
end;
procedure TBeaconScopeForm.DoTick(Sender: TObject);
begin
if (FBox <> nil) and Visible then FBox.Invalidate;
end;
procedure TBeaconScopeForm.DoPaint(Sender: TObject);
var
C: TCanvas;
W, H, cx, cy, r, i, px, py: Integer;
S: TBeaconScope;
ok: Boolean;
C1, C2: TColor;
st: string;
begin
C := FBox.Canvas;
W := FBox.Width; H := FBox.Height;
// фон
C.Brush.Style := bsSolid;
C.Brush.Color := FTheme.BG;
C.FillRect(0, 0, W, H);
// область констелляции — квадрат сверху, текст снизу
r := (Min(W, H - 70) - 20) div 2;
if r < 20 then r := 20;
cx := W div 2;
cy := 10 + r;
// сетка/оси
C.Pen.Style := psSolid;
C.Pen.Color := FTheme.SpecGrid;
C.Line(cx - r, cy, cx + r, cy); // ось I (горизонт)
C.Line(cx, cy - r, cx, cy + r); // ось Q (вертикаль)
C.Pen.Color := FTheme.Border;
C.Brush.Style := bsClear;
C.Rectangle(cx - r, cy - r, cx + r, cy + r);
// целевые точки BPSK (±1, 0) — ориентир
C.Pen.Color := FTheme.TextDim;
C.Ellipse(cx + r div 2 - 3, cy - 3, cx + r div 2 + 3, cy + 3);
C.Ellipse(cx - r div 2 - 3, cy - 3, cx - r div 2 + 3, cy + 3);
ok := (FCtl <> nil) and FCtl.GetBeaconScope(S);
if ok and S.Enabled then
begin
// точки констелляции (нормированы к ±1; масштаб r/2 → ±1 на половине радиуса)
C.Pen.Style := psClear;
C.Brush.Style := bsSolid;
C.Brush.Color := FTheme.MeterOn;
for i := 0 to S.Count - 1 do
begin
px := cx + Round(S.PtI[i] * (r / 2));
py := cy - Round(S.PtQ[i] * (r / 2));
if (px >= cx - r) and (px <= cx + r) and (py >= cy - r) and (py <= cy + r) then
C.FillRect(px - 1, py - 1, px + 2, py + 2);
end;
end;
// ----- метрики -----
C.Brush.Style := bsClear;
C.Font.Name := 'Courier New';
C.Font.Size := 9;
py := cy + r + 12;
if not ok then
begin
C.Font.Color := FTheme.TextDim;
C.TextOut(14, py, 'decoder unavailable');
Exit;
end;
if not S.Enabled then
begin
C.Font.Color := FTheme.TextDim;
C.TextOut(14, py, 'decoder OFF (enable via BEACON \ decode)');
Exit;
end;
if S.CarrierLock then C1 := FTheme.MeterOn else C1 := FTheme.TextDim;
if S.SymbolLock then C2 := FTheme.MeterOn else C2 := FTheme.TextDim;
C.Font.Color := C1;
C.TextOut(14, py, 'CARRIER ' + IfThen(S.CarrierLock, 'LOCK', '----'));
C.Font.Color := C2;
C.TextOut(14, py + 18, 'SYMBOL ' + IfThen(S.SymbolLock, 'LOCK', '----'));
C.Font.Color := FTheme.Text;
if S.SNRdB > -50 then st := Format('%.1f dB', [S.SNRdB]) else st := '--';
C.TextOut(14, py + 36, 'SNR ' + st);
C.TextOut(14, py + 54, Format('BAUD %.1f (off %.0f Hz)', [S.SymRate, S.OffsetHz]));
C.Font.Color := FTheme.TextDim;
C.TextOut(14, py + 72, Format('CARRIER resid %.0f Hz prom %.0f', [S.ResidHz, S.CarProm]));
if FCtl <> nil then
C.TextOut(14, py + 90, 'INVERT ' + IfThen(FCtl.BeaconDecodeInvert, 'ON', 'OFF'));
C.TextOut(14, py + 108, Format('RATE Fs %.0f Fdec %.0f sps %.1f',
[S.FsHz, S.FdecHz, S.Sps]));
// ----- FEC (AO-40) -----
if S.HasFrame then C.Font.Color := FTheme.MeterOn else C.Font.Color := FTheme.TextDim;
C.TextOut(14, py + 132, Format('FRAMES %d (RS err %d)', [S.Frames, S.LastRSErr]));
C.Font.Color := FTheme.TextDim;
C.TextOut(14, py + 150, Format('MANCHESTER phase %d', [S.ManPhase]));
end;
end.