mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:27:33 +00:00
Decode and show the AO-40 frame payload as text in the beacon scope window. - RadioController.GetBeaconFrame exposes the latest 256-byte frame. - BeaconScopeForm: poll the frame counter; on each new frame, parse the payload (drop trailing 2-byte CRC, printable ASCII, 64-char lines, as in gr-satellites qo100.parse) and append to a scrolling read-only memo. - Window widened/heightened; constellation size capped to leave room. Frame format verified offline against a real on-air capture: the decoder produces the live QO-100 AMSAT bulletin text (RS errors=0). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
235 lines
7.8 KiB
ObjectPascal
235 lines
7.8 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, StdCtrls,
|
|
AppTheme, BeaconDecoder, BeaconFEC, RadioController;
|
|
|
|
type
|
|
TBeaconScopeForm = class(TForm)
|
|
private
|
|
FCtl: TRadioController;
|
|
FBox: TPaintBox;
|
|
FMemo: TMemo; // декодированный текст бюллетеня (AO-40 кадры)
|
|
FTimer: TTimer;
|
|
FTheme: TAppTheme;
|
|
FLastFrames: Int64; // счётчик кадров на прошлом тике (детект нового)
|
|
procedure DoTick(Sender: TObject);
|
|
procedure DoPaint(Sender: TObject);
|
|
procedure PollFrame;
|
|
function FrameToText(const Frame: TBeaconFrame): string;
|
|
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 + Bulletin';
|
|
BorderStyle := bsSizeable;
|
|
Width := 600;
|
|
Height := 680;
|
|
Position := poScreenCenter;
|
|
|
|
// Текст бюллетеня (декодированные AO-40 кадры) — снизу, моноширинный, ReadOnly.
|
|
FMemo := TMemo.Create(Self);
|
|
FMemo.Parent := Self;
|
|
FMemo.Align := alBottom;
|
|
FMemo.Height := 230;
|
|
FMemo.ReadOnly := True;
|
|
FMemo.ScrollBars := ssVertical;
|
|
FMemo.WordWrap := False;
|
|
FMemo.Font.Name := 'Courier New';
|
|
FMemo.Font.Size := 9;
|
|
FMemo.Color := FTheme.BG;
|
|
FMemo.Font.Color := FTheme.Text;
|
|
|
|
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 FMemo <> nil then
|
|
begin
|
|
FMemo.Color := FTheme.BG;
|
|
FMemo.Font.Color := FTheme.Text;
|
|
end;
|
|
if FBox <> nil then FBox.Invalidate;
|
|
end;
|
|
|
|
procedure TBeaconScopeForm.DoTick(Sender: TObject);
|
|
begin
|
|
if not Visible then Exit;
|
|
PollFrame;
|
|
if FBox <> nil then FBox.Invalidate;
|
|
end;
|
|
|
|
function TBeaconScopeForm.FrameToText(const Frame: TBeaconFrame): string;
|
|
// AO-40 кадр QO-100 = ASCII-текст (space-padded), последние 2 байта = CRC-16.
|
|
// Как gr-satellites qo100.parse: отбрасываем CRC, печатные ASCII, строки по 64.
|
|
var
|
|
i, b: Integer;
|
|
line: string;
|
|
begin
|
|
Result := '';
|
|
i := 0;
|
|
while i <= 253 do
|
|
begin
|
|
line := '';
|
|
for b := i to Min(i + 63, 253) do
|
|
if (Frame[b] >= 32) and (Frame[b] < 127) then line := line + Chr(Frame[b])
|
|
else line := line + ' ';
|
|
Result := Result + TrimRight(line) + LineEnding;
|
|
Inc(i, 64);
|
|
end;
|
|
end;
|
|
|
|
procedure TBeaconScopeForm.PollFrame;
|
|
// Раз/тик: если декодер выдал НОВЫЙ кадр (S.Frames вырос) — печатаем бюллетень.
|
|
var
|
|
S: TBeaconScope;
|
|
Frame: TBeaconFrame;
|
|
rs: Integer;
|
|
begin
|
|
if FCtl = nil then Exit;
|
|
if not FCtl.GetBeaconScope(S) then Exit;
|
|
if S.Frames <= FLastFrames then Exit;
|
|
FLastFrames := S.Frames;
|
|
if not FCtl.GetBeaconFrame(Frame, rs) then Exit;
|
|
FMemo.Append(Format('--- frame %d (RS err %d) ---', [S.Frames, rs]));
|
|
FMemo.Append(FrameToText(Frame));
|
|
while FMemo.Lines.Count > 400 do FMemo.Lines.Delete(0); // ограничиваем рост
|
|
FMemo.SelStart := Length(FMemo.Text); // автоскролл вниз
|
|
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;
|
|
if r > 110 then r := 110; // кап: оставляем место метрикам над memo
|
|
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.
|