Module: PWN::SDR::Decoder::LoRa

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

Overview

LoRa (Semtech CSS) true-air preamble/sync-word decoder.

I/Q resampled so fs = BW (default 125 kHz), one complex sample per chirp step. For each SF ∈ 7..12: dechirp with a reference down-chirp (DSP.cmul + PWN::FFI::FFTW.cfft), find ≥6 consecutive symbols whose FFT-argmax bin is identical (preamble), then read the two sync-word symbols and two SFD down-chirps. Emits bw_hz:, sync_word:, preamble_len:, cfo_bins: — the same metadata gr-lora / rtl-lora surface, no external binary.

Defined Under Namespace

Classes: DemodIQ

Constant Summary collapse

DEFAULT_BW =
125_000
SF_RANGE =
(7..12)
KNOWN_SYNC =

Public LoRaWAN sync = 0x34; private/Meshtastic default = 0x12.

{ 0x34 => 'LoRaWAN', 0x12 => 'private/RadioLib' }.freeze

Class Method Summary collapse

Class Method Details

.authorsObject

Author(s)

0day Inc. support@0dayinc.com



170
171
172
# File 'lib/pwn/sdr/decoder/lora.rb', line 170

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::LoRa.decode( freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq' )



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/pwn/sdr/decoder/lora.rb', line 135

public_class_method def self.decode(opts = {})
  freq_obj = opts[:freq_obj]
  bw   = (opts[:bw] || freq_obj[:lora_bw] || DEFAULT_BW).to_i
  rate = (opts[:sample_rate] || freq_obj[:iq_rate] || (bw * 4)).to_i
  PWN::SDR::Decoder::Base.run_iq(
    freq_obj: freq_obj,
    protocol: 'LoRa',
    sample_rate: rate,
    source: opts[:source],
    file: opts[:file],
    demod: DemodIQ.new(rate: rate, bw: bw),
    note: 'CSS 125–500 kHz — I/Q→resample_iq(fs=BW)→dechirp+FFTW per SF→preamble/sync-word.',
    describe: proc { |b| { modulation: 'CSS', bw_khz_assumed: bw / 1000, sf_estimate: b[:sf] } }
  )
end

.helpObject



174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/pwn/sdr/decoder/lora.rb', line 174

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',
      bw:       'optional - LoRa BW Hz (default 125000)'
    )

    #{self}.authors
  "
end

.parse_line(opts = {}) ⇒ Object



151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/pwn/sdr/decoder/lora.rb', line 151

public_class_method def self.parse_line(opts = {})
  line = opts[:line].to_s
  h = begin
    JSON.parse(line, symbolize_names: true)
  rescue StandardError
    { unparsed: line }
  end
  out = { protocol: 'LoRa' }.merge(h)
  bits = []
  bits << "SF#{out[:sf]}" if out[:sf]
  bits << "BW#{out[:bw]}" if out[:bw]
  bits << "sync=#{out[:sync_word]}" if out[:sync_word]
  bits << out[:payload].to_s[0, 40] if out[:payload]
  out[:summary] = bits.empty? ? line[0, 120] : bits.join(' ')
  out
end