Module: PWN::SDR::Decoder::RDS
- Defined in:
- lib/pwn/sdr/decoder/rds.rb
Overview
RDS Decoder Module for FM Radio Signals.
Two entry points:
.sample — non-interactive structured Hash (agents / cron / tools)
.decode — realtime JSONL/callback stream with optional ENTER stop
Both share the same GQRX RDS protocol path (U RDS, p RDS_PI / PS_NAME / RADIOTEXT). .sample is the canonical mid-layer API that Extrospection and any other automation should call.
Constant Summary collapse
- DEFAULT_SETTLE_SECS =
8.0- DEFAULT_INTERVAL =
0.75- CALLSIGN_RX =
/\A[A-Z]{1,2}[A-Z0-9]{2,4}\z/- CALLSIGN_RT_RX =
/\A([A-Z]{1,2}[A-Z0-9]{2,4})\b/
Class Method Summary collapse
-
.authors ⇒ Object
- Author(s)
0day Inc.
- .decode(opts = {}) ⇒ Object
-
.detect(opts = {}) ⇒ Object
Realtime options forwarded to Base: on_frame (Hash callback), output (writable IO), interactive (default true), duration (seconds), stop (callable), queue_size (bounded chunks), log_file (path or false).
-
.help ⇒ Object
Display Usage for this Module.
-
.sample(opts = {}) ⇒ Object
- Supported Method Parameters
rds_hash = PWN::SDR::Decoder::RDS.sample( gqrx_sock: 'required unless freq_obj - TCPSocket from GQRX.connect', freq_obj: 'required unless gqrx_sock - Hash from GQRX.init_freq', settle_secs: 'optional - seconds to sample (default 8, max 30)', interval: 'optional - poll interval seconds (default 0.75)', leave_enabled: 'optional - leave RDS decoder ON after sample (default false)' ).
Class Method Details
.authors ⇒ Object
- Author(s)
0day Inc. [email protected]
163 164 165 166 167 |
# File 'lib/pwn/sdr/decoder/rds.rb', line 163 public_class_method def self. "AUTHOR(S): 0day Inc. <[email protected]> " end |
.decode(opts = {}) ⇒ Object
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 |
# File 'lib/pwn/sdr/decoder/rds.rb', line 120 public_class_method def self.decode(opts = {}) return decode_mpx(opts) if opts.fetch(:backend, :gqrx).to_sym == :redsea raise ArgumentError, 'backend must be :gqrx or :redsea' unless opts.fetch(:backend, :gqrx).to_sym == :gqrx freq_obj = opts[:freq_obj] raise ArgumentError, 'freq_obj: required' unless freq_obj.is_a?(Hash) gqrx_sock = freq_obj[:gqrx_sock] raise ArgumentError, 'freq_obj[:gqrx_sock] required' unless gqrx_sock raise 'RDS not supported by this radio backend' unless enable_rds!(sock: gqrx_sock) interval = [(opts[:interval] || DEFAULT_INTERVAL).to_f, 0.01].max first = true reader = proc do sleep interval unless first first = false poll_once(sock: gqrx_sock) end last_resp = nil Base.send(:run_stream, opts.merge( protocol: 'RDS', reader: reader, log_obj: Base.send(:strip_freq_obj, freq_obj: freq_obj) )) do |snap, emit| next unless snap pi = snap[:pi].to_s.upcase next unless pi.match?(/\A[0-9A-F]{4}\z/) && pi != '0000' response = { rds_pi: pi, rds_ps_name: snap[:ps].to_s, rds_radiotext: snap[:rt].to_s } next if response == last_resp last_resp = response.dup emit.call(response.merge( protocol: 'RDS', event: 'station', capability: 'backend-rds', summary: "Program ID: #{pi} | Station Name: #{response[:rds_ps_name]} | Radio Txt: #{response[:rds_radiotext]}" )) end ensure disable_rds!(sock: gqrx_sock) if gqrx_sock end |
.detect(opts = {}) ⇒ Object
Realtime options forwarded to Base: on_frame (Hash callback), output (writable IO), interactive (default true), duration (seconds), stop (callable), queue_size (bounded chunks), log_file (path or false). Energy detection only; does not identify or decode RDS payloads.
- Supported Method Parameters
RDS.detect(freq_obj: Hash, threshold: 8.0, on_frame: Proc)
112 113 114 115 116 117 118 |
# File 'lib/pwn/sdr/decoder/rds.rb', line 112 public_class_method def self.detect(opts = {}) Base.run_detector(opts.merge( protocol: 'RDS', note: 'Energy detection only; no protocol payload decoding.', describe: proc { |_burst| { event: 'detection', capability: 'energy-detection', decoded: false } } )) end |
.help ⇒ Object
Display Usage for this Module
171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 |
# File 'lib/pwn/sdr/decoder/rds.rb', line 171 public_class_method def self.help puts "USAGE: # Native PHY: backend: :redsea runs the actual C++/liquid-dsp decoder. # Requires an explicit FM-MPX file or raw mono s16le MPX IO (not speaker audio). # No implicit RF. Emits only complete four-block syndrome-checked groups. #{self}.decode(freq_obj: {}, backend: :redsea, executable: 'redsea', file: 'mpx.flac', interactive: false) # Default backend: :gqrx retains historical metadata snapshots, not native PHY. # Detect energy only (not protocol payloads); accepts Base runner controls. #{self}.detect(freq_obj: {}, threshold: 8.0, on_frame: nil) # Run sample and return its result #{self}.sample( gqrx_sock: 'required - required unless freq_obj - TCPSocket from GQRX.connect', freq_obj: 'required - required unless gqrx_sock - Hash from GQRX.init_freq', settle_secs: 'optional - seconds to sample (default 8, max 30)', interval: 'optional - poll interval seconds (default 0.75)', leave_enabled: 'optional - leave RDS decoder ON after sample (default false)', pi: 'required - ps_name:, radiotext:, station:', samples: 'required - Integer, settle_secs: Float', error: 'required - String? # present when RDS backend is unavailable' ) # Realtime RDS snapshots via Base (ENTER or stop/duration ends the stream) #{self}.decode( freq_obj: 'required - Hash returned from PWN::SDR::GQRX.init_freq', on_frame: 'optional - callback receiving each emitted Hash', output: 'optional - writable IO (default stdout)', interactive: 'optional - false disables ENTER input', duration: 'optional - finite seconds to run', stop: 'optional - callable returning true to stop', queue_size: 'optional - bounded pending snapshots (default 8)', log_file: 'optional - JSONL path or false to disable logging', interval: 'optional - polling interval seconds (default 0.75)' # backend: :redsea; executable: native binary path; file: containerized MPX # source: raw MPX IO; sample_rate: 128000..384000 (default 192000) ) # Print the AUTHOR(S) string for this module. #{self}.authors " constants.sort end |
.sample(opts = {}) ⇒ Object
- Supported Method Parameters
rds_hash = PWN::SDR::Decoder::RDS.sample( gqrx_sock: 'required unless freq_obj - TCPSocket from GQRX.connect', freq_obj: 'required unless gqrx_sock - Hash from GQRX.init_freq', settle_secs: 'optional - seconds to sample (default 8, max 30)', interval: 'optional - poll interval seconds (default 0.75)', leave_enabled: 'optional - leave RDS decoder ON after sample (default false)' )
- Returns
{ pi:, ps_name:, radiotext:, station:, samples: Integer, settle_secs: Float, error: String? # present when RDS backend is unavailable }
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 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 89 90 91 92 93 94 95 96 |
# File 'lib/pwn/sdr/decoder/rds.rb', line 40 public_class_method def self.sample(opts = {}) sock = resolve_sock(opts) raise ArgumentError, 'gqrx_sock: or freq_obj: with :gqrx_sock required' unless sock settle = (opts[:settle_secs] || DEFAULT_SETTLE_SECS).to_f.clamp(0.5, 30.0) interval = [(opts[:interval] || DEFAULT_INTERVAL).to_f, 0.1].max leave_on = opts[:leave_enabled] ? true : false samples = [] unless enable_rds!(sock: sock) return { pi: nil, ps_name: nil, radiotext: nil, station: nil, samples: 0, settle_secs: settle, error: 'RDS not supported by this radio backend' } end deadline = Time.now + settle while Time.now < deadline snap = poll_once(sock: sock) samples << snap unless snap[:pi].empty? && snap[:ps].empty? && snap[:rt].empty? # Early exit once we have a non-zero PI and a non-trivial RT — # give one more interval for RadioText to finish filling. pi = snap[:pi] rt = snap[:rt] if pi =~ /\A[0-9A-F]{4}\z/ && pi != '0000' && rt.length >= 8 sleep interval snap2 = poll_once(sock: sock) samples << snap2 break if snap2[:rt].length >= rt.length end sleep interval end disable_rds!(sock: sock) unless leave_on aggregate(samples: samples, settle_secs: settle) rescue ArgumentError raise rescue StandardError => e disable_rds!(sock: sock) if sock && !leave_on { pi: nil, ps_name: nil, radiotext: nil, station: nil, samples: samples&.length.to_i, settle_secs: settle, error: "#{e.class}: #{e.}" } end |