Module: PWN::SDR::Decoder::Flex

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

Overview

Pure-Ruby FLEX™ pager decoder.

FLEX is Motorola's synchronous paging protocol running at 1600 or 3200 symbols/s in 2- or 4-level FSK. GQRX's NBFM discriminator audio (48 kHz UDP tap) is fed into a per-sample PLL symbol clock, 4-level quantised, and driven through the Sync-1 → FIW → Sync-2 → 11-block state machine. All four interleaved phases (A/B/C/D) are de-interleaved into 88 × 32-bit BCH(31,21)+parity codewords, error corrected, and walked (BIW → address → vector → message words) to recover capcode + alphanumeric / numeric / binary payloads.

Algorithm is a clean-room Ruby port of the reference behavior in multimon-ng demod_flex.c (GPLv2), verified bit-exact against a live 929.625 MHz 3200/4 capture: sync 0xDEA0, FIW cycle 10 frame 70, 88/88 BCH-clean words per phase, capcodes 4294949118 / 002064207 / 002064227 all matching multimon-ng ground truth.

No multimon-ng, no sox — 100 % Ruby.

Defined Under Namespace

Classes: Demod

Constant Summary collapse

SYNC_MARKER =
0xA6C6AAAA
SLICE_TH =
0.667
LOCKED_RATE =
0.045
UNLOCK_RATE =
0.050
A_TABLE =

Sync-1 A-word (canonical, sym<2→1 polarity) → [symbol_rate, levels]

{
  0x870C => [1600, 2],
  0xB068 => [1600, 4],
  0x7B18 => [3200, 2],
  0xDEA0 => [3200, 4],
  0x4C7C => [3200, 4]
}.freeze
NUM_TABLE =
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '/', 'U', ' ', '-', ']', '['].freeze
TYPE_NAME =
%w[SEC SHI TON NUM SFN ALN BIN NNM].freeze
BCH_GEN =

BCH(31,21) syndrome/correction for FLEX word ordering: bit 0..20 data (x^30..x^10), bit 21..30 check (x^9..x^0), bit 31 parity. Verified: FIW 0xF27C46AE → syndrome 0.

0x769
BCH_TAB1 =
begin
  t = {}
  31.times { |i| t[bch_syn(word: 1 << i)] = 1 << i }
  t.freeze
end
BCH_TAB2 =
begin
  t = {}
  31.times do |i|
    ((i + 1)..30).each { |j| t[bch_syn(word: (1 << i) | (1 << j))] ||= (1 << i) | (1 << j) }
  end
  t.freeze
end

Class Method Summary collapse

Class Method Details

.alpha_decode(opts = {}) ⇒ Object

Supported Method Parameters

str, frag = PWN::SDR::Decoder::Flex.alpha_decode(words:, mw1:, mw2:)



377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
# File 'lib/pwn/sdr/decoder/flex.rb', line 377

public_class_method def self.alpha_decode(opts = {})
  words = opts[:words] || []
  mw1   = opts[:mw1].to_i
  mw2   = opts[:mw2].to_i
  return [nil, nil] unless mw1.between?(1, 87) && mw2.between?(mw1, 87)

  hdr  = words[mw1].to_i
  frag = (hdr >> 11) & 0x03
  cont = (hdr >> 10) & 0x01
  flag = if cont == 1 then 'F'
         elsif frag == 3 then 'K'
         else 'C'
         end
  out = +''
  ((mw1 + 1)..mw2).each do |i|
    dw = words[i].to_i
    [0, 7, 14].each do |sh|
      ch = (dw >> sh) & 0x7F
      out << ch.chr if ch != 0x03 && ch.between?(0x20, 0x7E)
    end
  end
  [out, flag]
end

.authorsObject

Author(s)

0day Inc. support@0dayinc.com



470
471
472
# File 'lib/pwn/sdr/decoder/flex.rb', line 470

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

.bch_fix(opts = {}) ⇒ Object

Supported Method Parameters

fixed, nerr = PWN::SDR::Decoder::Flex.bch_fix(word: Integer) → nerr = 0/1/2 (corrected) or -1 (uncorrectable)



296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
# File 'lib/pwn/sdr/decoder/flex.rb', line 296

public_class_method def self.bch_fix(opts = {})
  w = opts[:word].to_i & 0xFFFFFFFF
  s = bch_syn(word: w)
  return [w, 0] if s.zero? && even_parity?(word: w)
  return [w ^ 0x80000000, 1] if s.zero?

  if (m = BCH_TAB1[s])
    return [w ^ m, 1] if even_parity?(word: w ^ m)

    return [w ^ m ^ 0x80000000, 2]
  end
  m = BCH_TAB2[s]
  return [w ^ m, 2] if m && even_parity?(word: w ^ m)

  [w, -1]
end

.bch_syn(opts = {}) ⇒ Object

Supported Method Parameters

syn = PWN::SDR::Decoder::Flex.bch_syn(word: Integer)



270
271
272
273
274
275
276
277
# File 'lib/pwn/sdr/decoder/flex.rb', line 270

public_class_method def self.bch_syn(opts = {})
  word = opts[:word].to_i
  # reverse bits 0..30 so bit0 → x^30 (FLEX on-air ordering)
  r = 0
  31.times { |i| r |= ((word >> i) & 1) << (30 - i) }
  30.downto(10) { |i| r ^= BCH_GEN << (i - 10) if (r >> i).odd? }
  r & 0x3FF
end

.decode(opts = {}) ⇒ Object

Supported Method Parameters

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



442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
# File 'lib/pwn/sdr/decoder/flex.rb', line 442

public_class_method def self.decode(opts = {})
  freq_obj = opts[:freq_obj]
  # Prefer true-air I/Q (FM-demod → native audio demod) when the
  # operator asks for a source/file or sets freq_obj[:iq_source].
  # Otherwise keep the GQRX 48 kHz UDP audio path (run_native).
  want_iq = opts[:source] || opts[:file] || freq_obj[:iq_source] || freq_obj[:iq_file]
  if want_iq
    PWN::SDR::Decoder::Base.run_iq(
      freq_obj: freq_obj,
      protocol: 'FLEX',
      demod: Demod.new,
      sample_rate: (opts[:sample_rate] || freq_obj[:iq_rate] || 240_000).to_i,
      source: opts[:source],
      file: opts[:file],
      fm_demod: true,
      note: 'FLEX true-air: FM-demod I/Q → PLL symbol clock → 4-level slicer → full 4-phase de-interleave.'
    )
  else
    PWN::SDR::Decoder::Base.run_native(
      freq_obj: freq_obj,
      protocol: 'FLEX',
      demod: Demod.new
    )
  end
end

.emit_phase(opts = {}) ⇒ Object

Supported Method Parameters

PWN::SDR::Decoder::Flex.emit_phase( words:, phase:, cycle:, frame:, mode:, sync_cw: ) { |msg| ... }



318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
# File 'lib/pwn/sdr/decoder/flex.rb', line 318

public_class_method def self.emit_phase(opts = {})
  raw   = opts[:words] || []
  phase = opts[:phase]
  cycle = opts[:cycle]
  frame = opts[:frame]
  mode  = opts[:mode] || [1600, 2]
  # BCH-fix every word; abort phase on uncorrectable BIW.
  fixed = raw.map { |w| bch_fix(word: w) }
  words = fixed.map { |w, _| w & 0x1FFFFF }
  errs  = fixed.count { |_, e| e.negative? }
  biw   = words[0]
  return if fixed[0][1].negative?

  a_start = ((biw >> 8) & 0x03) + 1
  v_start = (biw >> 10) & 0x3F
  return unless a_start < v_start && v_start < 88

  (a_start...v_start).each do |i|
    aw = words[i]
    next if aw.nil? || aw.zero? || aw == 0x1FFFFF

    long_addr = aw < 0x008001 || aw > 0x1E0000
    capcode   = (aw - 0x8000) & 0xFFFFFFFF
    j    = v_start + (i - a_start)
    viw  = words[j] || 0
    type = (viw >> 4) & 0x7
    mw1  = (viw >> 7) & 0x7F
    len  = (viw >> 14) & 0x7F
    mw2  = mw1 + len - 1
    body, frag =
      case type
      when 0, 5 then alpha_decode(words: words, mw1: mw1, mw2: mw2)
      when 3, 4, 7 then [numeric_decode(words: words, mw1: mw1, mw2: mw2), nil]
      when 6 then [hex_decode(words: words[mw1..mw2]), nil]
      else [nil, nil]
      end
    out = {
      protocol: 'FLEX',
      mode: "#{mode[0]}/#{mode[1]}",
      phase: phase,
      cycle: cycle,
      frame: frame,
      capcode: capcode.to_s.rjust(9, '0'),
      long_address: long_addr,
      type: TYPE_NAME[type],
      frag: frag,
      type_payload: body,
      bch_errors: errs
    }.compact
    cf = "#{cycle.to_s.rjust(2, '0')}.#{frame.to_s.rjust(3, '0')}"
    out[:summary] =
      "FLEX|#{mode[0]}/#{mode[1]}|#{phase}|#{cf}|#{out[:capcode]}|#{out[:type]}|#{body}"[0, 200]
    yield out if block_given?
  end
end

.even_parity?(opts = {}) ⇒ Boolean

Supported Method Parameters

ok = PWN::SDR::Decoder::Flex.even_parity?(word: Integer)

Returns:

  • (Boolean)


258
259
260
# File 'lib/pwn/sdr/decoder/flex.rb', line 258

public_class_method def self.even_parity?(opts = {})
  popcnt(val: opts[:word].to_i & 0xFFFFFFFF).even?
end

.helpObject

Display Usage for this Module



476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
# File 'lib/pwn/sdr/decoder/flex.rb', line 476

public_class_method def self.help
  puts "USAGE (true-air I/Q + GQRX-audio native paths, no external binaries):
    #{self}.decode(
      freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
    )

    demod = #{self}::Demod.new(rate: 48_000)
    demod.feed(samples) { |msg| puts msg[:summary] }

    #{self}.bch_fix(word: Integer)        # → [fixed, nerr]
    #{self}.alpha_decode(words:, mw1:, mw2:)
    #{self}.numeric_decode(words:, mw1:, mw2:)

    NOTE: Set GQRX to Narrow FM (~15-20 kHz). All four FLEX modes
          (1600/2, 1600/4, 3200/2, 3200/4 sym-rate/levels) are
          fully decoded across every active phase (A/B/C/D).

    #{self}.authors
  "
end

.hex_decode(opts = {}) ⇒ Object

Supported Method Parameters

str = PWN::SDR::Decoder::Flex.hex_decode(words: [Integer, ...])



432
433
434
435
# File 'lib/pwn/sdr/decoder/flex.rb', line 432

public_class_method def self.hex_decode(opts = {})
  words = opts[:words] || []
  words.compact.map { |w| format('%06X', w & 0x1FFFFF) }.join(' ')
end

.numeric_decode(opts = {}) ⇒ Object

Supported Method Parameters

str = PWN::SDR::Decoder::Flex.numeric_decode(words:, mw1:, mw2:)



404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
# File 'lib/pwn/sdr/decoder/flex.rb', line 404

public_class_method def self.numeric_decode(opts = {})
  words = opts[:words] || []
  mw1   = opts[:mw1].to_i
  mw2   = opts[:mw2].to_i
  return nil unless mw1.between?(1, 87) && mw2.between?(mw1, 87)

  out = +''
  # Numeric payload: 4-bit digits packed across 21-bit data words,
  # first word bits [20:14] are header (skip 2 digits worth).
  bits = []
  (mw1..mw2).each do |i|
    dw = words[i].to_i
    21.times { |b| bits << ((dw >> b) & 1) }
  end
  # skip 10-bit header (checksum+len markers)
  bits.shift(10)
  bits.each_slice(4) do |nyb|
    break if nyb.length < 4

    v = nyb[0] | (nyb[1] << 1) | (nyb[2] << 2) | (nyb[3] << 3)
    out << NUM_TABLE[v]
  end
  out.strip
end

.popcnt(opts = {}) ⇒ Object

Supported Method Parameters

n = PWN::SDR::Decoder::Flex.popcnt(val: Integer)



245
246
247
248
249
250
251
252
253
# File 'lib/pwn/sdr/decoder/flex.rb', line 245

public_class_method def self.popcnt(opts = {})
  val = opts[:val].to_i
  c   = 0
  while val.positive?
    c += val & 1
    val >>= 1
  end
  c
end

.sync_check(opts = {}) ⇒ Object

Supported Method Parameters

code, polarity = PWN::SDR::Decoder::Flex.sync_check(buf: Integer) → [16-bit A-word, 0|1] or nil



231
232
233
234
235
236
237
238
239
240
# File 'lib/pwn/sdr/decoder/flex.rb', line 231

public_class_method def self.sync_check(opts = {})
  buf = opts[:buf].to_i
  [[buf, 0], [~buf & 0xFFFFFFFFFFFFFFFF, 1]].each do |b, pol|
    marker = (b >> 16) & 0xFFFFFFFF
    ch     = (b >> 48) & 0xFFFF
    cl     = (~b) & 0xFFFF
    return [ch, pol] if popcnt(val: marker ^ SYNC_MARKER) < 4 && popcnt(val: ch ^ cl) < 4
  end
  nil
end