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.
