Field Guide · algorithm

Also known as: NID, DUID, network identifier, data unit ID

The P25 NID (Network Identifier) is the 64-bit block that immediately follows the frame sync word in every P25 Phase 1 frame.1 It carries two things a decoder needs before it can do anything else: the 12-bit Network Access Code (NAC) that identifies the system, and the 4-bit DUID (Data Unit ID) that names the frame type to follow — a voice frame, a terminator, or a trunking block. The whole thing is wrapped in a BCH(63,16,11) code so it survives a noisy channel.2

NAC 12 DUID 4 BCH(63,16) parity · 47 bits flag bit 63 63 codeword bits corrected by BCH · bit 63 is a per-DUID flag, NOT overall parity
The NID packs the NAC and DUID into a 16-bit message, BCH-encodes it to 63 bits, and appends one flag bit whose value is fixed by the DUID — a detail that is easy to get wrong.

How it works

The 16 information bits (12-bit NAC followed by 4-bit DUID) are encoded with a shortened BCH(63,16,11) code — 63 bits able to correct up to 11 bit errors — and a 64th bit is appended. A receiver runs the BCH decoder over the first 63 bits to recover the NAC and DUID; if the codeword is beyond the correction radius, the frame is dropped. The DUID then tells the deframer what comes next: 0x0 HDU, 0x3 TDU, 0x5 LDU1, 0x7 TSDU (the control channel), 0xA LDU2, 0xC PDU, 0xF TDULC.

The trap is the 64th bit. It is not an overall parity check across the 63-bit codeword — the obvious assumption, and the one that masked GopherTrunk issue #275. It is a fixed flag whose value is dictated by the DUID: 1 for the two voice frames (LDU1, LDU2) and 0 for everything else. A decoder that validates it as parity will accept some corrupt frames and reject some good ones. GopherTrunk encodes the rule directly:

// expectedNIDParity returns the value of the 64th NID bit a P25 Phase 1
// transmitter sets for a given DUID — NOT an overall parity over the
// 63-bit codeword. Per TIA-102.BAAA Annex A and confirmed against OP25.
func expectedNIDParity(duid DUID) byte {
    switch duid {
    case DUIDLogicalLink1, DUIDLogicalLink2:
        return 1
    default:
        return 0
    }
}

In practice

Because the NID is so short and so heavily protected, it is the most reliable thing on a P25 channel — a decoder can usually recover the NAC and DUID even when the frame body is too corrupted to use. GopherTrunk exploits that: when it cannot fully align a frame, it inspects where the residual bit errors cluster across the 32 NID dibits to tell apart a post-sync timing slip (errors bunched at one end), a status-symbol phase fault (errors near the tail), and plain SNR-limited demod corruption (errors spread evenly). That per-dibit error pattern is how the closest-miss diagnostics localise a lock problem.

Relevance to SDR

internal/radio/p25/phase1/nid.go implements the full NID path: ParseNID reads 64 bits, runs framing.BCHDecode63_16, extracts the NAC and DUID, and validates the per-DUID flag; NIDFromDibitsWithErrors additionally returns the 32-entry per-dibit error pattern the diagnostics use. Getting the flag rule right — flag, not parity — is exactly the kind of spec detail that is invisible in synthetic round-trip tests (which set the bit the same wrong way on both ends) yet decides whether real off-air frames decode. Every P25 Phase 1 frame is gated on a clean NID, so this small block sits on the critical path of the whole decoder.

Sources

  1. Project 25 — Wikipedia, on the P25 standard and its Phase 1 data units. 

  2. BCH code — Wikipedia, on the error-correcting code family that protects the NID. 

See also