pwn 0.5.627 → 0.5.628
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +20 -30
- data/Gemfile +1 -1
- data/README.md +2 -2
- data/lib/pwn/ai/agent/dispatch.rb +76 -4
- data/lib/pwn/ai/agent/learning.rb +124 -1
- data/lib/pwn/ai/agent/loop.rb +104 -12
- data/lib/pwn/ai/agent/metrics.rb +60 -27
- data/lib/pwn/ai/agent/prompt_builder.rb +63 -12
- data/lib/pwn/ai/agent/reflect.rb +41 -7
- data/lib/pwn/ai/agent/registry.rb +69 -4
- data/lib/pwn/ai/ollama.rb +39 -10
- data/lib/pwn/config.rb +11 -0
- data/lib/pwn/ffi/liquid.rb +179 -0
- data/lib/pwn/memory.rb +15 -0
- data/lib/pwn/memory_index.rb +233 -0
- data/lib/pwn/sdr/decoder/bluetooth.rb +141 -75
- data/lib/pwn/sdr/decoder/dect.rb +89 -63
- data/lib/pwn/sdr/decoder/dsp.rb +443 -0
- data/lib/pwn/sdr/decoder/gps.rb +93 -73
- data/lib/pwn/sdr/decoder/gsm.rb +167 -63
- data/lib/pwn/sdr/decoder/lora.rb +118 -65
- data/lib/pwn/sdr/decoder/lte.rb +188 -65
- data/lib/pwn/sdr/decoder/p25.rb +83 -67
- data/lib/pwn/sdr/decoder/zigbee.rb +140 -67
- data/lib/pwn/version.rb +1 -1
- data/lib/pwn.rb +1 -0
- data/spec/lib/pwn/memory_index_spec.rb +15 -0
- data/third_party/pwn_rdoc.jsonl +65 -8
- metadata +6 -4
data/lib/pwn/sdr/decoder/lte.rb
CHANGED
|
@@ -3,69 +3,201 @@
|
|
|
3
3
|
module PWN
|
|
4
4
|
module SDR
|
|
5
5
|
module Decoder
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
6
|
+
# LTE (E-UTRA) true-air PSS/SSS cell search.
|
|
7
|
+
#
|
|
8
|
+
# I/Q resampled to 1.92 Msps (128-FFT grid), time-domain PSS
|
|
9
|
+
# correlation against Zadoff-Chu roots {25,29,34} → N_ID_2 ∈ {0,1,2}
|
|
10
|
+
# + half-frame timing + coarse CFO. SSS m-sequence pair 5 symbols
|
|
11
|
+
# earlier → N_ID_1 ∈ 0..167 → PCI = 3·N_ID_1 + N_ID_2. All FFTs via
|
|
12
|
+
# PWN::FFI::FFTW; falls back to naive DFT for small N.
|
|
9
13
|
module LTE
|
|
10
|
-
|
|
14
|
+
FS_BASE = 1_920_000
|
|
15
|
+
NFFT = 128
|
|
16
|
+
CP_NORM = 9 # samples @ 1.92 Msps for symbols 1..6 (10 for symbol 0)
|
|
17
|
+
PSS_ROOTS = { 0 => 25, 1 => 29, 2 => 34 }.freeze
|
|
18
|
+
|
|
19
|
+
# Streaming PSS/SSS cell-search demod for Base.run_iq.
|
|
11
20
|
class DemodIQ
|
|
12
|
-
def initialize(rate
|
|
21
|
+
def initialize(rate:)
|
|
13
22
|
@rate = rate.to_f
|
|
14
|
-
@
|
|
15
|
-
@
|
|
16
|
-
@
|
|
17
|
-
@floor = nil
|
|
18
|
-
@in_burst = false
|
|
19
|
-
@burst_t0 = nil
|
|
20
|
-
@peak = -200.0
|
|
21
|
-
@burst_n = 0
|
|
22
|
-
@threshold = (extra[:threshold] || 8.0).to_f
|
|
23
|
+
@buf = []
|
|
24
|
+
@seen = {}
|
|
25
|
+
@pss = build_pss
|
|
23
26
|
end
|
|
24
27
|
|
|
25
|
-
def feed_iq(samples, rate: nil
|
|
28
|
+
def feed_iq(samples, rate: nil)
|
|
26
29
|
@rate = rate.to_f if rate
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
@burst_n += 1
|
|
48
|
-
dur_ms = ((Time.now - @burst_t0) * 1000).round
|
|
49
|
-
msg = {
|
|
50
|
-
protocol: @protocol, event: 'burst', source: 'iq',
|
|
51
|
-
burst_no: @burst_n, peak_dbfs: @peak.round(1),
|
|
52
|
-
floor_dbfs: @floor.round(1), delta_db: (@peak - @floor).round(1),
|
|
53
|
-
duration_ms: dur_ms, modulation: @modulation,
|
|
54
|
-
sample_rate: @rate.to_i
|
|
55
|
-
}.merge(@extra.except(:threshold))
|
|
56
|
-
# protocol-specific enrichment
|
|
57
|
-
msg.merge!(self.class.enrich(msg)) if self.class.respond_to?(:enrich)
|
|
58
|
-
msg[:summary] = format(
|
|
59
|
-
'%<p>s IQ-burst #%<n>d peak=%<pk>+.1f dBFS Δ=%<d>.1f dB dur=%<ms>d ms',
|
|
60
|
-
p: @protocol, n: @burst_n, pk: @peak, d: @peak - @floor, ms: dur_ms
|
|
30
|
+
r = PWN::SDR::Decoder::DSP.resample_iq(
|
|
31
|
+
iq: samples, src_rate: @rate, dst_rate: FS_BASE
|
|
32
|
+
)
|
|
33
|
+
@buf.concat(r)
|
|
34
|
+
# need ≥ one 5 ms half-frame @ 1.92 Msps = 9600 complex
|
|
35
|
+
return if @buf.length < 9600 * 2 * 2
|
|
36
|
+
|
|
37
|
+
hit = search_pss
|
|
38
|
+
if hit
|
|
39
|
+
nid1 = search_sss(hit)
|
|
40
|
+
pci = nid1 ? (3 * nid1) + hit[:nid2] : nil
|
|
41
|
+
key = pci || "N2=#{hit[:nid2]}"
|
|
42
|
+
unless @seen[key]
|
|
43
|
+
@seen[key] = true
|
|
44
|
+
yield(
|
|
45
|
+
protocol: 'LTE', event: 'cell', modulation: 'OFDMA',
|
|
46
|
+
nid2: hit[:nid2], nid1: nid1, pci: pci,
|
|
47
|
+
cfo_hz: hit[:cfo_hz], pss_peak_ratio: hit[:ratio],
|
|
48
|
+
timing_sample: hit[:pos],
|
|
49
|
+
summary: pci ? "LTE cell PCI=#{pci} (N_ID_1=#{nid1} N_ID_2=#{hit[:nid2]}) CFO=#{hit[:cfo_hz]}Hz" : "LTE PSS lock N_ID_2=#{hit[:nid2]} CFO=#{hit[:cfo_hz]}Hz (SSS pending)"
|
|
61
50
|
)
|
|
62
|
-
yield msg if block_given?
|
|
63
51
|
end
|
|
64
|
-
i += hop
|
|
65
52
|
end
|
|
53
|
+
@buf.shift(@buf.length - (9600 * 2)) if @buf.length > 9600 * 4
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
# Build 3 time-domain PSS templates (128 complex samples each).
|
|
59
|
+
def build_pss
|
|
60
|
+
PSS_ROOTS.transform_values do |root|
|
|
61
|
+
zc = PWN::SDR::Decoder::DSP.zadoff_chu(root: root, n: 63)
|
|
62
|
+
# Map 62 ZC values (drop k=31) onto ±31 subcarriers of a 128-FFT.
|
|
63
|
+
spec = Array.new(NFFT * 2, 0.0)
|
|
64
|
+
62.times do |m|
|
|
65
|
+
zi = m < 31 ? m : m + 1 # skip DC element
|
|
66
|
+
sc = m - 31
|
|
67
|
+
k = sc.negative? ? sc + NFFT : sc + 1 # DC left null
|
|
68
|
+
spec[k * 2] = zc[zi * 2]
|
|
69
|
+
spec[(k * 2) + 1] = zc[(zi * 2) + 1]
|
|
70
|
+
end
|
|
71
|
+
td = PWN::FFI.available?(mod: :FFTW) ? PWN::FFI::FFTW.cfft(iq: spec, n: NFFT, sign: :backward) : PWN::SDR::Decoder::DSP.dft_naive(iq: spec, n: NFFT)
|
|
72
|
+
td.flat_map { |re, im| [re, im] }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def search_pss
|
|
77
|
+
n = @buf.length / 2
|
|
78
|
+
best = nil
|
|
79
|
+
@pss.each do |nid2, tmpl|
|
|
80
|
+
# sliding conj-multiply-accumulate every 4th sample for speed
|
|
81
|
+
i = 0
|
|
82
|
+
while i < n - NFFT
|
|
83
|
+
acc_r = 0.0
|
|
84
|
+
acc_i = 0.0
|
|
85
|
+
NFFT.times do |j|
|
|
86
|
+
ar = @buf[(i + j) * 2]
|
|
87
|
+
ai = @buf[((i + j) * 2) + 1]
|
|
88
|
+
br = tmpl[j * 2]
|
|
89
|
+
bi = tmpl[(j * 2) + 1]
|
|
90
|
+
acc_r += (ar * br) + (ai * bi)
|
|
91
|
+
acc_i += (ai * br) - (ar * bi)
|
|
92
|
+
end
|
|
93
|
+
pk = (acc_r * acc_r) + (acc_i * acc_i)
|
|
94
|
+
best = { nid2: nid2, pos: i, pk: pk, ang: Math.atan2(acc_i, acc_r) } if best.nil? || pk > best[:pk]
|
|
95
|
+
i += 4
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
return nil unless best
|
|
99
|
+
|
|
100
|
+
# crude floor: mean |buf|² · NFFT
|
|
101
|
+
m2 = PWN::SDR::Decoder::DSP.mag_sq(iq: @buf[0, NFFT * 8])
|
|
102
|
+
floor = (m2.sum / m2.length) * NFFT
|
|
103
|
+
ratio = best[:pk] / [floor, 1e-12].max
|
|
104
|
+
return nil unless ratio > 6.0
|
|
105
|
+
|
|
106
|
+
# coarse CFO from CP: correlate CP with tail of same OFDM symbol
|
|
107
|
+
pos = best[:pos]
|
|
108
|
+
cfo = cp_cfo(pos)
|
|
109
|
+
best.merge(ratio: ratio.round(1), cfo_hz: cfo)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def cp_cfo(pos)
|
|
113
|
+
return 0 if pos < CP_NORM
|
|
114
|
+
|
|
115
|
+
a = @buf[(pos - CP_NORM) * 2, CP_NORM * 2]
|
|
116
|
+
b = @buf[(pos + NFFT - CP_NORM) * 2, CP_NORM * 2]
|
|
117
|
+
r = 0.0
|
|
118
|
+
im = 0.0
|
|
119
|
+
CP_NORM.times do |j|
|
|
120
|
+
r += (a[j * 2] * b[j * 2]) + (a[(j * 2) + 1] * b[(j * 2) + 1])
|
|
121
|
+
im += (a[(j * 2) + 1] * b[j * 2]) - (a[j * 2] * b[(j * 2) + 1])
|
|
122
|
+
end
|
|
123
|
+
(Math.atan2(im, r) * FS_BASE / (2 * Math::PI * NFFT)).round
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# SSS is one OFDM symbol before PSS. Extract 62 subcarriers,
|
|
127
|
+
# brute-force N_ID_1 by testing all 168 m-sequence pairs (m0,m1).
|
|
128
|
+
def search_sss(hit)
|
|
129
|
+
pos = hit[:pos] - (NFFT + CP_NORM)
|
|
130
|
+
return nil if pos.negative? || (pos + NFFT) * 2 > @buf.length
|
|
131
|
+
|
|
132
|
+
sym = @buf[pos * 2, NFFT * 2]
|
|
133
|
+
spec = PWN::SDR::Decoder::DSP.cfft_mag(iq: sym, n: NFFT, shift: false)
|
|
134
|
+
# extract 62 SSS subcarriers (±31, skip DC)
|
|
135
|
+
d = Array.new(62)
|
|
136
|
+
62.times do |m|
|
|
137
|
+
sc = m - 31
|
|
138
|
+
k = sc.negative? ? sc + NFFT : sc + 1
|
|
139
|
+
d[m] = spec[k]
|
|
140
|
+
end
|
|
141
|
+
# Magnitude-only heuristic (BPSK): match interleaved even/odd
|
|
142
|
+
# sub-sequences against m-sequence energy pattern per N_ID_1.
|
|
143
|
+
best = nil
|
|
144
|
+
168.times do |nid1|
|
|
145
|
+
m0, m1 = LTE.sss_indices(nid1: nid1)
|
|
146
|
+
s0 = LTE.mseq(shift: m0)
|
|
147
|
+
s1 = LTE.mseq(shift: m1)
|
|
148
|
+
c0 = LTE.cseq(nid2: hit[:nid2])
|
|
149
|
+
score = 0.0
|
|
150
|
+
31.times do |n|
|
|
151
|
+
score += d[2 * n] * s0[n] * c0[n]
|
|
152
|
+
score += d[(2 * n) + 1] * s1[n] * c0[n]
|
|
153
|
+
end
|
|
154
|
+
best = { nid1: nid1, score: score.abs } if best.nil? || score.abs > best[:score]
|
|
155
|
+
end
|
|
156
|
+
best && best[:score].positive? ? best[:nid1] : nil
|
|
66
157
|
end
|
|
67
158
|
end
|
|
68
159
|
|
|
160
|
+
# SSS helper: (m0, m1) pair for a given N_ID_1 per TS 36.211 §6.11.2.
|
|
161
|
+
public_class_method def self.sss_indices(opts = {})
|
|
162
|
+
nid1 = opts[:nid1].to_i
|
|
163
|
+
qp = (nid1 / 30)
|
|
164
|
+
q = ((nid1 + (qp * (qp + 1) / 2)) / 30)
|
|
165
|
+
mp = nid1 + (q * (q + 1) / 2)
|
|
166
|
+
m0 = mp % 31
|
|
167
|
+
m1 = (m0 + (mp / 31) + 1) % 31
|
|
168
|
+
[m0, m1]
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Length-31 m-sequence x^5+x^2+1, cyclic-shifted by `shift`, as ±1.
|
|
172
|
+
public_class_method def self.mseq(opts = {})
|
|
173
|
+
@mseq_base ||= begin
|
|
174
|
+
reg = [0, 0, 0, 0, 1]
|
|
175
|
+
Array.new(31) do
|
|
176
|
+
o = reg[0]
|
|
177
|
+
fb = reg[0] ^ reg[3]
|
|
178
|
+
reg = reg[1..] + [fb]
|
|
179
|
+
1 - (2 * o)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
sh = opts[:shift].to_i
|
|
183
|
+
Array.new(31) { |n| @mseq_base[(n + sh) % 31] }
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Scrambling sequence c0 (x^5+x^3+1) tied to N_ID_2, ±1.
|
|
187
|
+
public_class_method def self.cseq(opts = {})
|
|
188
|
+
@cseq_base ||= begin
|
|
189
|
+
reg = [0, 0, 0, 0, 1]
|
|
190
|
+
Array.new(31) do
|
|
191
|
+
o = reg[0]
|
|
192
|
+
fb = reg[0] ^ reg[2]
|
|
193
|
+
reg = reg[1..] + [fb]
|
|
194
|
+
1 - (2 * o)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
sh = opts[:nid2].to_i
|
|
198
|
+
Array.new(31) { |n| @cseq_base[(n + sh) % 31] }
|
|
199
|
+
end
|
|
200
|
+
|
|
69
201
|
# Supported Method Parameters::
|
|
70
202
|
# PWN::SDR::Decoder::LTE.decode(
|
|
71
203
|
# freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
@@ -73,22 +205,15 @@ module PWN
|
|
|
73
205
|
|
|
74
206
|
public_class_method def self.decode(opts = {})
|
|
75
207
|
freq_obj = opts[:freq_obj]
|
|
76
|
-
|
|
77
|
-
rate = (opts[:sample_rate] || freq_obj[:iq_rate] || 2_000_000).to_i
|
|
78
|
-
proto = 'LTE'
|
|
79
|
-
demod = DemodIQ.new(
|
|
80
|
-
rate: rate, protocol: proto, modulation: 'OFDMA',
|
|
81
|
-
extra: { threshold: 4.0 }
|
|
82
|
-
)
|
|
208
|
+
rate = (opts[:sample_rate] || freq_obj[:iq_rate] || 1_920_000).to_i
|
|
83
209
|
PWN::SDR::Decoder::Base.run_iq(
|
|
84
210
|
freq_obj: freq_obj,
|
|
85
|
-
protocol:
|
|
211
|
+
protocol: 'LTE',
|
|
86
212
|
sample_rate: rate,
|
|
87
213
|
source: opts[:source],
|
|
88
214
|
file: opts[:file],
|
|
89
|
-
demod:
|
|
90
|
-
|
|
91
|
-
note: 'OFDMA 1.4–20 MHz — true-air I/Q path reports channel occupancy; PSS/SSS cell search layered on FFTW when available.',
|
|
215
|
+
demod: DemodIQ.new(rate: rate),
|
|
216
|
+
note: 'OFDMA — I/Q→1.92 Msps→PSS ZC-correlate (FFTW)→N_ID_2+CFO→SSS m-seq→PCI.',
|
|
92
217
|
describe: proc { |_b| { modulation: 'OFDMA', subcarrier_khz: 15 } }
|
|
93
218
|
)
|
|
94
219
|
end
|
|
@@ -99,9 +224,7 @@ module PWN
|
|
|
99
224
|
out[:earfcn] = ::Regexp.last_match(1) if line =~ /EARFCN[:= ]+(\d+)/i
|
|
100
225
|
out[:pci] = ::Regexp.last_match(1) if line =~ /(?:PCI|N_id_cell|Id)[:= ]+(\d{1,3})/i
|
|
101
226
|
out[:prb] = (::Regexp.last_match(1) || ::Regexp.last_match(2)) if line =~ /(?:PRB[:= ]+(\d+)|(\d+)\s*PRB)/i
|
|
102
|
-
out[:cp] = ::Regexp.last_match(1) if line =~ /\b(Normal|Extended)\b\s*CP/i
|
|
103
227
|
out[:rsrp] = ::Regexp.last_match(1) if line =~ /(-?\d+(?:\.\d+)?)\s*dBm/
|
|
104
|
-
out[:freq_mhz] = ::Regexp.last_match(1) if line =~ /(\d{3,4}\.\d)\s*MHz/
|
|
105
228
|
out[:summary] = "LTE PCI=#{out[:pci]} EARFCN=#{out[:earfcn]} PRB=#{out[:prb]} RSRP=#{out[:rsrp]}dBm"
|
|
106
229
|
out.compact
|
|
107
230
|
end
|
|
@@ -117,7 +240,7 @@ module PWN
|
|
|
117
240
|
#{self}.decode(
|
|
118
241
|
freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq',
|
|
119
242
|
source: 'optional - :auto|:rtlsdr|:adalm_pluto|:file',
|
|
120
|
-
file: 'optional - .cu8/.cs16 capture'
|
|
243
|
+
file: 'optional - .cu8/.cs16 capture (≥1.92 Msps)'
|
|
121
244
|
)
|
|
122
245
|
|
|
123
246
|
#{self}.authors
|
data/lib/pwn/sdr/decoder/p25.rb
CHANGED
|
@@ -3,66 +3,95 @@
|
|
|
3
3
|
module PWN
|
|
4
4
|
module SDR
|
|
5
5
|
module Decoder
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
6
|
+
# APCO Project 25 Phase-1 (C4FM) true-air decoder.
|
|
7
|
+
#
|
|
8
|
+
# I/Q → PWN::FFI::Liquid.freq_demod (or DSP.fm_demod_iq) → resample to
|
|
9
|
+
# 48 kHz → 4-level slice at 4800 sym/s → dibits → hunt the 24-symbol
|
|
10
|
+
# Frame Sync (0x5575F5FF77FF) → recover the 64-bit NID (12-bit NAC +
|
|
11
|
+
# 4-bit DUID + BCH(63,16,23) parity). Emits {nac:, duid:, duid_name:}
|
|
12
|
+
# per frame — the same intel OP25 / DSD show — with no external binary.
|
|
9
13
|
module P25
|
|
10
|
-
#
|
|
14
|
+
# 24-symbol / 48-bit Frame Sync (dibit MSB-first)
|
|
15
|
+
FS_DIBITS = [
|
|
16
|
+
1, 1, 1, 1, 3, 1, 1, 3, 3, 3, 3, 1, 1, 3, 3, 3,
|
|
17
|
+
3, 3, 3, 3, 1, 3, 3, 3, 3, 3, 3, 3
|
|
18
|
+
].freeze # → 0x5575F5FF77FF (see TIA-102.BAAA)
|
|
19
|
+
FS_DIBITS_24 = [
|
|
20
|
+
1, 1, 1, 1, 3, 1, 1, 3, 3, 3, 3, 1,
|
|
21
|
+
3, 3, 1, 1, 3, 3, 3, 3, 3, 3, 3, 3
|
|
22
|
+
].freeze
|
|
23
|
+
# Correct 24-dibit Frame Sync per TIA-102 (+3 +3 +3 +3 −3 +3 …).
|
|
24
|
+
# Derived from bit pattern 5575F5FF77FF, MSB-first, 2 bits/sym,
|
|
25
|
+
# C4FM map: 01→+3, 00→+1, 10→−1, 11→−3 → dibits {1,0,2,3}.
|
|
26
|
+
FRAME_SYNC = 0x5575F5FF77FF
|
|
27
|
+
FS_BITS = Array.new(48) { |i| (FRAME_SYNC >> (47 - i)) & 1 }.freeze
|
|
28
|
+
FS_SYMS = FS_BITS.each_slice(2).map { |a, b| (a << 1) | b }.freeze
|
|
29
|
+
|
|
30
|
+
DUID_NAME = {
|
|
31
|
+
0x0 => 'HDU', 0x3 => 'TDU', 0x5 => 'LDU1', 0x7 => 'TSBK',
|
|
32
|
+
0xA => 'LDU2', 0xC => 'PDU', 0xF => 'TDULC'
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
# Streaming C4FM demod for Base.run_iq — I/Q → NAC/DUID frames.
|
|
11
36
|
class DemodIQ
|
|
12
|
-
|
|
37
|
+
AUDIO_RATE = 48_000
|
|
38
|
+
|
|
39
|
+
def initialize(rate:)
|
|
13
40
|
@rate = rate.to_f
|
|
14
|
-
@
|
|
15
|
-
@
|
|
16
|
-
@extra = extra
|
|
17
|
-
@floor = nil
|
|
18
|
-
@in_burst = false
|
|
19
|
-
@burst_t0 = nil
|
|
20
|
-
@peak = -200.0
|
|
21
|
-
@burst_n = 0
|
|
22
|
-
@threshold = (extra[:threshold] || 8.0).to_f
|
|
41
|
+
@dibits = []
|
|
42
|
+
@seen_fs = 0
|
|
23
43
|
end
|
|
24
44
|
|
|
25
45
|
def feed_iq(samples, rate: nil, &)
|
|
26
46
|
@rate = rate.to_f if rate
|
|
27
|
-
|
|
28
|
-
|
|
47
|
+
audio = PWN::SDR::Decoder::DSP.fm_demod_iq(iq: samples)
|
|
48
|
+
audio = PWN::SDR::Decoder::DSP.resample(
|
|
49
|
+
samples: audio, src_rate: @rate, dst_rate: AUDIO_RATE
|
|
50
|
+
)
|
|
51
|
+
syms = PWN::SDR::Decoder::DSP.slice_4fsk(
|
|
52
|
+
samples: audio, rate: AUDIO_RATE, baud: 4800
|
|
53
|
+
)
|
|
54
|
+
@dibits.concat(syms)
|
|
55
|
+
scan(&) if block_given?
|
|
56
|
+
@dibits.shift(@dibits.length - 4096) if @dibits.length > 8192
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def scan
|
|
62
|
+
fs = P25::FS_SYMS
|
|
29
63
|
i = 0
|
|
30
|
-
while i
|
|
31
|
-
|
|
32
|
-
|
|
64
|
+
while i <= @dibits.length - (24 + 32)
|
|
65
|
+
# Allow up to 3 symbol errors on FS
|
|
66
|
+
err = 0
|
|
67
|
+
j = 0
|
|
68
|
+
while j < 24
|
|
69
|
+
err += 1 if @dibits[i + j] != fs[j]
|
|
70
|
+
break if err > 3
|
|
33
71
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
protocol: @protocol, event: 'burst', source: 'iq',
|
|
51
|
-
burst_no: @burst_n, peak_dbfs: @peak.round(1),
|
|
52
|
-
floor_dbfs: @floor.round(1), delta_db: (@peak - @floor).round(1),
|
|
53
|
-
duration_ms: dur_ms, modulation: @modulation,
|
|
54
|
-
sample_rate: @rate.to_i
|
|
55
|
-
}.merge(@extra.except(:threshold))
|
|
56
|
-
# protocol-specific enrichment
|
|
57
|
-
msg.merge!(self.class.enrich(msg)) if self.class.respond_to?(:enrich)
|
|
58
|
-
msg[:summary] = format(
|
|
59
|
-
'%<p>s IQ-burst #%<n>d peak=%<pk>+.1f dBFS Δ=%<d>.1f dB dur=%<ms>d ms',
|
|
60
|
-
p: @protocol, n: @burst_n, pk: @peak, d: @peak - @floor, ms: dur_ms
|
|
72
|
+
j += 1
|
|
73
|
+
end
|
|
74
|
+
if err <= 3
|
|
75
|
+
nid_syms = @dibits[i + 24, 32] # 32 dibits = 64 bits
|
|
76
|
+
nid_bits = nid_syms.flat_map { |d| [(d >> 1) & 1, d & 1] }
|
|
77
|
+
nid = PWN::SDR::Decoder::DSP.bits_to_int(bits: nid_bits[0, 16])
|
|
78
|
+
nac = (nid >> 4) & 0xFFF
|
|
79
|
+
duid = nid & 0xF
|
|
80
|
+
@seen_fs += 1
|
|
81
|
+
yield(
|
|
82
|
+
protocol: 'P25', event: 'frame', modulation: 'C4FM',
|
|
83
|
+
fs_errors: err, nac: format('%03X', nac), duid: duid,
|
|
84
|
+
duid_name: P25::DUID_NAME[duid] || format('0x%X', duid),
|
|
85
|
+
nid_hex: format('%016X', PWN::SDR::Decoder::DSP.bits_to_int(bits: nid_bits)),
|
|
86
|
+
frame_no: @seen_fs,
|
|
87
|
+
summary: "P25 NAC=#{format('%03X', nac)} DUID=#{P25::DUID_NAME[duid] || duid} (fs_err=#{err})"
|
|
61
88
|
)
|
|
62
|
-
|
|
89
|
+
i += 24 + 32
|
|
90
|
+
else
|
|
91
|
+
i += 1
|
|
63
92
|
end
|
|
64
|
-
i += hop
|
|
65
93
|
end
|
|
94
|
+
@dibits.shift([i - 24, 0].max) if i > 24
|
|
66
95
|
end
|
|
67
96
|
end
|
|
68
97
|
|
|
@@ -73,29 +102,16 @@ module PWN
|
|
|
73
102
|
|
|
74
103
|
public_class_method def self.decode(opts = {})
|
|
75
104
|
freq_obj = opts[:freq_obj]
|
|
76
|
-
|
|
77
|
-
rate = (opts[:sample_rate] || freq_obj[:iq_rate] || 480_000).to_i
|
|
78
|
-
proto = 'P25'
|
|
79
|
-
demod = DemodIQ.new(
|
|
80
|
-
rate: rate, protocol: proto, modulation: 'C4FM',
|
|
81
|
-
extra: { threshold: 6.0 }
|
|
82
|
-
)
|
|
105
|
+
rate = (opts[:sample_rate] || freq_obj[:iq_rate] || (48_000 * 20)).to_i
|
|
83
106
|
PWN::SDR::Decoder::Base.run_iq(
|
|
84
107
|
freq_obj: freq_obj,
|
|
85
|
-
protocol:
|
|
108
|
+
protocol: 'P25',
|
|
86
109
|
sample_rate: rate,
|
|
87
110
|
source: opts[:source],
|
|
88
111
|
file: opts[:file],
|
|
89
|
-
demod:
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
describe: proc { |b|
|
|
93
|
-
{ modulation: 'C4FM', classification: (if b[:duration_ms] > 1500
|
|
94
|
-
'voice-superframe'
|
|
95
|
-
else
|
|
96
|
-
(b[:duration_ms] > 150 ? 'LDU/HDR' : 'TSBK/control')
|
|
97
|
-
end) }
|
|
98
|
-
}
|
|
112
|
+
demod: DemodIQ.new(rate: rate),
|
|
113
|
+
note: 'C4FM 4800 sym/s — I/Q→FM→4-FSK→FS 0x5575F5FF77FF→NAC/DUID.',
|
|
114
|
+
describe: proc { |b| { modulation: 'C4FM', classification: b[:duration_ms] > 180 ? 'voice-LDU' : 'TSBK/control' } }
|
|
99
115
|
)
|
|
100
116
|
end
|
|
101
117
|
|
|
@@ -105,7 +121,7 @@ module PWN
|
|
|
105
121
|
out[:nac] = ::Regexp.last_match(1) if line =~ /NAC[:= ]+([0-9A-Fa-f]+)/
|
|
106
122
|
out[:tg] = ::Regexp.last_match(1) if line =~ /(?:TG|talkgroup)[:= ]+(\d+)/i
|
|
107
123
|
out[:rid] = ::Regexp.last_match(1) if line =~ /(?:RID|src|source)[:= ]+(\d+)/i
|
|
108
|
-
out[:
|
|
124
|
+
out[:duid] = ::Regexp.last_match(1) if line =~ /DUID[:= ]+(\w+)/i
|
|
109
125
|
out[:summary] = "P25 NAC=#{out[:nac]} TG=#{out[:tg]} RID=#{out[:rid]}"
|
|
110
126
|
out.compact
|
|
111
127
|
end
|