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.