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
|
@@ -3,73 +3,147 @@
|
|
|
3
3
|
module PWN
|
|
4
4
|
module SDR
|
|
5
5
|
module Decoder
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
6
|
+
# Bluetooth LE (& BR/EDR sync-trailer) true-air decoder.
|
|
7
|
+
#
|
|
8
|
+
# I/Q → PWN::FFI::Liquid.gmsk_demod (or DSP.fm_demod_iq→NRZ) at
|
|
9
|
+
# 1 Mbit/s → hunt LSB-first Access Address (adv = 0x8E89BED6) →
|
|
10
|
+
# dewhiten (7-bit LFSR seeded ch|0x40) → PDU header (type/len) →
|
|
11
|
+
# AdvA (6 bytes) → CRC-24 (poly 0x65B, init 0x555555). Emits per-PDU
|
|
12
|
+
# {access_addr:, pdu_type:, adv_addr:, crc_ok:} — ubertooth-parity
|
|
13
|
+
# advertising sniff with no external binary.
|
|
9
14
|
module Bluetooth
|
|
10
|
-
|
|
15
|
+
BLE_ADV_AA = 0x8E89BED6
|
|
16
|
+
BLE_CRC_POLY = 0x65B
|
|
17
|
+
BLE_CRC_INIT = 0x555555
|
|
18
|
+
BLE_PDU_TYPE = {
|
|
19
|
+
0 => 'ADV_IND', 1 => 'ADV_DIRECT_IND', 2 => 'ADV_NONCONN_IND',
|
|
20
|
+
3 => 'SCAN_REQ', 4 => 'SCAN_RSP', 5 => 'CONNECT_IND',
|
|
21
|
+
6 => 'ADV_SCAN_IND', 7 => 'ADV_EXT_IND'
|
|
22
|
+
}.freeze
|
|
23
|
+
BLE_ADV_CHANNELS = { 37 => 2_402_000_000, 38 => 2_426_000_000, 39 => 2_480_000_000 }.freeze
|
|
24
|
+
# BR/EDR: 64-bit sync word derived from LAP; general-inquiry LAP=0x9E8B33.
|
|
25
|
+
GIAC_LAP = 0x9E8B33
|
|
26
|
+
|
|
27
|
+
# Streaming BLE GFSK demod for Base.run_iq — I/Q → advertising PDUs.
|
|
11
28
|
class DemodIQ
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
@
|
|
16
|
-
@
|
|
17
|
-
@
|
|
18
|
-
@
|
|
19
|
-
@
|
|
20
|
-
@peak = -200.0
|
|
21
|
-
@burst_n = 0
|
|
22
|
-
@threshold = (extra[:threshold] || 8.0).to_f
|
|
29
|
+
BAUD = 1_000_000
|
|
30
|
+
|
|
31
|
+
def initialize(rate:, channel: 37, ble: true)
|
|
32
|
+
@rate = rate.to_f
|
|
33
|
+
@channel = channel
|
|
34
|
+
@ble = ble
|
|
35
|
+
@bits = []
|
|
36
|
+
@seen = {}
|
|
23
37
|
end
|
|
24
38
|
|
|
25
39
|
def feed_iq(samples, rate: nil, &)
|
|
26
40
|
@rate = rate.to_f if rate
|
|
27
|
-
|
|
28
|
-
|
|
41
|
+
# Try both polarities — GFSK sign depends on tuner spectral inversion.
|
|
42
|
+
new_bits = PWN::SDR::Decoder::DSP.gfsk_slice(
|
|
43
|
+
iq: samples, rate: @rate, baud: BAUD, bt: 0.5
|
|
44
|
+
)
|
|
45
|
+
@bits.concat(new_bits)
|
|
46
|
+
scan(&) if block_given?
|
|
47
|
+
@bits.shift(@bits.length - 4096) if @bits.length > 65_536
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
# 32-bit AA is transmitted LSB-first on the air.
|
|
53
|
+
AA_BITS = Array.new(32) { |i| (BLE_ADV_AA >> i) & 1 }
|
|
54
|
+
AA_INV = AA_BITS.map { |b| b ^ 1 }.freeze
|
|
55
|
+
|
|
56
|
+
def scan
|
|
29
57
|
i = 0
|
|
30
|
-
while i
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
58
|
+
while i <= @bits.length - (32 + 16 + 24)
|
|
59
|
+
inv = nil
|
|
60
|
+
inv = false if match_at?(i, AA_BITS)
|
|
61
|
+
inv = true if inv.nil? && match_at?(i, AA_INV)
|
|
62
|
+
unless inv.nil?
|
|
63
|
+
pdu = decode_pdu(@bits[(i + 32)..], inv)
|
|
64
|
+
if pdu
|
|
65
|
+
key = "#{pdu[:adv_addr]}:#{pdu[:pdu_type]}:#{pdu[:crc_rx]}"
|
|
66
|
+
unless @seen[key]
|
|
67
|
+
@seen[key] = true
|
|
68
|
+
@seen.shift if @seen.length > 256
|
|
69
|
+
yield pdu
|
|
70
|
+
end
|
|
71
|
+
i += 32 + ((2 + pdu[:length].to_i + 3) * 8)
|
|
72
|
+
next
|
|
43
73
|
end
|
|
44
|
-
@peak = lvl if lvl > @peak
|
|
45
|
-
elsif @in_burst
|
|
46
|
-
@in_burst = false
|
|
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: 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
|
|
61
|
-
)
|
|
62
|
-
yield msg if block_given?
|
|
63
74
|
end
|
|
64
|
-
i +=
|
|
75
|
+
i += 1
|
|
65
76
|
end
|
|
77
|
+
@bits.shift([i - 32, 0].max) if i > 32
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def match_at?(idx, pat, max_err: 2)
|
|
81
|
+
err = 0
|
|
82
|
+
j = 0
|
|
83
|
+
while j < pat.length
|
|
84
|
+
err += 1 if @bits[idx + j] != pat[j]
|
|
85
|
+
return false if err > max_err
|
|
86
|
+
|
|
87
|
+
j += 1
|
|
88
|
+
end
|
|
89
|
+
true
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def decode_pdu(stream, inv)
|
|
93
|
+
stream = stream.map { |b| b ^ 1 } if inv
|
|
94
|
+
hdr_b = PWN::SDR::Decoder::DSP.bytes_from_bits(bits: stream[0, 16], lsb_first: true)
|
|
95
|
+
hdr = PWN::SDR::Decoder::DSP.whiten_lfsr(
|
|
96
|
+
bytes: hdr_b, poly: 0x11, init: (@channel & 0x3F) | 0x40, width: 7
|
|
97
|
+
)
|
|
98
|
+
return nil if hdr.length < 2
|
|
99
|
+
|
|
100
|
+
ptype = hdr[0] & 0x0F
|
|
101
|
+
len = hdr[1] & 0xFF
|
|
102
|
+
return nil if len > 255 || (2 + len + 3) * 8 > stream.length
|
|
103
|
+
|
|
104
|
+
body_b = PWN::SDR::Decoder::DSP.bytes_from_bits(
|
|
105
|
+
bits: stream[0, (2 + len + 3) * 8], lsb_first: true
|
|
106
|
+
)
|
|
107
|
+
dewh = PWN::SDR::Decoder::DSP.whiten_lfsr(
|
|
108
|
+
bytes: body_b, poly: 0x11, init: (@channel & 0x3F) | 0x40, width: 7
|
|
109
|
+
)
|
|
110
|
+
payload = dewh[2, len] || []
|
|
111
|
+
crc_rx = dewh[2 + len, 3] || []
|
|
112
|
+
crc_ok = Bluetooth.ble_crc24(bytes: dewh[0, 2 + len]) ==
|
|
113
|
+
(crc_rx[0].to_i | (crc_rx[1].to_i << 8) | (crc_rx[2].to_i << 16))
|
|
114
|
+
adv_addr = payload.length >= 6 ? payload[0, 6].reverse.map { |b| format('%02X', b) }.join(':') : nil
|
|
115
|
+
{
|
|
116
|
+
protocol: @ble ? 'BLE' : 'BT-BR/EDR', event: 'pdu',
|
|
117
|
+
modulation: 'GFSK', channel: @channel,
|
|
118
|
+
access_addr: format('%08X', BLE_ADV_AA),
|
|
119
|
+
pdu_type: BLE_PDU_TYPE[ptype] || ptype,
|
|
120
|
+
tx_add: (hdr[0] >> 6) & 1, rx_add: (hdr[0] >> 7) & 1,
|
|
121
|
+
length: len, adv_addr: adv_addr, crc_ok: crc_ok,
|
|
122
|
+
crc_rx: crc_rx.map { |b| format('%02X', b) }.join,
|
|
123
|
+
payload_hex: payload.map { |b| format('%02X', b) }.join,
|
|
124
|
+
summary: "BLE #{BLE_PDU_TYPE[ptype] || ptype} AdvA=#{adv_addr} len=#{len} ch=#{@channel} crc=#{crc_ok ? 'OK' : 'BAD'}"
|
|
125
|
+
}
|
|
66
126
|
end
|
|
127
|
+
end
|
|
67
128
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
129
|
+
# Supported Method Parameters::
|
|
130
|
+
# crc = PWN::SDR::Decoder::Bluetooth.ble_crc24(bytes: Array<Integer>)
|
|
131
|
+
# BLE CRC-24 (LSB-first LFSR, poly 0x65B, init 0x555555).
|
|
132
|
+
|
|
133
|
+
public_class_method def self.ble_crc24(opts = {})
|
|
134
|
+
bytes = opts[:bytes] || []
|
|
135
|
+
reg = BLE_CRC_INIT
|
|
136
|
+
bytes.each do |byte|
|
|
137
|
+
8.times do |i|
|
|
138
|
+
b = (byte >> i) & 1
|
|
139
|
+
fb = (reg ^ b) & 1
|
|
140
|
+
reg >>= 1
|
|
141
|
+
reg ^= (BLE_CRC_POLY << 0) | 0xB4C000 if fb == 1
|
|
142
|
+
reg &= 0xFFFFFF
|
|
143
|
+
end
|
|
72
144
|
end
|
|
145
|
+
# Register holds CRC LSB-first — return as-is (matched LSB-first on air)
|
|
146
|
+
reg
|
|
73
147
|
end
|
|
74
148
|
|
|
75
149
|
# Supported Method Parameters::
|
|
@@ -80,31 +154,20 @@ module PWN
|
|
|
80
154
|
public_class_method def self.decode(opts = {})
|
|
81
155
|
freq_obj = opts[:freq_obj]
|
|
82
156
|
hz = PWN::SDR.hz_to_i(freq: freq_obj[:freq])
|
|
83
|
-
ble = freq_obj[:ble] || freq_obj[:mode].to_s.casecmp('ble').zero?
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
demod = DemodIQ.new(
|
|
89
|
-
rate: rate, protocol: proto, modulation: 'GFSK',
|
|
90
|
-
extra: { threshold: 9.0 }
|
|
91
|
-
)
|
|
157
|
+
ble = freq_obj[:ble] || freq_obj[:mode].to_s.casecmp('ble').zero? || true
|
|
158
|
+
# Nearest BLE advertising channel unless caller forces one.
|
|
159
|
+
ch = opts[:channel] ||
|
|
160
|
+
BLE_ADV_CHANNELS.min_by { |_, f| (f - hz).abs }&.first || 37
|
|
161
|
+
rate = (opts[:sample_rate] || freq_obj[:iq_rate] || 4_000_000).to_i
|
|
92
162
|
PWN::SDR::Decoder::Base.run_iq(
|
|
93
163
|
freq_obj: freq_obj,
|
|
94
|
-
protocol:
|
|
164
|
+
protocol: ble ? 'BLE' : 'BT-BR/EDR',
|
|
95
165
|
sample_rate: rate,
|
|
96
166
|
source: opts[:source],
|
|
97
167
|
file: opts[:file],
|
|
98
|
-
demod:
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
describe: proc { |b|
|
|
102
|
-
{ modulation: 'GFSK', channel: begin
|
|
103
|
-
b[:channel]
|
|
104
|
-
rescue StandardError
|
|
105
|
-
nil
|
|
106
|
-
end, hop_slots: (b[:duration_ms] / 0.625).round }
|
|
107
|
-
}
|
|
168
|
+
demod: DemodIQ.new(rate: rate, channel: ch, ble: ble),
|
|
169
|
+
note: '1 Mbit/s GFSK — I/Q→gmskdem→AA 0x8E89BED6→dewhiten→PDU/AdvA/CRC-24.',
|
|
170
|
+
describe: proc { |b| { modulation: 'GFSK', channel: ch, hop_slots: (b[:duration_ms] / 0.625).round } }
|
|
108
171
|
)
|
|
109
172
|
end
|
|
110
173
|
|
|
@@ -132,9 +195,12 @@ module PWN
|
|
|
132
195
|
#{self}.decode(
|
|
133
196
|
freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq',
|
|
134
197
|
source: 'optional - :auto|:rtlsdr|:adalm_pluto|:file',
|
|
135
|
-
file: 'optional - .cu8/.cs16 capture'
|
|
198
|
+
file: 'optional - .cu8/.cs16 capture (≥2 Msps)',
|
|
199
|
+
channel: 'optional - BLE adv channel 37|38|39 (default nearest to freq)'
|
|
136
200
|
)
|
|
137
201
|
|
|
202
|
+
#{self}.ble_crc24(bytes: [..])
|
|
203
|
+
|
|
138
204
|
#{self}.authors
|
|
139
205
|
"
|
|
140
206
|
end
|
data/lib/pwn/sdr/decoder/dect.rb
CHANGED
|
@@ -3,66 +3,96 @@
|
|
|
3
3
|
module PWN
|
|
4
4
|
module SDR
|
|
5
5
|
module Decoder
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
6
|
+
# DECT (ETSI EN 300 175) true-air decoder.
|
|
7
|
+
#
|
|
8
|
+
# 1.152 Mbit/s GFSK, 24-slot / 10 ms TDMA. I/Q → PWN::FFI::Liquid
|
|
9
|
+
# gmskdem (or DSP.fm_demod_iq→NRZ) → hunt 32-bit S-field
|
|
10
|
+
# (16-bit preamble + 16-bit sync 0xE98A FP / 0x1675 PP) → A-field
|
|
11
|
+
# (64 bits: 8-bit header + 40-bit tail + 16-bit R-CRC) → RFPI
|
|
12
|
+
# extraction on Nt/Qt tails. Emits {rfpi:, role:, slot_est:, crc_ok:}.
|
|
9
13
|
module DECT
|
|
10
|
-
|
|
14
|
+
SYNC_FP = 0xAAAAE98A
|
|
15
|
+
SYNC_PP = 0x55551675
|
|
16
|
+
BAUD = 1_152_000
|
|
17
|
+
# R-CRC-16 poly x^16+x^10+x^8+x^7+x^3+1 = 0x0589, init 0x0000.
|
|
18
|
+
RCRC_POLY = 0x0589
|
|
19
|
+
A_TA = { 0 => 'Ct', 1 => 'Ct', 2 => 'Nt', 3 => 'Nt', 4 => 'Qt', 5 => 'Nt', 6 => 'Mt', 7 => 'Pt' }.freeze
|
|
20
|
+
|
|
21
|
+
# Streaming DECT GFSK demod for Base.run_iq — I/Q → RFPI/A-field.
|
|
11
22
|
class DemodIQ
|
|
12
|
-
def initialize(rate:,
|
|
13
|
-
@rate
|
|
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
|
+
def initialize(rate:, carrier: nil)
|
|
24
|
+
@rate = rate.to_f
|
|
25
|
+
@carrier = carrier
|
|
26
|
+
@bits = []
|
|
27
|
+
@seen = {}
|
|
23
28
|
end
|
|
24
29
|
|
|
25
30
|
def feed_iq(samples, rate: nil, &)
|
|
26
31
|
@rate = rate.to_f if rate
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
[false, true].each do |inv|
|
|
33
|
+
nb = PWN::SDR::Decoder::DSP.gfsk_slice(
|
|
34
|
+
iq: samples, rate: @rate, baud: BAUD, bt: 0.5, invert: inv
|
|
35
|
+
)
|
|
36
|
+
scan(nb, inv, &) if block_given?
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
33
41
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
def scan(bits, inv)
|
|
43
|
+
@bits.concat(bits)
|
|
44
|
+
fp = Array.new(32) { |i| (SYNC_FP >> (31 - i)) & 1 }
|
|
45
|
+
pp = Array.new(32) { |i| (SYNC_PP >> (31 - i)) & 1 }
|
|
46
|
+
i = 0
|
|
47
|
+
while i <= @bits.length - (32 + 64)
|
|
48
|
+
role = nil
|
|
49
|
+
role = 'FP' if err(@bits, i, fp) <= 3
|
|
50
|
+
role ||= 'PP' if err(@bits, i, pp) <= 3
|
|
51
|
+
if role
|
|
52
|
+
a = @bits[i + 32, 64]
|
|
53
|
+
hdr = PWN::SDR::Decoder::DSP.bits_to_int(bits: a[0, 8])
|
|
54
|
+
tail = a[8, 40]
|
|
55
|
+
rcrc = PWN::SDR::Decoder::DSP.bits_to_int(bits: a[48, 16])
|
|
56
|
+
bytes = PWN::SDR::Decoder::DSP.bytes_from_bits(bits: a[0, 48])
|
|
57
|
+
calc = PWN::SDR::Decoder::DSP.crc16(bytes: bytes, poly: RCRC_POLY, init: 0x0000)
|
|
58
|
+
# DECT R-CRC XORs the final register with 0x0001
|
|
59
|
+
crc_ok = ((calc ^ 0x0001) & 0xFFFF) == rcrc
|
|
60
|
+
ta = (hdr >> 5) & 0x7
|
|
61
|
+
rfpi = A_TA[ta] == 'Nt' ? tail[0, 40] : nil
|
|
62
|
+
rfpi_hex = rfpi ? format('%010X', PWN::SDR::Decoder::DSP.bits_to_int(bits: rfpi)) : nil
|
|
63
|
+
key = "#{role}:#{rfpi_hex}:#{format('%02X', hdr)}"
|
|
64
|
+
unless @seen[key] && crc_ok
|
|
65
|
+
@seen[key] = true if crc_ok
|
|
66
|
+
@seen.shift if @seen.length > 128
|
|
67
|
+
yield(
|
|
68
|
+
protocol: 'DECT', event: 'a_field', role: role,
|
|
69
|
+
modulation: 'GFSK', header: format('%02X', hdr),
|
|
70
|
+
ta: ta, ta_name: A_TA[ta], rfpi: rfpi_hex,
|
|
71
|
+
tail_hex: format('%010X', PWN::SDR::Decoder::DSP.bits_to_int(bits: tail)),
|
|
72
|
+
rcrc: format('%04X', rcrc), crc_ok: crc_ok,
|
|
73
|
+
carrier: @carrier, polarity_inverted: inv,
|
|
74
|
+
summary: "DECT #{role} TA=#{A_TA[ta]}#{" RFPI=#{rfpi_hex}" if rfpi_hex} R-CRC=#{crc_ok ? 'OK' : 'BAD'}"
|
|
75
|
+
)
|
|
43
76
|
end
|
|
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
|
|
61
|
-
)
|
|
62
|
-
yield msg if block_given?
|
|
77
|
+
i += 32 + 64 + 320 # skip past B-field
|
|
78
|
+
else
|
|
79
|
+
i += 1
|
|
63
80
|
end
|
|
64
|
-
i += hop
|
|
65
81
|
end
|
|
82
|
+
@bits.shift([i - 32, 0].max) if i > 32
|
|
83
|
+
@bits.shift(@bits.length - 8192) if @bits.length > 65_536
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def err(bits, idx, pat)
|
|
87
|
+
e = 0
|
|
88
|
+
j = 0
|
|
89
|
+
while j < pat.length
|
|
90
|
+
e += 1 if bits[idx + j] != pat[j]
|
|
91
|
+
return 99 if e > 3
|
|
92
|
+
|
|
93
|
+
j += 1
|
|
94
|
+
end
|
|
95
|
+
e
|
|
66
96
|
end
|
|
67
97
|
end
|
|
68
98
|
|
|
@@ -73,23 +103,19 @@ module PWN
|
|
|
73
103
|
|
|
74
104
|
public_class_method def self.decode(opts = {})
|
|
75
105
|
freq_obj = opts[:freq_obj]
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
rate: rate, protocol: proto, modulation: 'GFSK',
|
|
81
|
-
extra: { threshold: 7.0 }
|
|
82
|
-
)
|
|
106
|
+
hz = PWN::SDR.hz_to_i(freq: freq_obj[:freq])
|
|
107
|
+
# EU: carrier 0 = 1897.344 MHz, step 1.728 MHz down; US 1.9296 GHz.
|
|
108
|
+
carrier = ((1_897_344_000 - hz) / 1_728_000.0).round
|
|
109
|
+
rate = (opts[:sample_rate] || freq_obj[:iq_rate] || 2_304_000).to_i
|
|
83
110
|
PWN::SDR::Decoder::Base.run_iq(
|
|
84
111
|
freq_obj: freq_obj,
|
|
85
|
-
protocol:
|
|
112
|
+
protocol: 'DECT',
|
|
86
113
|
sample_rate: rate,
|
|
87
114
|
source: opts[:source],
|
|
88
115
|
file: opts[:file],
|
|
89
|
-
demod:
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
describe: proc { |b| { modulation: 'GFSK', tdma_slots: (b[:duration_ms] / 0.417).round, classification: (b[:duration_ms] / 0.417).round >= 24 ? 'FP-beacon-frame' : 'PP-burst' } }
|
|
116
|
+
demod: DemodIQ.new(rate: rate, carrier: carrier),
|
|
117
|
+
note: '1.152 Mbit/s GFSK — I/Q→gmskdem→S-field 0xE98A→A-field/RFPI/R-CRC.',
|
|
118
|
+
describe: proc { |b| { modulation: 'GFSK', tdma_slots: (b[:duration_ms] / 0.417).round } }
|
|
93
119
|
)
|
|
94
120
|
end
|
|
95
121
|
|
|
@@ -116,7 +142,7 @@ module PWN
|
|
|
116
142
|
#{self}.decode(
|
|
117
143
|
freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq',
|
|
118
144
|
source: 'optional - :auto|:rtlsdr|:adalm_pluto|:file',
|
|
119
|
-
file: 'optional - .cu8/.cs16 capture'
|
|
145
|
+
file: 'optional - .cu8/.cs16 capture (≥2.304 Msps)'
|
|
120
146
|
)
|
|
121
147
|
|
|
122
148
|
#{self}.authors
|