Class: PWN::SDR::Decoder::Tempest::Demod

Inherits:
Object
  • Object
show all
Defined in:
lib/pwn/sdr/decoder/tempest.rb

Overview

Streaming I/Q demodulator for Base.run_iq.

Instance Method Summary collapse

Constructor Details

#initialize(opts = {}) ⇒ Demod

Returns a new instance of Demod.

Raises:

  • (ArgumentError)


39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/pwn/sdr/decoder/tempest.rb', line 39

def initialize(opts = {})
  timing = PWN::SDR::Decoder::Tempest.resolve_timing(opts)
  @h_active = timing[:h_active]
  @h_total  = timing[:h_total]
  @v_active = timing[:v_active]
  @v_total  = timing[:v_total]
  @refresh  = timing[:refresh]
  unless [@h_active, @h_total, @v_active, @v_total].all?(&:positive?) &&
         @h_active <= @h_total && @v_active <= @v_total && @refresh.finite? && @refresh.positive?
    raise ArgumentError, 'invalid raster timing: active dimensions must fit positive totals and refresh'
  end

  @pixel_hz = (@h_total * @v_total * @refresh).to_f
  raise ArgumentError, 'pixel rate must be finite' unless @pixel_hz.finite?

  stamp = Time.now.strftime('%Y%m%d_%H%M%S')
  @pgm_path = opts[:out_path] || "/tmp/tempest_#{stamp}.pgm"
  @frames_want = opts[:continuous] ? nil : [opts[:frames].to_i, 1].max
  @mag_buf = []
  @src_rate = nil
  @phase = 0.0
  @pending_i = nil
  @written = 0
end

Instance Method Details

#feed_iq(iq_samples, rate:, &emit) ⇒ Object

Raises:

  • (ArgumentError)


64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/pwn/sdr/decoder/tempest.rb', line 64

def feed_iq(iq_samples, rate:, &emit)
  raise ArgumentError, 'sample rate must be finite and positive' unless rate.to_f.finite? && rate.to_f.positive?
  raise ArgumentError, 'sample rate changed during raster' if @src_rate && (@src_rate - rate.to_f).abs > 1e-9

  @src_rate ||= rate.to_f
  iq_samples.each do |value|
    break if @frames_want && @written >= @frames_want

    if @pending_i.nil?
      @pending_i = value
      next
    end
    magnitude = Math.hypot(@pending_i, value)
    @pending_i = nil
    # Stateful sample-and-hold: transport chunk boundaries never reset
    # pixel phase. Retain at most one raster, even during upsampling.
    @phase += @pixel_hz
    while @phase >= @src_rate
      @phase -= @src_rate
      @mag_buf << magnitude
      drain(emit: emit) if @mag_buf.length == @h_total * @v_total
      break if @frames_want && @written >= @frames_want
    end
  end
end

#finishObject



90
91
92
93
# File 'lib/pwn/sdr/decoder/tempest.rb', line 90

def finish
  drain(force: true)
  { path: @pgm_path, width: @h_active, height: @v_active, frames: @written }
end