Module: PWN::SDR::Decoder::ADSB
- Defined in:
- lib/pwn/sdr/decoder/adsb.rb
Overview
ADS-B 1090 MHz Mode-S long-frame decoder. The UAT band selector is retained for compatibility, but UAT demodulation is not implemented.
Prefer PWN::FFI::RTLSdr,AdalmPluto,HackRF at exactly 2 Msps and run a pure-Ruby Mode-S preamble correlator + 112-bit PPM slicer over magnitude samples. Missing I/Q raises; use .detect for energy only. Offline SBS-1 CSV → .parse_line.
Defined Under Namespace
Classes: DemodIQ
Constant Summary collapse
- SBS_FIELDS =
%i[ msg_type tx_type session_id aircraft_id icao24 flight_id date_gen time_gen date_log time_log callsign altitude_ft ground_speed_kt track_deg lat lon vertical_rate_fpm squawk alert emergency spi on_ground ].freeze
- PREAMBLE =
8 μs Mode-S preamble at 2 Msps → 16 samples: 1 0 1 0 0 0 0 1 0 1 0 0 0 0 0 0
[1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0].map(&:to_f).freeze
- SAMPLES_PER_US =
@ 2 Msps
2- MODE_S_CRC_POLY =
CRC-24 (Mode-S) generator 0x1FFF409 (poly over GF(2), 24-bit)
0x1FFF409
Class Method Summary collapse
-
.airborne_position(opts = {}) ⇒ Object
Global airborne CPR only; timestamps are seconds on the same clock.
- .ais_char(opts = {}) ⇒ Object
-
.authors ⇒ Object
- Author(s)
0day Inc.
-
.crc24(opts = {}) ⇒ Object
- Supported Method Parameters
crc = PWN::SDR::Decoder::ADSB.crc24(bits: Array<0|1>) CRC over all bits except the final 24 (which hold the parity).
-
.crc_ok?(opts = {}) ⇒ Boolean
- Supported Method Parameters
ok = PWN::SDR::Decoder::ADSB.crc_ok?(bits: Array<0|1>).
- .decode(opts = {}) ⇒ Object
-
.decode_modes(opts = {}) ⇒ Object
- Supported Method Parameters
h = PWN::SDR::Decoder::ADSB.decode_modes(bits: Array<0|1> of length 56 or 112).
-
.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.
- .modes_altitude(opts = {}) ⇒ Object
-
.parse_line(opts = {}) ⇒ Object
- Supported Method Parameters
PWN::SDR::Decoder::ADSB.parse_line(line: 'MSG,3,1,1,ABCDEF,...').
-
.surface_position(opts = {}) ⇒ Object
Local surface CPR: reference must be within 45 NM of the aircraft.
Class Method Details
.airborne_position(opts = {}) ⇒ Object
Global airborne CPR only; timestamps are seconds on the same clock. Surface CPR requires a reference and is deliberately not accepted.
309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 |
# File 'lib/pwn/sdr/decoder/adsb.rb', line 309 public_class_method def self.airborne_position(opts = {}) even = opts[:even] odd = opts[:odd] times = [opts[:even_time], opts[:odd_time]] return nil unless even.is_a?(Hash) && odd.is_a?(Hash) && times.all? { |t| t.is_a?(Numeric) && t.finite? } return nil if (times[0] - times[1]).abs > 10 return nil unless even[:icao24] == odd[:icao24] && even[:df] == odd[:df] return nil unless even[:cpr_format].eql?(0) && odd[:cpr_format] == 1 return nil unless [even, odd].all? do |f| ((9..18).cover?(f[:type_code]) || (20..22).cover?(f[:type_code])) && %i[cpr_lat cpr_lon].all? { |k| f[k].is_a?(Integer) && f[k].between?(0, 131_071) } end return nil unless (even[:type_code] < 19) == (odd[:type_code] < 19) yz = [even[:cpr_lat], odd[:cpr_lat]].map { |n| n / 131_072.0 } j = ((59 * yz[0]) - (60 * yz[1]) + 0.5).floor lat = [0, 1].map do |i| value = (360.0 / (60 - i)) * ((j % (60 - i)) + yz[i]) value >= 270 ? value - 360 : value end return nil unless lat.all? { |v| v.abs <= 90 } nl = lat.map { |v| cpr_nl(latitude: v) } return nil unless nl[0] == nl[1] latest = times[0] >= times[1] ? 0 : 1 xz = [even[:cpr_lon], odd[:cpr_lon]].map { |n| n / 131_072.0 } m = ((xz[0] * (nl[0] - 1)) - (xz[1] * nl[0]) + 0.5).floor ni = [nl[latest] - latest, 1].max lon = (360.0 / ni) * ((m % ni) + xz[latest]) { lat: lat[latest], lon: lon >= 180 ? lon - 360 : lon } end |
.ais_char(opts = {}) ⇒ Object
350 351 352 353 354 |
# File 'lib/pwn/sdr/decoder/adsb.rb', line 350 public_class_method def self.ais_char(opts = {}) code = opts[:code] table = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ##### ###############0123456789######' table[code] || ' ' end |
.authors ⇒ Object
- Author(s)
0day Inc. [email protected]
446 447 448 |
# File 'lib/pwn/sdr/decoder/adsb.rb', line 446 public_class_method def self. "AUTHOR(S):\n 0day Inc. <[email protected]>\n" end |
.crc24(opts = {}) ⇒ Object
- Supported Method Parameters
crc = PWN::SDR::Decoder::ADSB.crc24(bits: Array<0|1>) CRC over all bits except the final 24 (which hold the parity).
156 157 158 159 160 161 162 163 164 165 166 167 168 169 |
# File 'lib/pwn/sdr/decoder/adsb.rb', line 156 public_class_method def self.crc24(opts = {}) bits = opts[:bits] || [] return nil if bits.length < 32 # Mode-S CRC-24: left-shift register fed by every message bit # (including the 24 parity bits). A valid frame leaves residual 0. reg = 0 bits.each do |b| reg <<= 1 reg |= (b & 1) reg ^= MODE_S_CRC_POLY if reg.anybits?(0x1000000) end reg & 0xFFFFFF end |
.crc_ok?(opts = {}) ⇒ Boolean
- Supported Method Parameters
ok = PWN::SDR::Decoder::ADSB.crc_ok?(bits: Array<0|1>)
174 175 176 177 178 179 |
# File 'lib/pwn/sdr/decoder/adsb.rb', line 174 public_class_method def self.crc_ok?(opts = {}) bits = opts[:bits] || [] return false unless [56, 112].include?(bits.length) crc24(bits: bits).zero? end |
.decode(opts = {}) ⇒ Object
401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 |
# File 'lib/pwn/sdr/decoder/adsb.rb', line 401 public_class_method def self.decode(opts = {}) freq_obj = opts[:freq_obj] hz = PWN::SDR.hz_to_i(freq: freq_obj[:freq]) uat = hz.between?(977_000_000, 979_000_000) raise ArgumentError, 'ADSB UAT decoding is not implemented; use detect for energy only' if uat proto = 'ADSB-1090ES' rate = (opts[:sample_rate] || freq_obj[:iq_rate] || 2_000_000).to_i PWN::SDR::Decoder::Base.run_iq( **opts, fallback: :raise, freq_obj: freq_obj, protocol: proto, sample_rate: rate, source: opts[:source], file: opts[:file], demod: DemodIQ.new(rate: rate, reference: opts[:reference]), note: 'Mode-S 1 Mbit/s PPM at 2 Msps; DF17/18 CRC-validated frames only.', describe: proc { |b| { modulation: 'PPM', frame_len_us: 120, classification: b[:duration_ms] < 5 ? 'squitter' : 'interrogation-train' } } ) end |
.decode_modes(opts = {}) ⇒ Object
- Supported Method Parameters
h = PWN::SDR::Decoder::ADSB.decode_modes(bits: Array<0|1> of length 56 or 112)
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 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 |
# File 'lib/pwn/sdr/decoder/adsb.rb', line 184 public_class_method def self.decode_modes(opts = {}) bits = opts[:bits] || [] return nil unless bits.length == 112 && bits.all? { |b| [0, 1].include?(b) } && crc_ok?(bits: bits) return nil unless [17, 18].include?(DSP.bits_to_int(bits: bits[0, 5])) # DF (5) + CA (3) + ICAO (24) + ... df = PWN::SDR::Decoder::DSP.bits_to_int(bits: bits[0, 5]) icao = format('%06X', PWN::SDR::Decoder::DSP.bits_to_int(bits: bits[8, 24])) out = { protocol: 'ADSB', df: df, icao24: icao, bits: bits.length, raw_hex: bits.each_slice(4).map { |n| PWN::SDR::Decoder::DSP.bits_to_int(bits: n).to_s(16) }.join.upcase } # DF17/18 ME field (56 bits starting at bit 32) if [17, 18].include?(df) && bits.length >= 88 tc = PWN::SDR::Decoder::DSP.bits_to_int(bits: bits[32, 5]) out[:type_code] = tc if tc.between?(1, 4) # aircraft identification — 8× 6-bit AIS chars cs = bits[40, 48].each_slice(6).map { |ch| ais_char(code: PWN::SDR::Decoder::DSP.bits_to_int(bits: ch)) }.join.strip out[:callsign] = cs elsif tc.between?(5, 8) out.merge!(surface_movement(bits: bits)) out[:cpr_format] = bits[53] out[:cpr_lat] = DSP.bits_to_int(bits: bits[54, 17]) out[:cpr_lon] = DSP.bits_to_int(bits: bits[71, 17]) elsif tc.between?(9, 18) || tc.between?(20, 22) out[:altitude_ft] = modes_altitude(bits12: bits[40, 12]) if tc.between?(9, 18) out[:gnss_height_m] = DSP.bits_to_int(bits: bits[40, 12]) if tc.between?(20, 22) out[:cpr_format] = bits[53] out[:cpr_lat] = DSP.bits_to_int(bits: bits[54, 17]) out[:cpr_lon] = DSP.bits_to_int(bits: bits[71, 17]) elsif tc == 19 out.merge!(airborne_velocity(bits: bits)) end end bits_s = [] bits_s << "ICAO=#{out[:icao24]}" bits_s << "DF=#{df}" bits_s << "CS=#{out[:callsign]}" if out[:callsign] bits_s << "ALT=#{out[:altitude_ft]}ft" if out[:altitude_ft] bits_s << "TC=#{out[:type_code]}" if out[:type_code] out[:summary] = "ADSB #{bits_s.join(' ')}" out 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 ADSB payloads.
- Supported Method Parameters
ADSB.detect(freq_obj: Hash, threshold: 8.0, on_frame: Proc)
393 394 395 396 397 398 399 |
# File 'lib/pwn/sdr/decoder/adsb.rb', line 393 public_class_method def self.detect(opts = {}) Base.run_detector(opts.merge( protocol: 'ADSB', 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
452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 |
# File 'lib/pwn/sdr/decoder/adsb.rb', line 452 public_class_method def self.help puts "USAGE: # Detect energy only (not protocol payloads); accepts Base runner controls. #{self}.detect(freq_obj: {}, threshold: 8.0, on_frame: nil) # Run crc24 and return its result #{self}.crc24( bits: 'optional - bits value consumed by #crc24 (defaults to [])' ) # Run crc ok and return its result #{self}.crc_ok?( bits: 'optional - bits value consumed by #crc_ok? (defaults to [])' ) # Run decode modes and return its result #{self}.decode_modes( bits: 'optional - bits value consumed by #decode_modes (defaults to [])' ) # Resolve local surface CPR using a reference within 45 NM of the aircraft. #{self}.surface_position(frame: {}, reference: [52.0, 4.0]) # Resolve a same-aircraft airborne even/odd CPR pair within ten seconds. #{self}.airborne_position( even: 'required - decoded even airborne CPR frame Hash', odd: 'required - decoded odd airborne CPR frame Hash', even_time: 'required - even frame capture time in seconds on the shared clock', odd_time: 'required - odd frame capture time in seconds on the shared clock' ) # Run ais char and return its result #{self}.ais_char( code: 'optional - code value consumed by #ais_char' ) # Run modes altitude and return its result #{self}.modes_altitude( bits12: 'optional - bits12 value consumed by #modes_altitude' ) # Run decode and return its result #{self}.decode( reference: 'optional - [latitude, longitude] within 45 NM for local surface CPR', freq_obj: 'required - freq_obj 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 chunks (default 8)', log_file: 'optional - JSONL path or false to disable logging', sample_rate: 'optional - sample rate value consumed by #decode', source: 'optional - source value consumed by #decode', file: 'optional - filesystem path' ) # Run parse line and return its result #{self}.parse_line( line: 'optional - line value consumed by #parse_line' ) # Print the AUTHOR(S) string for this module. #{self}.authors " constants.sort end |
.modes_altitude(opts = {}) ⇒ Object
356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 |
# File 'lib/pwn/sdr/decoder/adsb.rb', line 356 public_class_method def self.modes_altitude(opts = {}) bits12 = opts[:bits12] return nil unless bits12.is_a?(Array) && bits12.length == 12 && bits12.all? { |bit| [0, 1].include?(bit) } if bits12[7] == 1 n = PWN::SDR::Decoder::DSP.bits_to_int(bits: bits12[0, 7] + bits12[8, 4]) return (n * 25) - 1000 end # Q=0: D2,D4,A1,A2,A4,B1,B2,B4 Gray-code 500-ft steps; # C1,C2,C4 encode the reflected five-state 100-ft sequence. coarse = [9, 11, 1, 3, 5, 6, 8, 10].map { |i| bits12[i] } fine = [0, 2, 4].map { |i| bits12[i] } n500, n100 = [coarse, fine].map do |gray| binary = 0 gray.inject(0) do |value, bit| binary ^= bit (value << 1) | binary end end return nil if [0, 5, 6].include?(n100) n100 = 5 if n100 == 7 n100 = 6 - n100 if n500.odd? (n500 * 500) + (n100 * 100) - 1300 end |
.parse_line(opts = {}) ⇒ Object
- Supported Method Parameters
PWN::SDR::Decoder::ADSB.parse_line(line: 'MSG,3,1,1,ABCDEF,...')
426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 |
# File 'lib/pwn/sdr/decoder/adsb.rb', line 426 public_class_method def self.parse_line(opts = {}) line = opts[:line].to_s return nil unless line.start_with?('MSG,') f = line.split(',', -1) out = { protocol: 'ADSB' } SBS_FIELDS.each_with_index { |k, i| out[k] = f[i] unless f[i].to_s.empty? } bits = [] bits << "ICAO=#{out[:icao24]}" if out[:icao24] bits << "CS=#{out[:callsign].to_s.strip}" if out[:callsign] bits << "ALT=#{out[:altitude_ft]}ft" if out[:altitude_ft] bits << "POS=#{out[:lat]},#{out[:lon]}" if out[:lat] && out[:lon] bits << "GS=#{out[:ground_speed_kt]}kt" if out[:ground_speed_kt] bits << "SQK=#{out[:squawk]}" if out[:squawk] out[:summary] = "ADSB #{bits.join(' ')}".strip out end |
.surface_position(opts = {}) ⇒ Object
Local surface CPR: reference must be within 45 NM of the aircraft. This ambiguity constraint is caller-owned, not measurable from one message. No reference means no invented position.
252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 |
# File 'lib/pwn/sdr/decoder/adsb.rb', line 252 public_class_method def self.surface_position(opts = {}) frame = opts[:frame] reference = opts[:reference] return nil unless reference.is_a?(Array) && reference.length == 2 && reference.all? { |v| v.is_a?(Numeric) && v.finite? } return nil unless reference[0].between?(-90, 90) && reference[1].between?(-180, 180) return nil unless frame.is_a?(Hash) && (5..8).cover?(frame[:type_code]) && [0, 1].include?(frame[:cpr_format]) return nil unless %i[cpr_lat cpr_lon].all? { |key| frame[key].is_a?(Integer) && frame[key].between?(0, 131_071) } odd = frame[:cpr_format] yz = frame[:cpr_lat] / 131_072.0 xz = frame[:cpr_lon] / 131_072.0 dlat = 90.0 / (60 - odd) j = (reference[0] / dlat).floor + (0.5 + ((reference[0] % dlat) / dlat) - yz).floor lat = dlat * (j + yz) return nil unless lat.between?(-90, 90) dlon = 90.0 / [cpr_nl(latitude: lat) - odd, 1].max m = (reference[1] / dlon).floor + (0.5 + ((reference[1] % dlon) / dlon) - xz).floor lon = (((dlon * (m + xz)) + 180) % 360) - 180 { lat: lat, lon: lon } end |