Module: PWN::SDR::Decoder::P25

Defined in:
lib/pwn/sdr/decoder/p25.rb

Overview

APCO Project 25 Phase-1 (C4FM) true-air decoder.

I/Q → PWN::FFI::Liquid.freq_demod (or DSP.fm_demod_iq) → resample to 48 kHz → 4-level slice at 4800 sym/s → dibits → hunt the 24-symbol Frame Sync (0x5575F5FF77FF) → recover the 64-bit NID (12-bit NAC + 4-bit DUID + BCH(63,16,23) parity). Emits duid:, duid_name: per frame — the same intel OP25 / DSD show — with no external binary.

Defined Under Namespace

Classes: DemodIQ

Constant Summary collapse

FS_DIBITS =

24-symbol / 48-bit Frame Sync (dibit MSB-first)

[
  1, 1, 1, 1, 3, 1, 1, 3, 3, 3, 3, 1, 1, 3, 3, 3,
  3, 3, 3, 3, 1, 3, 3, 3, 3, 3, 3, 3
].freeze
FS_DIBITS_24 =

→ 0x5575F5FF77FF (see TIA-102.BAAA)

[
  1, 1, 1, 1, 3, 1, 1, 3, 3, 3, 3, 1,
  3, 3, 1, 1, 3, 3, 3, 3, 3, 3, 3, 3
].freeze
FRAME_SYNC =

Correct 24-dibit Frame Sync per TIA-102 (+3 +3 +3 +3 −3 +3 …). Derived from bit pattern 5575F5FF77FF, MSB-first, 2 bits/sym, C4FM map: 01→+3, 00→+1, 10→−1, 11→−3 → dibits 1,0,2,3.

0x5575F5FF77FF
FS_BITS =
Array.new(48) { |i| (FRAME_SYNC >> (47 - i)) & 1 }.freeze
FS_SYMS =
FS_BITS.each_slice(2).map { |a, b| (a << 1) | b }.freeze
DUID_NAME =
{
  0x0 => 'HDU', 0x3 => 'TDU', 0x5 => 'LDU1', 0x7 => 'TSBK',
  0xA => 'LDU2', 0xC => 'PDU', 0xF => 'TDULC'
}.freeze

Class Method Summary collapse

Class Method Details

.authorsObject

Author(s)

0day Inc. support@0dayinc.com



131
132
133
# File 'lib/pwn/sdr/decoder/p25.rb', line 131

public_class_method def self.authors
  "AUTHOR(S):\n  0day Inc. <support@0dayinc.com>\n"
end

.decode(opts = {}) ⇒ Object

Supported Method Parameters

PWN::SDR::Decoder::P25.decode( freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq' )



103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/pwn/sdr/decoder/p25.rb', line 103

public_class_method def self.decode(opts = {})
  freq_obj = opts[:freq_obj]
  rate = (opts[:sample_rate] || freq_obj[:iq_rate] || (48_000 * 20)).to_i
  PWN::SDR::Decoder::Base.run_iq(
    freq_obj: freq_obj,
    protocol: 'P25',
    sample_rate: rate,
    source: opts[:source],
    file: opts[:file],
    demod: DemodIQ.new(rate: rate),
    note: 'C4FM 4800 sym/s — I/Q→FM→4-FSK→FS 0x5575F5FF77FF→NAC/DUID.',
    describe: proc { |b| { modulation: 'C4FM', classification: b[:duration_ms] > 180 ? 'voice-LDU' : 'TSBK/control' } }
  )
end

.helpObject



135
136
137
138
139
140
141
142
143
144
145
# File 'lib/pwn/sdr/decoder/p25.rb', line 135

public_class_method def self.help
  puts "USAGE (true-air I/Q via PWN::FFI + detector fallback):
    #{self}.decode(
      freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq',
      source:   'optional - :auto|:rtlsdr|:adalm_pluto|:file',
      file:     'optional - .cu8/.cs16 capture'
    )

    #{self}.authors
  "
end

.parse_line(opts = {}) ⇒ Object



118
119
120
121
122
123
124
125
126
127
# File 'lib/pwn/sdr/decoder/p25.rb', line 118

public_class_method def self.parse_line(opts = {})
  line = opts[:line].to_s
  out  = { protocol: 'P25' }
  out[:nac]  = ::Regexp.last_match(1) if line =~ /NAC[:= ]+([0-9A-Fa-f]+)/
  out[:tg]   = ::Regexp.last_match(1) if line =~ /(?:TG|talkgroup)[:= ]+(\d+)/i
  out[:rid]  = ::Regexp.last_match(1) if line =~ /(?:RID|src|source)[:= ]+(\d+)/i
  out[:duid] = ::Regexp.last_match(1) if line =~ /DUID[:= ]+(\w+)/i
  out[:summary] = "P25 NAC=#{out[:nac]} TG=#{out[:tg]} RID=#{out[:rid]}"
  out.compact
end