Documentation
¶
Overview ¶
Package phase2 implements P25 Phase 2 (H-DQPSK TDMA) demodulation and framing. Output is classified bursts with slot/location/DUID metadata. Voice decode, descrambling, and audio output are handled by callers.
Index ¶
- Constants
- Variables
- func GenerateXORMask(nac uint16, sysid uint16, wacn uint32) [SuperframeDibits]p25.Dibit
- func PackAMBE(u [4]uint16) [49]uint8
- func PackCW(u [4]uint16) [7]byte
- func UnpackCW(cw [7]byte) [4]uint16
- type ACCHType
- type Burst
- type BurstType
- type Decoder
- func (d *Decoder) Close()
- func (d *Decoder) EVM() float64
- func (d *Decoder) HasKey(algID uint8, keyID uint16) bool
- func (d *Decoder) Process(iq []complex64) ([]Burst, []P2VoiceFrame)
- func (d *Decoder) Reset()
- func (d *Decoder) ResetStats()
- func (d *Decoder) SetKeyLookup(fn KeyLookupFunc)
- func (d *Decoder) SetScrambleParams(nac uint16, sysid uint16, wacn uint32)
- type ESSState
- type Framer
- type HDQPSKDemod
- type ISCHInfo
- type KeyLookupFunc
- type MACPDU
- type P2VoiceFrame
- type TDMAProcessor
- func (t *TDMAProcessor) Close()
- func (t *TDMAProcessor) HasKey(algID uint8, keyID uint16) bool
- func (t *TDMAProcessor) ProcessBurst(b Burst) *P2VoiceFrame
- func (t *TDMAProcessor) ResetSlot(slot int)
- func (t *TDMAProcessor) SetKeyLookup(fn KeyLookupFunc)
- func (t *TDMAProcessor) SetScrambleParams(nac uint16, sysid uint16, wacn uint32)
- func (t *TDMAProcessor) SetXORMask(mask [SuperframeDibits]p25.Dibit)
- type VoiceCWResult
Constants ¶
const ( // SymbolRate is the H-DQPSK channel symbol rate. SymbolRate = 6000.0 // BurstDibits is the per-burst payload length (180 dibits = 360 bits = 30 ms // at SymbolRate 6000 sym/s). BurstDibits = 180 // SuperframeBursts is the number of bursts in one superframe (12 x 30 ms = 360 ms). SuperframeBursts = 12 // SyncDibits is the length of the Phase 2 frame sync pattern. SyncDibits = 20 // SyncBits is SyncDibits * 2. SyncBits = 40 // SyncMagic is the P25 Phase 2 frame sync pattern, 40 bits, MSB-first. // Source: op25 frame_sync_magics.h (P25P2_FRAME_SYNC_MAGIC). SyncMagic uint64 = 0x575D57F7FF // SyncMask masks the relevant 40 bits. SyncMask uint64 = 0xFFFFFFFFFF // SyncErrorThreshold is the maximum bit-error count for sync detection. // op25 uses 4. Matches the 10% threshold typical for marginal SNR. SyncErrorThreshold = 4 // DUIDPositions are the ABSOLUTE dibit indices within a 180-dibit burst // that carry the DUID bits. op25 extract_duid reads burstp[10/47/132/169] // where burstp = &dibits[10], i.e. absolute 20/57/142/179, on the RAW // (pre-descramble) burst. Source: op25 p25p2_duid.cc::extract_duid + // p25p2_tdma.cc:698 (burstp = &dibits[10]). DUIDPos0 = 20 DUIDPos1 = 57 DUIDPos2 = 142 DUIDPos3 = 179 // PayloadOffset is where the descrambled payload starts within the // 180-dibit burst. op25 calls this "burstp = &dibits[10]". PayloadOffset = 10 // VCW (voice codeword) offsets relative to PayloadOffset. // Full burst position = PayloadOffset + offset. // Source: op25 p25p2_tdma.cc lines 737-741. VCW1Offset = 11 // burst[21]: first 36-dibit voice codeword VCW2Offset = 48 // burst[58]: second voice codeword VCW3Offset = 96 // burst[106]: third voice codeword (4V only) VCW4Offset = 133 // burst[143]: fourth voice codeword (4V only) // ESSOffset is the ESS position relative to PayloadOffset. // 12 dibits carrying encryption sync signal (algid/keyid/MI fragments). // Source: op25 p25p2_tdma.cc line 736. ESSOffset = 84 // burst[94] ESSDibits = 12 )
const ( MACOpSignal uint8 = 0 MACOpPTT uint8 = 1 MACOpEndPTT uint8 = 2 MACOpIdle uint8 = 3 MACOpActive uint8 = 4 MACOpHangtime uint8 = 6 )
MAC control opcodes (3-bit). Source: op25 process_mac_pdu (p25p2_tdma.cc:171).
const PayloadDibitsPerBurst = BurstDibits - 10 // 170
PayloadDibitsPerBurst is the number of payload dibits per burst (positions 10..179 = 170 dibits). Used for burst-level payload size calculations.
const SuperframeDibits = SuperframeBursts * BurstDibits // 2160
SuperframeDibits is the total number of dibits in one superframe XOR mask.
const VoiceCWDibits = 36
VoiceCWDibits is the number of dibits per voice codeword.
Variables ¶
var WhichSlot = [SuperframeBursts]int{0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0}
WhichSlot maps the 12 superframe burst positions (ISCH "location" field) to slot index (0 or 1). Note position 10 maps to slot 1, not 0 — the pattern is asymmetric. Source: op25 p25p2_tdma.cc.
Functions ¶
func GenerateXORMask ¶
func GenerateXORMask(nac uint16, sysid uint16, wacn uint32) [SuperframeDibits]p25.Dibit
GenerateXORMask computes the 2160-dibit XOR descrambling mask for one superframe, given the system identity parameters. The LFSR produces 4320 bits (2160 dibits = 12 × 180). Indexed at stride 180 (BurstDibits): mask[burstPosition*180 + i] where i=0..169. The first 10 entries per burst slot (ISCH area) exist in the array but are not applied. Source: op25 lfsr.py:mk_xor_bits + p25p2_tdma.cc:set_xormask
func PackAMBE ¶
PackAMBE packs the four u[] words into a 49-element bit vector (1 bit per byte, MSB-first within each word) suitable for mbelib's ambe_d[49] input. Layout: u[0] bits 11..0 → d[0..11], u[1] bits 11..0 → d[12..23], u[2] bits 10..0 → d[24..34], u[3] bits 13..0 → d[35..48].
Types ¶
type Burst ¶
type Burst struct {
Dibits [BurstDibits]p25.Dibit // descrambled payload after Decoder.processBurst
Raw [BurstDibits]p25.Dibit // raw pre-descramble dibits; set by the decoder
ISCH ISCHInfo
DUID uint8 // raw 8-bit DUID extracted from RAW dibits 20/57/142/179
Type BurstType
}
Burst is one Phase 2 burst: 180 dibits plus decoded metadata.
func Descramble ¶
func Descramble(b Burst, mask [SuperframeDibits]p25.Dibit) Burst
Descramble applies the XOR mask to a burst at the given superframe position. Only the 170 payload dibits (burst[10:180]) are XOR'd; the first 10 dibits (ISCH/sync) are preserved. Returns a new burst with descrambled payload.
The mask stride is BurstDibits (180), NOT PayloadDibitsPerBurst (170). Each superframe position occupies a 180-dibit block in the mask array; the first 10 entries of each block (corresponding to ISCH) are skipped. Source: op25 p25p2_tdma.cc handle_packet:
tdma_xormask[sync.tdma_slotid() * BURST_SIZE + i] (BURST_SIZE=180, i=0..169)
type BurstType ¶
type BurstType int
BurstType is the high-level classification of a burst.
const ( BurstUnknown BurstType = iota Burst4V // four voice codewords (FEC-detected; DUID id 0) Burst2V // two voice codewords + SACCH/FACCH (FEC-detected; DUID id 6) BurstSACCH // DUID id 3 (scrambled) or 12 (unscrambled) SACCH; no voice BurstLCCH // DUID id 13: unscrambled LCCH (CRC-16); no voice BurstFACCH // DUID id 9 (scrambled) or 15 (unscrambled) FACCH; no voice )
func Classify ¶
Classify determines a burst's type. Voice detection is FEC-based and authoritative (ClassifyByFEC); the DUID is consulted ONLY when FEC rejects the burst as non-voice, to label it as a control burst. b.DUID must already be set from the RAW burst (see Decoder.processBurst).
func ClassifyByFEC ¶
func ClassifyByFEC(dibits [BurstDibits]p25.Dibit) BurstType
ClassifyByFEC classifies a burst by checking Golay FEC quality at the voice codeword positions. This is far more reliable than DUID-based classification for voice bursts.
Detection uses the SUM of c0 Golay errors across all 4 VCW positions. For voice, each VCW typically has c0=0, so sum ≈ 0-4. For random data, each c0 averages ~2.5 errors (Golay(23,12) is perfect, always ≤3), so sum ≈ 10. The false-positive rate at sum ≤ 4 is negligible (< 10^-6).
4V vs 2V: compare the c0-error sum of VCW1+2 vs VCW3+4. If VCW3+4 contribute ≤ 2 errors, it's 4V (4 voice codewords). Otherwise 2V (2 voice codewords + control data in VCW3+4 positions).
type Decoder ¶
type Decoder struct {
// Counters (diagnostic only).
BurstsTotal int
BurstsValid int // ISCH decoded successfully
VoiceFrames int // voice frames produced
// contains filtered or unexported fields
}
Decoder is the Phase 2 receive chain: complex IQ -> classified bursts and decoded voice frames. State persists across Process() calls; feed contiguous IQ samples.
func NewDecoder ¶
NewDecoder builds a decoder for the given input sample rate (e.g. 25000).
func (*Decoder) HasKey ¶
HasKey reports whether a usable decryption key exists for (algID, keyID). Passthrough to the TDMA processor; see TDMAProcessor.HasKey.
func (*Decoder) Process ¶
func (d *Decoder) Process(iq []complex64) ([]Burst, []P2VoiceFrame)
Process consumes IQ and returns classified bursts and decoded voice frames. Voice frames are only produced for 4V/2V bursts when scramble params are set.
The processing pipeline for each burst is:
- Assign superframe position from the running counter (or buffer for FEC-guided sync detection if counter not yet initialized).
- Descramble payload (positions 10-179) using the XOR mask at the burst's superframe location.
- Extract DUID and classify from the descrambled burst.
- Feed voice-bearing bursts to the TDMA processor for voice decode.
Superframe offset detection: the ISCH second half (positions 10-19) is in the scrambled region, making the (40,9,16) Hamming decode unreliable for I-ISCH location extraction. Instead, we buffer the first 24 bursts and brute-force all 12 offsets, selecting the one with the most perfect Golay c0 decodes in the voice codeword positions.
func (*Decoder) Reset ¶
func (d *Decoder) Reset()
Reset reinitializes the decoder's demod, framer, and per-slot state so that stale inter-block state does not contaminate a new TDMA session. The scramble mask is preserved (it depends on system identity, not call state). Call when switching from FDMA to TDMA mode or vice versa.
func (*Decoder) ResetStats ¶
func (d *Decoder) ResetStats()
ResetStats clears EVM accumulators without affecting demod state.
func (*Decoder) SetKeyLookup ¶
func (d *Decoder) SetKeyLookup(fn KeyLookupFunc)
SetKeyLookup configures the key-resolution function used for ADP decryption of encrypted Phase 2 voice. Thread-safe.
type ESSState ¶
type ESSState struct {
// Decoded encryption parameters. Updated on successful RS decode.
AlgID uint8
KeyID uint16
MI [9]byte
Valid bool // true after at least one successful RS decode
// contains filtered or unexported fields
}
ESSState tracks ESS (Encryption Sync Signal) accumulation for one TDMA slot.
In P25 Phase 2, encryption metadata (AlgID/KeyID/MI) is distributed across a superframe's voice bursts:
- ESS-B: 4 hexbits per 4V burst × 4 bursts = 16 data hexbits
- ESS-A: 28 parity hexbits from the 2V burst
After a complete cycle (4 × 4V + 1 × 2V), RS(44,16) decode over GF(2^6) yields the 96-bit encryption metadata: AlgID(8) + KeyID(16) + MI(72).
Source: op25 p25p2_tdma.cc handle_4V2V_ess().
func NewESSState ¶
func NewESSState() *ESSState
NewESSState creates an ESS accumulator in the initial (not-synced) state.
func (*ESSState) BurstPosition ¶
BurstPosition returns the current burst position within the superframe voice cycle: 0-3 for 4V bursts, 4 for 2V, -1 if not synced. This is used by the ADP decryption offset calculation.
func (*ESSState) Encrypted ¶
Encrypted returns true if the decoded AlgID indicates encryption. AlgID 0x80 means "unencrypted" and 0x00 means "not applicable".
func (*ESSState) Feed ¶
func (e *ESSState) Feed(burstType BurstType, dibits [BurstDibits]p25.Dibit)
Feed processes ESS dibits from a descrambled voice burst. burstType must be Burst4V or Burst2V; other types are ignored. The full descrambled 180-dibit burst is required (ESS spans positions 94–178).
For 4V bursts: accumulates 4 ESS-B hexbits from 12 dibits at ESSOffset. For 2V bursts: extracts 28 ESS-A hexbits, then performs RS decode.
type Framer ¶
type Framer struct {
// contains filtered or unexported fields
}
Framer scans a Phase 2 dibit stream for the SyncMagic pattern and accumulates 180-dibit (30 ms) bursts. After initial sync, it tracks consecutive bursts by counting dibits (like op25's d_in_sync mechanism), collecting both S-ISCH and I-ISCH bursts.
type HDQPSKDemod ¶
type HDQPSKDemod struct {
// contains filtered or unexported fields
}
HDQPSKDemod converts complex64 IQ at sampleRate Hz into Phase 2 dibits at SymbolRate sym/s using:
- linear-interpolated symbol-rate strobe
- Gardner timing error detector with PI loop filter (complex-sample form)
- decision-directed phase rotator (first-order PLL on the differential phase)
- Gray-coded pi/4 DQPSK slicer (differentialDecode)
Sample rate must be ≥ 2·SymbolRate. At 25 kSPS the loop runs at 4.167 samp/sym, which Gardner handles cleanly.
func NewHDQPSKDemod ¶
func NewHDQPSKDemod(sampleRate float64) *HDQPSKDemod
NewHDQPSKDemod returns a demod configured for the given input sample rate. Loop gains are tuned for 4–10 samp/sym; the defaults work at 25 kSPS.
func (*HDQPSKDemod) CarrierPhase ¶
func (d *HDQPSKDemod) CarrierPhase() float64
CarrierPhase returns the current accumulated carrier-phase estimate (radians). Diagnostic only.
func (*HDQPSKDemod) EVM ¶
func (d *HDQPSKDemod) EVM() float64
EVM returns the RMS Error Vector Magnitude (phase-domain), normalized by the nominal π/4 phase advance. A perfect signal returns 0; typical values for good P25 Phase 2 signals are 0.05–0.15.
func (*HDQPSKDemod) Process ¶
func (d *HDQPSKDemod) Process(in []complex64) []p25.Dibit
Process consumes complex IQ and returns recovered dibits. State carries across calls — feed contiguous samples from the upstream source.
The returned slice is backed by an internal buffer reused on the next Process call; callers must consume it before calling Process again (framer.Feed does so synchronously within Decoder.Process).
func (*HDQPSKDemod) ResetStats ¶
func (d *HDQPSKDemod) ResetStats()
ResetStats clears the EVM accumulator without affecting timing or carrier state.
type ISCHInfo ¶
type ISCHInfo struct {
Location int // 0..11, burst position within superframe; -1 if unknown
Slot int // 0 or 1, derived from WhichSlot[Location]; -1 if Location is -1
IsSISCH bool // true for the rare "super-ISCH" sync indicator codeword
Valid bool // false if Hamming decode failed
}
ISCHInfo is the decoded Inter-Slot Signalling CHannel field. Identifies which superframe burst and which slot a burst belongs to.
func DecodeISCH ¶
func DecodeISCH(dibits [SyncDibits]p25.Dibit) ISCHInfo
DecodeISCH decodes a 20-dibit (40-bit) ISCH codeword to ISCHInfo. First tries an exact lookup; on miss, finds the nearest codeword by Hamming distance and accepts if ≤7 bits (the code's correction limit).
type KeyLookupFunc ¶
KeyLookupFunc resolves an encryption key from algorithm ID and key ID. Returns the raw key bytes and true if found, or nil and false if not.
type MACPDU ¶
type MACPDU struct {
Opcode uint8
Offset uint8
Bytes []byte
HasIdentity bool
Talkgroup uint16
SourceID uint32
ServiceOpts uint8
HasEncryption bool
AlgID uint8
KeyID uint16
MI [9]byte
// GPS holds an in-call Harris Talker GPS position (vendor sub-message op=0xAA,
// MFID=0xA4); GPSOK is true when decoded.
GPS p25.GPSPosition
GPSOK bool
// contains filtered or unexported fields
}
MACPDU is a decoded Phase 2 MAC PDU from a FACCH/SACCH burst. Opcode is the 3-bit MAC control opcode (0 SIGNAL,1 PTT,2 END_PTT,3 IDLE, 4 ACTIVE,6 HANGTIME). Source: op25 process_mac_pdu (p25p2_tdma.cc:171-221).
func DecodeACCH ¶
func DecodeACCH(dibits [BurstDibits]p25.Dibit, typ ACCHType) (*MACPDU, bool)
DecodeACCH decodes one burst's ACCH into a MAC PDU. typ selects FACCH / SACCH / LCCH (layout + CRC). It runs the FEC pipeline (decodeACCHBytes) then parses call identity per op25's MAC opcode handlers. Returns (nil,false) on FEC/CRC failure. Source: op25 process_mac_pdu (p25p2_tdma.cc:171-221).
type P2VoiceFrame ¶
type P2VoiceFrame struct {
PCM []float32 // decoded 8 kHz float32 PCM samples
Slot int // 0 or 1 — which TDMA slot this came from
Errs int // total FEC errors summed across all voice codewords
// Total is the number of voice codewords processed in this frame (4 for a
// 4V burst, 2 for a 2V burst). Uncorrectable is how many of those had a
// DETECTED uncorrectable c0 (extended Golay(24,12,8) weight->=4 error, via
// Golay24DetectUncorrectable). Because Golay(23,12) is a perfect code the
// decoder never self-reports failure; this detection uses the overall-parity
// bit the (23,12) path discards, so Uncorrectable/Total is a real c0
// decode-failure signal (Errs only sums CORRECTED bits and is biased low by
// miscorrected words). Both are 0 for ControlOnly frames. Metric-only:
// audio emission is unaffected.
Total int
Uncorrectable int
// ControlOnly is true for frames decoded from a voice-less control burst
// (SACCH/FACCH/LCCH). PCM is nil for these; they carry MAC identity / alias /
// encryption and a MACOpcode for the pipeline lifecycle layer.
ControlOnly bool
MACOpcode uint8 // MAC control opcode (MACOp*); only meaningful when ControlOnly
// Encryption metadata from ESS decode. Updated after each complete
// superframe cycle (4×4V + 1×2V). AlgID 0x80 = clear, 0x00 = N/A.
AlgID uint8
KeyID uint16
MI [9]byte
Encrypted bool // true if AlgID indicates active encryption
Decrypted bool // true if encrypted AND successfully decrypted (PCM is clear)
// Call identity decoded from the in-call MAC signalling (FACCH on the 2V
// burst), independent of the Phase 1 control-channel grant. Zero/false when
// no MAC identity has been seen on this slot yet.
Talkgroup uint16
SourceID uint32
ServiceOpts uint8 // MAC service options byte (emergency/enc/priority bits)
IdentityFromMAC bool
// Talker alias decoded from vendor-specific MAC sub-messages (0x91/0x95
// Motorola, 0xA8 Harris) on the in-call FACCH. Empty until a complete alias
// is assembled; TalkerAliasTGID is the talkgroup from the Motorola header.
TalkerAlias string
TalkerAliasTGID uint16
TalkerAliasUnit uint32 // SUID unit ID from the Motorola alias (0 for Harris)
// In-call GPS decoded from a Harris Talker GPS MAC sub-message (op=0xAA,
// MFID=0xA4) on the FACCH. GPSOK is false until a position has been seen.
GPS p25.GPSPosition
GPSOK bool
}
P2VoiceFrame holds decoded PCM audio from one Phase 2 TDMA voice burst. A 4V burst produces up to 640 PCM samples (4 × 160); a 2V burst up to 320.
type TDMAProcessor ¶
type TDMAProcessor struct {
// contains filtered or unexported fields
}
TDMAProcessor manages the two-slot TDMA state machine for P25 Phase 2. It descrambles voice bursts, extracts voice codewords, decodes FEC, and invokes the AMBE+2 vocoder. One TDMAProcessor handles both timeslots.
Usage: call ProcessBurst for each classified burst; returned P2VoiceFrames contain decoded PCM audio ready for playback or recording.
SetScrambleParams / SetXORMask / SetKeyLookup may be called from a different goroutine (e.g. a control-channel decoder propagating WACN/SYSID or key config). A mutex guards shared fields; the lock is uncontended on the hot ProcessBurst path.
func NewTDMAProcessor ¶
func NewTDMAProcessor() *TDMAProcessor
NewTDMAProcessor creates a processor. Call SetScrambleParams once system identity parameters are known (from the control channel).
func (*TDMAProcessor) HasKey ¶
func (t *TDMAProcessor) HasKey(algID uint8, keyID uint16) bool
HasKey reports whether a usable decryption key exists for (algID, keyID) via the configured key lookup, WITHOUT decrypting. The slot-open gate uses this as a deterministic key-availability signal: the per-frame Decrypted flag only goes true after the ADP cipher is primed (one superframe into the call), which would clip the head of a keyed call joined mid-stream.
func (*TDMAProcessor) ProcessBurst ¶
func (t *TDMAProcessor) ProcessBurst(b Burst) *P2VoiceFrame
ProcessBurst handles one classified, already-descrambled burst. Returns a P2VoiceFrame if the burst is voice-bearing (4V or 2V) and decoding succeeds, or nil otherwise. Descrambling and DUID classification are performed by Decoder.Process before this method is called.
For encrypted calls with a matching key, ADP decryption is applied between FEC decode and vocoder. The cipher is prepared once per superframe cycle on the 2V burst, after that burst's voice codewords have been processed (matching op25 timing: current 2V voice uses the old cipher state, then prepare() sets up the keystream for the next cycle).
func (*TDMAProcessor) ResetSlot ¶
func (t *TDMAProcessor) ResetSlot(slot int)
ResetSlot reinitializes the vocoder and encryption state for the given slot (0 or 1). Call at call boundaries to prevent state leakage between conversations.
func (*TDMAProcessor) SetKeyLookup ¶
func (t *TDMAProcessor) SetKeyLookup(fn KeyLookupFunc)
SetKeyLookup configures the key-resolution function used for ADP decryption. Thread-safe: may be called from any goroutine.
func (*TDMAProcessor) SetScrambleParams ¶
func (t *TDMAProcessor) SetScrambleParams(nac uint16, sysid uint16, wacn uint32)
SetScrambleParams configures the XOR descrambling mask from the system identity. Must be called before voice bursts can be descrambled. Thread-safe: may be called from any goroutine.
func (*TDMAProcessor) SetXORMask ¶
func (t *TDMAProcessor) SetXORMask(mask [SuperframeDibits]p25.Dibit)
SetXORMask sets the XOR mask directly (e.g. from an external source). Thread-safe: may be called from any goroutine.
type VoiceCWResult ¶
type VoiceCWResult struct {
U [4]uint16 // u[0..3]: 12+12+11+14 = 49 bits of AMBE+2 parameters
Errs int // total FEC errors (c0 + c1)
OK bool // true if both Golay decodes succeeded
C0Uncorrectable bool // c0 extended-Golay detected a weight->=4 (uncorrectable) error
}
VoiceCWResult holds the decoded AMBE+2 parameters from one voice codeword.
func DecodeVoiceCW ¶
func DecodeVoiceCW(dibits []p25.Dibit) VoiceCWResult
DecodeVoiceCW decodes one Phase 2 voice codeword from 36 dibits. Returns the four AMBE+2 parameter words (49 bits total), FEC error count, and whether decoding succeeded.