{
  Copyright (C)
  2026 - Uladzimir Karpenka, EW8BAK

  This program is free software; you can redistribute it and/or
  modify it under the terms of the GNU General Public License
  as published by the Free Software Foundation; either version 2
  of the License, or (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
}

unit BeaconFEC;

{
  QO-100 центральный маяк — FEC-бэкенд (Milestone 2).

  Декодирует AO-40 FEC поверх потока мягких символов (400/с, после Manchester+
  дифф-декода фронтенда). Цепочка (ровно как gr-satellites ao40_fec_deframer):

    мягкие символы 400/с
      → distributed sync (65 бит, шаг 80, окно 5200, порог 8, обе полярности)
      → matrix deinterleave 80×65: out[i]=data[80*(i%65)+i/65], skip 65, take 5132
      → Viterbi r=1/2 k=7, полиномы [0x4F, -0x6D] (CCSDS), terminated → 2560 бит/320 байт
      → CCSDS additive descramble (LFSR mask 0xA9, seed 0xFF, len 7, XOR по битам)
      → 2× RS(255,223) укороч. pad=95 → (160,128), conventional basis, 2-way интерлив
      → 256 байт кадра → OnFrame.

  Viterbi и RS — через libfec Карна (разделяемая libfec.so/.dll/.dylib, load-time
  линковка как WDSP/IIO). libfec ставится отдельно на каждой ОС.
}

{$IFDEF FPC}{$MODE Delphi}{$ENDIF}
{$PACKRECORDS C}

interface

uses
  Classes, SysUtils;

const
  AO40_SYNC_LEN    = 65;
  AO40_SYNC_STEP   = 80;
  AO40_FRAME_SYMS  = AO40_SYNC_LEN * AO40_SYNC_STEP;   // 5200
  AO40_IL_ROWS     = 80;
  AO40_IL_COLS     = 65;
  AO40_VITERBI_SYM = 5132;                             // после skip 65
  AO40_DATA_BITS   = 2560;                             // выход Viterbi (без 6 tail)
  AO40_CONV_BYTES  = AO40_DATA_BITS div 8;             // 320
  AO40_RS_NROOTS   = 32;
  AO40_RS_PAD      = 95;                               // 255-160
  AO40_RS_NN       = 160;                              // укороченная длина
  AO40_RS_KK       = 128;
  AO40_INTERLEAVE  = 2;
  AO40_FRAME_BYTES = AO40_RS_KK * AO40_INTERLEAVE;     // 256
  AO40_SYNC_THRESH = 8;                                // допуск ошибок синхры

type
  TBeaconFrame = array[0..AO40_FRAME_BYTES-1] of Byte;
  TBeaconFrameEvent = procedure(const Frame: TBeaconFrame; RSErrors: Integer) of object;

  TBeaconFEC = class
  private
    FSync:     array[0..AO40_SYNC_LEN-1] of Byte;
    FRing:     array[0..AO40_FRAME_SYMS-1] of Single;
    FRingPos:  Integer;                 // позиция следующей записи = старейший элемент
    FRingCnt:  Integer;
    FCooldown: Integer;                 // не перепроверять сразу после успешного кадра
    FVp:       Pointer;                 // viterbi27 instance
    FFrames:   Int64;
    FOnFrame:  TBeaconFrameEvent;

    function  CheckSyncAt: Integer;     // -1 нет; 0 норм. полярность; 1 инверсия
    procedure DecodeCurrentFrame(Invert: Boolean);
    procedure Descramble(var Buf: array of Byte; Count: Integer);
  public
    constructor Create;
    destructor Destroy; override;
    procedure Reset;
    procedure PushSoftSymbol(const S: Single);
    function  SelfTest(out Msg: string): Boolean;
    property  Frames: Int64 read FFrames;
    property  OnFrame: TBeaconFrameEvent read FOnFrame write FOnFrame;
  end;

implementation

// ---------------- libfec bindings (Karn) ----------------
// Динамическая (load-time) линковка против разделяемой libfec — как WDSP.
// libfec ставится отдельно на каждой ОС (Linux: make install из исходников;
// Windows/macOS — отдельная сборка).
//
// {$LINKLIB fec} обязателен: одни лишь `external FEC_LIB` на функциях НЕ передают
// библиотеку линкеру (ld) на FPC/Darwin — символы остаются undefined. Точно так
// же работает WDSP ({$linklib wdsp}). Это НЕ форсирует статику: при наличии и
// libfec.dylib, и libfec.a в пути поиска ld предпочитает .dylib (как для wdsp).
//
// Только Darwin: на Windows {$LINKLIB fec} заставляет ld искать импорт-либу
// (libfec.a/.dll.a) и падает с "Import library not found for fec" — там load-time
// линковка делается самим FPC из external 'libfec.dll'. На Linux аналогично (как WDSP).
{$IFDEF DARWIN}{$LINKLIB fec}{$ENDIF}
const
{$IFDEF WINDOWS}
  FEC_LIB = 'libfec.dll';
{$ENDIF}
{$IFDEF LINUX}
  FEC_LIB = 'libfec.so';
{$ENDIF}
{$IFDEF DARWIN}
  FEC_LIB = 'libfec.dylib';
{$ENDIF}

{$IFDEF UNIX}{$LINKLIB m}{$ENDIF}   // libfec использует log() из libm

function  create_viterbi27(len: LongInt): Pointer; cdecl; external FEC_LIB;
function  init_viterbi27(vp: Pointer; starting_state: LongInt): LongInt; cdecl; external FEC_LIB;
function  update_viterbi27_blk(vp: Pointer; syms: PByte; npairs: LongInt): LongInt; cdecl; external FEC_LIB;
function  chainback_viterbi27(vp: Pointer; data: PByte; nbits: LongWord; endstate: LongWord): LongInt; cdecl; external FEC_LIB;
procedure delete_viterbi27(vp: Pointer); cdecl; external FEC_LIB;

function  decode_rs_8(data: PByte; eras_pos: PLongInt; no_eras: LongInt; pad: LongInt): LongInt; cdecl; external FEC_LIB;
procedure encode_rs_8(data: PByte; parity: PByte; pad: LongInt); cdecl; external FEC_LIB;

// ---------------- helpers ----------------

function ParityB(x: LongWord): Byte; inline;
// чётность числа единичных битов: 1=нечётно, 0=чётно
begin
  x := x xor (x shr 16);
  x := x xor (x shr 8);
  x := x xor (x shr 4);
  x := x xor (x shr 2);
  x := x xor (x shr 1);
  Result := x and 1;
end;

const
  // 65-битная распределённая синхра AO-40 (gr-satellites _syncword)
  AO40_SYNCWORD: array[0..AO40_SYNC_LEN-1] of Byte = (
    1,1,1,1,1,1,1,0,0,0,0,1,1,1,0,1,1,1,1,0,0,1,0,1,1,0,0,1,0,0,1,0,
    0,0,0,0,0,1,0,0,0,1,0,0,1,1,0,0,0,1,0,1,1,1,0,1,0,1,1,0,1,1,0,0,0);

// ---------------- TBeaconFEC ----------------

constructor TBeaconFEC.Create;
var i: Integer;
begin
  inherited Create;
  for i := 0 to AO40_SYNC_LEN-1 do FSync[i] := AO40_SYNCWORD[i];
  FVp := create_viterbi27(AO40_DATA_BITS);
  Reset;
end;

destructor TBeaconFEC.Destroy;
begin
  if FVp <> nil then delete_viterbi27(FVp);
  inherited Destroy;
end;

procedure TBeaconFEC.Reset;
var i: Integer;
begin
  for i := 0 to AO40_FRAME_SYMS-1 do FRing[i] := 0;
  FRingPos := 0; FRingCnt := 0; FCooldown := 0;
end;

procedure TBeaconFEC.PushSoftSymbol(const S: Single);
var r: Integer;
begin
  FRing[FRingPos] := S;
  FRingPos := FRingPos + 1;
  if FRingPos >= AO40_FRAME_SYMS then FRingPos := 0;
  if FRingCnt < AO40_FRAME_SYMS then Inc(FRingCnt);

  if FCooldown > 0 then begin Dec(FCooldown); Exit; end;
  if FRingCnt < AO40_FRAME_SYMS then Exit;

  r := CheckSyncAt;
  if r >= 0 then
  begin
    DecodeCurrentFrame(r = 1);
    FCooldown := AO40_FRAME_SYMS;   // следующий кадр — через полный фрейм
  end;
end;

function TBeaconFEC.CheckSyncAt: Integer;
// Кандидат-кадр = текущее содержимое кольца (старейший→новейший), старт = FRingPos.
// Синхра на относительных позициях j*step. Возвращает полярность или -1.
var
  j, idx, hard, matches: Integer;
begin
  matches := 0;
  for j := 0 to AO40_SYNC_LEN-1 do
  begin
    idx := FRingPos + j * AO40_SYNC_STEP;
    if idx >= AO40_FRAME_SYMS then Dec(idx, AO40_FRAME_SYMS);
    if FRing[idx] > 0.0 then hard := 1 else hard := 0;   // gr-конвенция: +soft = «1»
    if hard = FSync[j] then Inc(matches);
  end;
  if matches >= AO40_SYNC_LEN - AO40_SYNC_THRESH then Exit(0);   // норм. полярность
  if matches <= AO40_SYNC_THRESH then Exit(1);                   // инверсия
  Result := -1;
end;

procedure TBeaconFEC.Descramble(var Buf: array of Byte; Count: Integer);
// CCSDS additive descrambler = GNU Radio additive_scrambler_bb(0xA9, 0xFF, 7).
// Fibonacci LFSR, seed 0xFF, XOR с каждым битом (MSB-first), reset на старте кадра.
var
  state: Byte;
  i, b, outp, newbit, bit: Integer;
  v: Byte;
begin
  state := $FF;
  for i := 0 to Count-1 do
  begin
    v := Buf[i];
    for b := 7 downto 0 do
    begin
      outp := state and 1;
      newbit := ParityB(state and $A9);
      state := (state shr 1) or (newbit shl 7);
      bit := (v shr b) and 1;
      bit := bit xor outp;
      if bit <> 0 then v := v or (1 shl b) else v := v and not (1 shl b);
    end;
    Buf[i] := v;
  end;
end;

procedure TBeaconFEC.DecodeCurrentFrame(Invert: Boolean);
var
  win:    array[0..AO40_FRAME_SYMS-1] of Single;
  deint:  array[0..AO40_FRAME_SYMS-1] of Single;
  syms:   array[0..AO40_VITERBI_SYM-1] of Byte;
  conv:   array[0..AO40_CONV_BYTES-1] of Byte;
  cw:     array[0..AO40_RS_NN-1] of Byte;
  frame:  TBeaconFrame;
  i, idx, k, j, sgn, totErr, res: Integer;
  v: Double;
begin
  // 1. снимок кольца в хронологическом порядке (+ инверсия полярности)
  if Invert then sgn := -1 else sgn := 1;
  for i := 0 to AO40_FRAME_SYMS-1 do
  begin
    idx := FRingPos + i;
    if idx >= AO40_FRAME_SYMS then Dec(idx, AO40_FRAME_SYMS);
    win[i] := sgn * FRing[idx];
  end;

  // 2. матричный деинтерливер 80×65, out[i]=data[80*(i%65)+i/65]
  for i := 0 to AO40_FRAME_SYMS-1 do
    deint[i] := win[AO40_IL_ROWS * (i mod AO40_IL_COLS) + (i div AO40_IL_COLS)];

  // 3. skip 65 (синхра) → 5132 мягких символа → байты для libfec (255=«1»).
  // РЕМАП КОНВЕНЦИИ Viterbi: AO-40/gr-satellites кодирует полиномами [0x4F, -0x6D]
  // (cc_decoder), а libfec viterbi27 ЗАШИВАЕТ [0x6D, 0x4F] БЕЗ инверсии. Эмпирически
  // (брут-форс против gr post_viterbi_reference, см. историю) точное соответствие:
  //   libfec[2p]   = -soft[2p+1]   (своп пары + инверсия первого символа)
  //   libfec[2p+1] =  soft[2p]
  // Без ремапа Viterbi выдаёт мусор → RS не корректируется.
  for i := 0 to (AO40_VITERBI_SYM div 2) - 1 do
  begin
    v := 128.0 - deint[AO40_SYNC_LEN + 2*i + 1] * 100.0;     // -soft[2p+1]
    if v > 255.0 then v := 255.0 else if v < 0.0 then v := 0.0;
    syms[2*i]   := Round(v);
    v := 128.0 + deint[AO40_SYNC_LEN + 2*i] * 100.0;         //  soft[2p]
    if v > 255.0 then v := 255.0 else if v < 0.0 then v := 0.0;
    syms[2*i+1] := Round(v);
  end;

  // 4. Viterbi r=1/2 k=7 terminated → 2560 бит = 320 байт (MSB-first)
  init_viterbi27(FVp, 0);
  update_viterbi27_blk(FVp, @syms[0], (AO40_DATA_BITS + 6));   // 2566 пар
  chainback_viterbi27(FVp, @conv[0], AO40_DATA_BITS, 0);

  // 5. CCSDS дескремблер
  Descramble(conv, AO40_CONV_BYTES);

  // 6. RS: 2-way байт-интерлив, decode_rs_8 укороч. pad=95
  totErr := 0;
  for j := 0 to AO40_INTERLEAVE-1 do
  begin
    for k := 0 to AO40_RS_NN-1 do
      cw[k] := conv[j + k * AO40_INTERLEAVE];
    res := decode_rs_8(@cw[0], nil, 0, AO40_RS_PAD);
    if res < 0 then Exit;                   // некорректируемо → кадр невалиден
    Inc(totErr, res);
    for k := 0 to AO40_RS_KK-1 do
      frame[j + k * AO40_INTERLEAVE] := cw[k];
  end;

  Inc(FFrames);
  if Assigned(FOnFrame) then FOnFrame(frame, totErr);
end;

// ---------------- self-test ----------------

function TBeaconFEC.SelfTest(out Msg: string): Boolean;
// Проверяет линковку libfec и базовую корректность: RS encode/decode round-trip
// с внесёнными ошибками (укорочение pad=95) + Viterbi encode/decode round-trip.
var
  data: array[0..AO40_RS_KK-1] of Byte;
  par:  array[0..AO40_RS_NROOTS-1] of Byte;
  cw:   array[0..AO40_RS_NN-1] of Byte;
  i, res, encst, sym0, sym1, errs: Integer;
  bits: array[0..AO40_DATA_BITS-1] of Byte;
  vsyms: array[0..AO40_VITERBI_SYM-1] of Byte;
  outb: array[0..AO40_CONV_BYTES-1] of Byte;
  b: Integer;
begin
  Result := False;

  // --- RS round-trip ---
  for i := 0 to AO40_RS_KK-1 do data[i] := (i * 37 + 11) and $FF;
  encode_rs_8(@data[0], @par[0], AO40_RS_PAD);
  for i := 0 to AO40_RS_KK-1 do cw[i] := data[i];
  for i := 0 to AO40_RS_NROOTS-1 do cw[AO40_RS_KK + i] := par[i];
  // вносим 16 байтовых ошибок (= предел исправления для nroots=32)
  for i := 0 to 15 do cw[i * 9] := cw[i * 9] xor $A5;
  res := decode_rs_8(@cw[0], nil, 0, AO40_RS_PAD);
  if res < 0 then begin Msg := 'RS decode FAILED (link/params?)'; Exit; end;
  for i := 0 to AO40_RS_KK-1 do
    if cw[i] <> data[i] then begin Msg := Format('RS mismatch at %d', [i]); Exit; end;

  // --- Viterbi round-trip ---
  // libfec viterbi27 зашивает полиномы V27POLYA=$6D, V27POLYB=$4F (CCSDS) БЕЗ
  // инверсии и без API смены. Энкодер строго как у Карна (vtest27.c):
  //   sr=(sr shl 1)|bit; sym0=parity(sr & $6D); sym1=parity(sr & $4F).
  // Мягкий «1» = 255, «0» = 0. + 6 хвостовых нулей (terminated).
  Randomize;
  for i := 0 to AO40_DATA_BITS-1 do bits[i] := Random(2);
  encst := 0;
  for i := 0 to AO40_DATA_BITS + 6 - 1 do
  begin
    if i < AO40_DATA_BITS then b := bits[i] else b := 0;
    encst := ((encst shl 1) or b) and $7F;
    sym0 := ParityB(encst and $6D);
    sym1 := ParityB(encst and $4F);
    vsyms[2*i]   := sym0 * 255;
    vsyms[2*i+1] := sym1 * 255;
  end;
  init_viterbi27(FVp, 0);
  update_viterbi27_blk(FVp, @vsyms[0], AO40_DATA_BITS + 6);
  chainback_viterbi27(FVp, @outb[0], AO40_DATA_BITS, 0);
  errs := 0;
  for i := 0 to AO40_DATA_BITS-1 do
  begin
    b := (outb[i div 8] shr (7 - (i mod 8))) and 1;   // MSB-first
    if b <> bits[i] then Inc(errs);
  end;
  if errs <> 0 then
  begin
    Msg := Format('Viterbi round-trip: %d/%d bit errors (convention mismatch)',
                  [errs, AO40_DATA_BITS]);
    Exit;
  end;

  Msg := Format('OK: RS corrected 16 errs; Viterbi 0/%d bit errors', [AO40_DATA_BITS]);
  Result := True;
end;

end.
