{
  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 DMRProtocol;

{
  Small, allocation-free DMR burst protocol layer.

  Implements CACH/AMBE interleavers and Hamming/Golay matrices.
  The applicable third-party permissive notice follows.

  Copyright (C) 2010 DSD Author
  Permission to use, copy, modify, and/or distribute this software for any
  purpose with or without fee is hereby granted, provided that the above
  copyright notice and this permission notice appear in all copies.

  THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
  REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
  AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
  INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
  LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
  OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
  PERFORMANCE OF THIS SOFTWARE.
}

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

interface

uses
  SysUtils;

const
  DMR_PROTOCOL_BURST_DIBITS = 144;

type
  TDMRDibitBurst = array[0..DMR_PROTOCOL_BURST_DIBITS - 1] of Byte;
  TDMRBit7 = array[0..6] of Byte;
  TDMRBit20 = array[0..19] of Byte;
  TDMRBit96 = array[0..95] of Byte;
  TDMRAMBEFrame = array[0..95] of Byte;
  TDMRAMBEFrames = array[0..2] of TDMRAMBEFrame;

  TDMRCACH = record
    Valid: Boolean;
    CorrectedBits: Integer;
    AccessType: Boolean;
    Slot: Integer;
    LCSS: Integer;
  end;

  TDMRSlotType = record
    Valid: Boolean;
    CorrectedBits: Integer;
    ColorCode: Integer;
    DataType: Integer;
  end;

  TDMRLinkControl = record
    Decoded: Boolean;
    CRCValid: Boolean;
    RSCorrectedBytes: Integer;
    ProtectedFlag: Boolean;
    Reserved: Boolean;
    Opcode: Integer;
    FeatureSetID: Integer;
    ServiceOptions: Integer;
    TargetID: LongWord;
    SourceID: LongWord;
  end;

  TDMRBurstInfo = record
    CACH: TDMRCACH;
    SlotType: TDMRSlotType;
    BPTCValid: Boolean;
    BPTCIrrecoverableErrors: Integer;
    Payload96: TDMRBit96;
    LinkControl: TDMRLinkControl;
  end;

function DMRHamming74Encode(Value: Byte): TDMRBit7;
function DMRGolay208Encode(Value: Byte): TDMRBit20;
procedure DMRBPTC19696Encode(const Payload: TDMRBit96;
  out Burst: TDMRDibitBurst);
procedure DMRFullLCSetParity(var Payload: TDMRBit96; DataType: Integer);
procedure DMRExtractAMBEFrames(const Burst: TDMRDibitBurst;
  out Frames: TDMRAMBEFrames);
procedure DMRInsertAMBEFrames(const Frames: TDMRAMBEFrames;
  var Burst: TDMRDibitBurst);
function DMRDecodeCACH(const Burst: TDMRDibitBurst; out CACH: TDMRCACH): Boolean;
function DMRDecodeSlotType(const Burst: TDMRDibitBurst;
  out SlotType: TDMRSlotType): Boolean;
function DMRDecodeBPTC19696(const Burst: TDMRDibitBurst;
  out Payload: TDMRBit96; out IrrecoverableErrors: Integer): Boolean;
procedure DMRParseBurst(const Burst: TDMRDibitBurst; out Info: TDMRBurstInfo);
function DMRDataTypeName(DataType: Integer): string;

implementation

const
  CACH_INTERLEAVE: array[0..23] of Byte = (
     0,  7,  8,  9,  1, 10,
    11, 12,  2, 13, 14, 15,
     3, 16,  4, 17, 18, 19,
     5, 20, 21, 22,  6, 23
  );

  HAMMING_7_4_G: array[0..3, 0..6] of Byte = (
    (1, 0, 0, 0, 1, 0, 1),
    (0, 1, 0, 0, 1, 1, 1),
    (0, 0, 1, 0, 1, 1, 0),
    (0, 0, 0, 1, 0, 1, 1)
  );

  GOLAY_20_8_G: array[0..7, 0..19] of Byte = (
    (1,0,0,0,0,0,0,0, 0,0,1,1,1,1,0,1,1,0,1,0),
    (0,1,0,0,0,0,0,0, 1,1,0,1,1,0,0,1,1,0,0,1),
    (0,0,1,0,0,0,0,0, 0,1,1,0,1,1,0,0,1,1,0,1),
    (0,0,0,1,0,0,0,0, 0,0,1,1,0,1,1,0,0,1,1,1),
    (0,0,0,0,1,0,0,0, 1,1,0,1,1,1,0,0,0,1,1,0),
    (0,0,0,0,0,1,0,0, 1,0,1,0,1,0,0,1,0,1,1,1),
    (0,0,0,0,0,0,1,0, 1,0,0,1,0,0,1,1,1,1,1,0),
    (0,0,0,0,0,0,0,1, 1,0,0,0,1,1,1,0,1,0,1,1)
  );

  // Syndrome values for bit positions of the systematic Hamming codewords.
  HAMMING_13_9_SYNDROME: array[0..12] of Byte =
    (15,14,7,10,5,11,12,6,3, 8,4,2,1);
  HAMMING_15_11_SYNDROME: array[0..14] of Byte =
    (9,13,15,14,7,10,5,11,12,6,3, 8,4,2,1);

  AMBE_W: array[0..35] of Byte = (
    0,1,0,1,0,1, 0,1,0,1,0,1, 0,1,0,1,0,1,
    0,1,0,1,0,2, 0,2,0,2,0,2, 0,2,0,2,0,2);
  AMBE_X: array[0..35] of Byte = (
    23,10,22,9,21,8, 20,7,19,6,18,5, 17,4,16,3,15,2,
    14,1,13,0,12,10, 11,9,10,8,9,7, 8,6,7,5,6,4);
  AMBE_Y: array[0..35] of Byte = (
    0,2,0,2,0,2, 0,2,0,3,0,3, 1,3,1,3,1,3,
    1,3,1,3,1,3, 1,3,1,3,1,3, 1,3,1,3,1,3);
  AMBE_Z: array[0..35] of Byte = (
    5,3,4,2,3,1, 2,0,1,13,0,12, 22,11,21,10,20,9,
    19,8,18,7,17,6, 16,5,15,4,14,3, 13,2,12,1,11,0);

function DMRHamming74Encode(Value: Byte): TDMRBit7;
var
  i, j: Integer;
begin
  FillChar(Result, SizeOf(Result), 0);
  for i := 0 to 3 do
    if ((Value shr (3 - i)) and 1) <> 0 then
      for j := 0 to 6 do Result[j] := Result[j] xor HAMMING_7_4_G[i, j];
end;

function DMRGolay208Encode(Value: Byte): TDMRBit20;
var
  i, j: Integer;
begin
  FillChar(Result, SizeOf(Result), 0);
  for i := 0 to 7 do
    if ((Value shr (7 - i)) and 1) <> 0 then
      for j := 0 to 19 do Result[j] := Result[j] xor GOLAY_20_8_G[i, j];
end;

procedure EncodeHamming(var Codeword: array of Byte; DataBits: Integer;
  const Syndromes: array of Byte);
var
  i, Syndrome: Integer;
begin
  Syndrome := 0;
  for i := 0 to DataBits - 1 do
    if (Codeword[i] and 1) <> 0 then Syndrome := Syndrome xor Syndromes[i];
  // The final four systematic parity positions have syndromes 8,4,2,1.
  for i := 0 to 3 do Codeword[DataBits + i] := (Syndrome shr (3 - i)) and 1;
end;

function CorrectHamming(var Codeword: array of Byte;
  const Syndromes: array of Byte): Boolean;
var
  i, Syndrome, Position: Integer;
begin
  Syndrome := 0;
  for i := 0 to High(Codeword) do
    if (Codeword[i] and 1) <> 0 then Syndrome := Syndrome xor Syndromes[i];
  if Syndrome = 0 then Exit(True);
  Position := -1;
  for i := 0 to High(Codeword) do
    if Syndromes[i] = Syndrome then begin Position := i; Break; end;
  Result := Position >= 0;
  if Result then Codeword[Position] := Codeword[Position] xor 1;
end;

procedure DMRBPTC19696Encode(const Payload: TDMRBit96;
  out Burst: TDMRDibitBurst);
var
  Matrix: array[0..12, 0..14] of Byte;
  Deinterleaved, Air: array[0..195] of Byte;
  Row: array[0..14] of Byte;
  Column: array[0..12] of Byte;
  i, j, k: Integer;
begin
  FillChar(Burst, SizeOf(Burst), 0);
  FillChar(Matrix, SizeOf(Matrix), 0);
  k := 0;
  for j := 3 to 10 do begin Matrix[0, j] := Payload[k] and 1; Inc(k); end;
  for i := 1 to 8 do
    for j := 0 to 10 do begin Matrix[i, j] := Payload[k] and 1; Inc(k); end;

  for i := 0 to 8 do
  begin
    for j := 0 to 14 do Row[j] := Matrix[i, j];
    EncodeHamming(Row, 11, HAMMING_15_11_SYNDROME);
    for j := 0 to 14 do Matrix[i, j] := Row[j];
  end;
  for j := 0 to 14 do
  begin
    for i := 0 to 12 do Column[i] := Matrix[i, j];
    EncodeHamming(Column, 9, HAMMING_13_9_SYNDROME);
    for i := 0 to 12 do Matrix[i, j] := Column[i];
  end;

  Deinterleaved[0] := 0; // R(3)
  k := 1;
  for i := 0 to 12 do
    for j := 0 to 14 do begin Deinterleaved[k] := Matrix[i, j]; Inc(k); end;
  for i := 0 to 195 do Air[i] := Deinterleaved[(i * 13) mod 196];
  k := 0;
  for i := 12 to 60 do
  begin
    Burst[i] := (Air[k] shl 1) or Air[k + 1]; Inc(k, 2);
  end;
  for i := 95 to 143 do
  begin
    Burst[i] := (Air[k] shl 1) or Air[k + 1]; Inc(k, 2);
  end;
end;

function AMBEDibitIndex(FrameIndex, DibitIndex: Integer): Integer;
begin
  case FrameIndex of
    0: Result := 12 + DibitIndex;
    1: if DibitIndex < 18 then Result := 48 + DibitIndex
       else Result := 90 + DibitIndex - 18;
  else
    Result := 108 + DibitIndex;
  end;
end;

procedure DMRExtractAMBEFrames(const Burst: TDMRDibitBurst;
  out Frames: TDMRAMBEFrames);
var
  FrameIndex, i, DibitIndex: Integer;
  Dibit: Byte;
begin
  FillChar(Frames, SizeOf(Frames), 0);
  for FrameIndex := 0 to 2 do
    for i := 0 to 35 do
    begin
      DibitIndex := AMBEDibitIndex(FrameIndex, i);
      Dibit := Burst[DibitIndex];
      Frames[FrameIndex][AMBE_W[i] * 24 + AMBE_X[i]] := (Dibit shr 1) and 1;
      Frames[FrameIndex][AMBE_Y[i] * 24 + AMBE_Z[i]] := Dibit and 1;
    end;
end;

procedure DMRInsertAMBEFrames(const Frames: TDMRAMBEFrames;
  var Burst: TDMRDibitBurst);
var
  FrameIndex, i, DibitIndex: Integer;
begin
  for FrameIndex := 0 to 2 do
    for i := 0 to 35 do
    begin
      DibitIndex := AMBEDibitIndex(FrameIndex, i);
      Burst[DibitIndex] :=
        ((Frames[FrameIndex][AMBE_W[i] * 24 + AMBE_X[i]] and 1) shl 1) or
        (Frames[FrameIndex][AMBE_Y[i] * 24 + AMBE_Z[i]] and 1);
    end;
end;

function DecodeHamming74(const Received: TDMRBit7; out Value: Byte;
  out Corrected: Integer): Boolean;
var
  Candidate: TDMRBit7;
  V, i, Distance, BestDistance: Integer;
begin
  Value := 0;
  BestDistance := 8;
  for V := 0 to 15 do
  begin
    Candidate := DMRHamming74Encode(V);
    Distance := 0;
    for i := 0 to 6 do Inc(Distance, Candidate[i] xor (Received[i] and 1));
    if Distance < BestDistance then
    begin
      BestDistance := Distance;
      Value := V;
    end;
  end;
  Corrected := BestDistance;
  Result := BestDistance <= 1;
end;

function DecodeGolay208(const Received: TDMRBit20; out Value: Byte;
  out Corrected: Integer): Boolean;
var
  Candidate: TDMRBit20;
  V, i, Distance, BestDistance: Integer;
begin
  Value := 0;
  BestDistance := 21;
  for V := 0 to 255 do
  begin
    Candidate := DMRGolay208Encode(V);
    Distance := 0;
    for i := 0 to 19 do Inc(Distance, Candidate[i] xor (Received[i] and 1));
    if Distance < BestDistance then
    begin
      BestDistance := Distance;
      Value := V;
      if Distance = 0 then Break;
    end;
  end;
  Corrected := BestDistance;
  Result := BestDistance <= 2;
end;

function DMRDecodeCACH(const Burst: TDMRDibitBurst; out CACH: TDMRCACH): Boolean;
var
  Deinterleaved: array[0..23] of Byte;
  TACT: TDMRBit7;
  Value: Byte;
  i: Integer;
begin
  FillChar(CACH, SizeOf(CACH), 0);
  FillChar(Deinterleaved, SizeOf(Deinterleaved), 0);
  for i := 0 to 11 do
  begin
    Deinterleaved[CACH_INTERLEAVE[2 * i]] := (Burst[i] shr 1) and 1;
    Deinterleaved[CACH_INTERLEAVE[2 * i + 1]] := Burst[i] and 1;
  end;
  for i := 0 to 6 do TACT[i] := Deinterleaved[i];
  CACH.Valid := DecodeHamming74(TACT, Value, CACH.CorrectedBits);
  if CACH.Valid then
  begin
    CACH.AccessType := (Value and 8) <> 0;
    CACH.Slot := (Value shr 2) and 1;
    CACH.LCSS := Value and 3;
  end;
  Result := CACH.Valid;
end;

function DMRDecodeSlotType(const Burst: TDMRDibitBurst;
  out SlotType: TDMRSlotType): Boolean;
var
  Bits: TDMRBit20;
  Value: Byte;
  i, k: Integer;
begin
  FillChar(SlotType, SizeOf(SlotType), 0);
  k := 0;
  for i := 61 to 65 do
  begin
    Bits[k] := (Burst[i] shr 1) and 1; Inc(k);
    Bits[k] := Burst[i] and 1; Inc(k);
  end;
  for i := 90 to 94 do
  begin
    Bits[k] := (Burst[i] shr 1) and 1; Inc(k);
    Bits[k] := Burst[i] and 1; Inc(k);
  end;
  SlotType.Valid := DecodeGolay208(Bits, Value, SlotType.CorrectedBits);
  if SlotType.Valid then
  begin
    SlotType.ColorCode := (Value shr 4) and $0F;
    SlotType.DataType := Value and $0F;
  end;
  Result := SlotType.Valid;
end;

function DMRDecodeBPTC19696(const Burst: TDMRDibitBurst;
  out Payload: TDMRBit96; out IrrecoverableErrors: Integer): Boolean;
var
  Air, Deinterleaved: array[0..195] of Byte;
  Matrix: array[0..12, 0..14] of Byte;
  Row: array[0..14] of Byte;
  Column: array[0..12] of Byte;
  i, j, k, Pass: Integer;
begin
  FillChar(Payload, SizeOf(Payload), 0);
  k := 0;
  for i := 12 to 60 do
  begin
    Air[k] := (Burst[i] shr 1) and 1; Inc(k);
    Air[k] := Burst[i] and 1; Inc(k);
  end;
  for i := 95 to 143 do
  begin
    Air[k] := (Burst[i] shr 1) and 1; Inc(k);
    Air[k] := Burst[i] and 1; Inc(k);
  end;
  for i := 0 to 195 do Deinterleaved[(i * 13) mod 196] := Air[i];
  k := 1; // discard R(3)
  for i := 0 to 12 do
    for j := 0 to 14 do begin Matrix[i, j] := Deinterleaved[k]; Inc(k); end;

  IrrecoverableErrors := 0;
  // Alternating row/column passes allow a correction in one dimension to
  // make a previously ambiguous syndrome correctable in the other.
  for Pass := 0 to 1 do
  begin
    if Pass = 1 then IrrecoverableErrors := 0;
    for i := 0 to 8 do
    begin
      for j := 0 to 14 do Row[j] := Matrix[i, j];
      if not CorrectHamming(Row, HAMMING_15_11_SYNDROME) then
        Inc(IrrecoverableErrors);
      for j := 0 to 10 do Matrix[i, j] := Row[j];
    end;
    for j := 0 to 14 do
    begin
      for i := 0 to 12 do Column[i] := Matrix[i, j];
      if not CorrectHamming(Column, HAMMING_13_9_SYNDROME) then
        Inc(IrrecoverableErrors);
      for i := 0 to 8 do Matrix[i, j] := Column[i];
    end;
  end;

  k := 0;
  for j := 3 to 10 do begin Payload[k] := Matrix[0, j]; Inc(k); end;
  for i := 1 to 8 do
    for j := 0 to 10 do begin Payload[k] := Matrix[i, j]; Inc(k); end;
  Result := IrrecoverableErrors = 0;
end;

function BitsToUInt(const Bits: TDMRBit96; Start, Count: Integer): LongWord;
var
  i: Integer;
begin
  Result := 0;
  for i := 0 to Count - 1 do Result := (Result shl 1) or (Bits[Start + i] and 1);
end;

function GFMul(A, B: Byte): Byte;
var
  Carry: Boolean;
  P: Byte;
  i: Integer;
begin
  P := 0;
  for i := 0 to 7 do
  begin
    if (B and 1) <> 0 then P := P xor A;
    Carry := (A and $80) <> 0;
    A := A shl 1;
    if Carry then A := A xor $1D; // GF(256), primitive polynomial x^8+x^4+x^3+x^2+1
    B := B shr 1;
  end;
  Result := P;
end;

procedure BytesToBits(const Bytes: array of Byte; var Bits: TDMRBit96);
var
  i, j: Integer;
begin
  for i := 0 to 11 do
    for j := 0 to 7 do Bits[i * 8 + j] := (Bytes[i] shr (7 - j)) and 1;
end;

procedure BitsToBytes(const Bits: TDMRBit96; var Bytes: array of Byte);
var
  i, j: Integer;
begin
  for i := 0 to 11 do
  begin
    Bytes[i] := 0;
    for j := 0 to 7 do Bytes[i] := (Bytes[i] shl 1) or (Bits[i * 8 + j] and 1);
  end;
end;

procedure RS129Syndrome(const Codeword: array of Byte; out S0, S1, S2: Byte);
var
  i: Integer;
begin
  S0 := 0; S1 := 0; S2 := 0;
  for i := 0 to 11 do
  begin
    S0 := Codeword[i] xor GFMul(2, S0);
    S1 := Codeword[i] xor GFMul(4, S1);
    S2 := Codeword[i] xor GFMul(8, S2);
  end;
end;

function FullLCMask(DataType: Integer): LongWord;
begin
  if DataType = 1 then Result := $969696
  else if DataType = 2 then Result := $999999
  else Result := 0;
end;

procedure DMRFullLCSetParity(var Payload: TDMRBit96; DataType: Integer);
var
  Bytes: array[0..11] of Byte;
  Feedback: Byte;
  Mask: LongWord;
  i: Integer;
begin
  BitsToBytes(Payload, Bytes);
  Bytes[9] := 0; Bytes[10] := 0; Bytes[11] := 0;
  for i := 0 to 8 do
  begin
    Feedback := Bytes[i] xor Bytes[9];
    Bytes[9] := Bytes[10] xor GFMul($0E, Feedback);
    Bytes[10] := Bytes[11] xor GFMul($38, Feedback);
    Bytes[11] := GFMul($40, Feedback);
  end;
  Mask := FullLCMask(DataType);
  Bytes[9] := Bytes[9] xor Byte(Mask shr 16);
  Bytes[10] := Bytes[10] xor Byte(Mask shr 8);
  Bytes[11] := Bytes[11] xor Byte(Mask);
  BytesToBits(Bytes, Payload);
end;

function ValidateAndCorrectFullLC(var Payload: TDMRBit96; DataType: Integer;
  out CorrectedBytes: Integer): Boolean;
var
  Bytes: array[0..11] of Byte;
  S0, S1, S2: Byte;
  Mask: LongWord;
  Position, Delta: Integer;
begin
  CorrectedBytes := 0;
  BitsToBytes(Payload, Bytes);
  Mask := FullLCMask(DataType);
  Bytes[9] := Bytes[9] xor Byte(Mask shr 16);
  Bytes[10] := Bytes[10] xor Byte(Mask shr 8);
  Bytes[11] := Bytes[11] xor Byte(Mask);
  RS129Syndrome(Bytes, S0, S1, S2);
  if (S0 <> 0) or (S1 <> 0) or (S2 <> 0) then
  begin
    // RS(12,9) has three check symbols and corrects one complete byte. Full
    // Berlekamp-Massey machinery is unnecessary at this block size: search
    // the 12*255 possible one-symbol corrections and require zero syndrome.
    for Position := 0 to 11 do
    begin
      for Delta := 1 to 255 do
      begin
        Bytes[Position] := Bytes[Position] xor Byte(Delta);
        RS129Syndrome(Bytes, S0, S1, S2);
        if (S0 = 0) and (S1 = 0) and (S2 = 0) then
        begin
          CorrectedBytes := 1;
          Break;
        end;
        Bytes[Position] := Bytes[Position] xor Byte(Delta);
      end;
      if CorrectedBytes <> 0 then Break;
    end;
    if CorrectedBytes = 0 then Exit(False);
  end;
  // Payload data may itself have been the corrected RS symbol.
  Bytes[9] := Bytes[9] xor Byte(Mask shr 16);
  Bytes[10] := Bytes[10] xor Byte(Mask shr 8);
  Bytes[11] := Bytes[11] xor Byte(Mask);
  BytesToBits(Bytes, Payload);
  Result := True;
end;

procedure DMRParseBurst(const Burst: TDMRDibitBurst; out Info: TDMRBurstInfo);
begin
  FillChar(Info, SizeOf(Info), 0);
  DMRDecodeCACH(Burst, Info.CACH);
  DMRDecodeSlotType(Burst, Info.SlotType);
  if Info.SlotType.Valid and (Info.SlotType.DataType in [0..7, 9, 11]) then
  begin
    Info.BPTCValid := DMRDecodeBPTC19696(Burst, Info.Payload96,
      Info.BPTCIrrecoverableErrors);
    if Info.BPTCValid and (Info.SlotType.DataType in [1, 2]) then
    begin
      Info.LinkControl.CRCValid := ValidateAndCorrectFullLC(Info.Payload96,
        Info.SlotType.DataType, Info.LinkControl.RSCorrectedBytes);
      Info.LinkControl.Decoded := Info.LinkControl.CRCValid;
      if Info.LinkControl.Decoded then
      begin
        Info.LinkControl.ProtectedFlag := Info.Payload96[0] <> 0;
        Info.LinkControl.Reserved := Info.Payload96[1] <> 0;
        Info.LinkControl.Opcode := BitsToUInt(Info.Payload96, 2, 6);
        Info.LinkControl.FeatureSetID := BitsToUInt(Info.Payload96, 8, 8);
        Info.LinkControl.ServiceOptions := BitsToUInt(Info.Payload96, 16, 8);
        Info.LinkControl.TargetID := BitsToUInt(Info.Payload96, 24, 24);
        Info.LinkControl.SourceID := BitsToUInt(Info.Payload96, 48, 24);
      end;
    end;
  end;
end;

function DMRDataTypeName(DataType: Integer): string;
begin
  case DataType of
    0: Result := 'PI header';
    1: Result := 'voice LC';
    2: Result := 'terminator LC';
    3: Result := 'CSBK';
    4: Result := 'MBC header';
    5: Result := 'MBC continuation';
    6: Result := 'data header';
    7: Result := 'rate 1/2 data';
    8: Result := 'rate 3/4 data';
    9: Result := 'idle';
    10: Result := 'rate 1 data';
    11: Result := 'unified single block';
  else
    Result := 'reserved ' + IntToStr(DataType);
  end;
end;

end.
