Formatting

This commit is contained in:
2025-04-22 22:23:34 -04:00
parent 95e4b8abf3
commit fdb53f0b7f
9 changed files with 170 additions and 168 deletions

489
adsb/adsb_lib/src/adsb.rs Normal file
View File

@@ -0,0 +1,489 @@
use crate::hex_to_bytes;
use std::fmt::Display;
use std::io::{Error, ErrorKind, Result};
#[derive(Debug)]
pub struct ADSBFrame {
pub raw_frame: String,
/// Downlink format (DF, 5 bits)
pub downlink_format: u8,
/// Transponder capability (CA, 3 bits)
pub capability: Capability,
/// Unique aircraft number (ICAO, 24 bits)
pub icao: String,
/// Message (ME, 56 bits)
pub message: ADSBMessage,
/// Parity/Interrogator ID/Checksum (PI, 24 bits)
pub parity: u32,
}
impl ADSBFrame {
/// Parse exactly 14 bytes (112 bits) of raw ADS-B ES data into its fields
///
/// [ DF:5 ][ CA:3 ][ ICAO:24 ][ ME:56 ][ PI:24 ]
pub fn decode(frame: &[u8]) -> Result<ADSBFrame> {
if frame.len() != 14 {
return Err(Error::new(
ErrorKind::InvalidInput,
format!("expected 14 bytes, received {}", frame.len()),
));
}
let mut raw_frame = "".to_string();
for byte in frame {
raw_frame.push_str(&format!("{:02x}", byte).to_uppercase());
}
// Decode the downlink format by discarding the lower 3 bits
let downlink_format = &frame[0] >> 3;
if downlink_format != 17 {
return Err(Error::new(
ErrorKind::Unsupported,
format!(
"downlink format {} is not currently supported",
downlink_format
),
));
}
// Decode the capability by masking off everything but the lower 3 bits
let capability_value = &frame[0] & 0b0000_0111;
let capability = Capability::try_from(capability_value)?;
let icao = Self::decode_icao(&frame[1..=3])?;
let message = ADSBMessage::decode(&frame[4..=10])?;
let parity = Self::decode_parity(&frame[11..])?;
Ok(Self {
raw_frame,
downlink_format,
capability,
icao,
message,
parity,
})
}
pub fn encode(&self) -> Result<Vec<u8>> {
Ok(hex_to_bytes(&self.raw_frame)?)
}
fn decode_icao(data: &[u8]) -> Result<String> {
if data.len() != 3 {
return Err(Error::new(
ErrorKind::InvalidInput,
format!("ICAO must be 3 bytes, received {}", data.len()),
));
}
let s = data
.iter()
.map(|b| format!("{:02X}", b))
.collect::<String>();
Ok(s)
}
fn decode_parity(data: &[u8]) -> Result<u32> {
if data.len() != 3 {
return Err(Error::new(
ErrorKind::InvalidInput,
format!("parity must be 3 bytes, received {}", data.len()),
));
}
let p = ((data[0] as u32) << 16) | ((data[1] as u32) << 8) | (data[2] as u32);
Ok(p)
}
}
impl Display for ADSBFrame {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Frame: {}\
\nDF: {}\
\nCA: {:?}\
\nICAO: {}\
\nME: {:?}\
\nPI: {}",
self.raw_frame, self.downlink_format, &self.capability, self.icao, &self.message, self.parity
)
}
}
/// Transponder Capability (CA) codes from the ADSB spec
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Capability {
/// 0: Level 1 transponder
Level1,
/// 13: Reserved
Reserved(u8),
/// 4: Level 2+ transponder, onground (can set CA=7)
Level2OnGround,
/// 5: Level 2+ transponder, airborne (can set CA=7)
Level2Airborne,
/// 6: Level 2+ transponder, either onground or airborne (can set CA=7)
Level2Either,
/// 7: Downlink Request = 0, or Flight Status = 2,3,4,5
DownlinkRequestOrFlightStatus,
}
impl TryFrom<u8> for Capability {
type Error = Error;
fn try_from(value: u8) -> Result<Self> {
let capability = match value {
0 => Capability::Level1,
1..=3 => Capability::Reserved(value),
4 => Capability::Level2OnGround,
5 => Capability::Level2Airborne,
6 => Capability::Level2Either,
7 => Capability::DownlinkRequestOrFlightStatus,
_ => {
return Err(Error::new(
ErrorKind::InvalidData,
format!("invalid CA value: {}", value),
))
}
};
Ok(capability)
}
}
// fn get_bits(data: &[u8], from: usize, len: usize) -> u32 {
// let mut val = 0;
// for bit in 0..len {
// let idx = from + bit;
// let byte = data[idx / 8];
// let shift = 7 - (idx % 8);
// let bit_val = ((byte >> shift) & 0x01) as u32;
// val = (val << 1) | bit_val;
// }
// val
// }
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ADSBMessage {
AircraftIdentification(AircraftIdentification),
SurfacePosition(SurfacePosition),
AirbornePositionBaro(AirbornePositionBaro),
AirborneVelocities(AirborneVelocities),
AirbornePositionGNSS(AirbornePositionGNSS),
Reserved(u8),
AircraftStatus(AircraftStatus),
TargetState(TargetState),
AircraftOperationStatus(AircraftOperationStatus),
}
impl ADSBMessage {
pub fn decode(data: &[u8]) -> Result<ADSBMessage> {
if data.len() != 7 {
return Err(Error::new(
ErrorKind::InvalidInput,
format!("ME field must be 7 bytes, received {}", data.len()),
));
}
// First 5 bits is the type code
let type_code = data[0] >> 3;
let message = match type_code {
1..=4 => {
ADSBMessage::AircraftIdentification(AircraftIdentification::decode(type_code, data)?)
}
5..=8 => ADSBMessage::SurfacePosition(SurfacePosition::decode(data)?),
9..=18 => ADSBMessage::AirbornePositionBaro(AirbornePositionBaro::decode(data)?),
19 => ADSBMessage::AirborneVelocities(AirborneVelocities::decode(data)?),
20..=22 => ADSBMessage::AirbornePositionGNSS(AirbornePositionGNSS::decode(data)?),
23..=27 => ADSBMessage::Reserved(type_code),
28 => ADSBMessage::AircraftStatus(AircraftStatus::decode(data)?),
29 => ADSBMessage::TargetState(TargetState::decode(data)?),
31 => ADSBMessage::AircraftOperationStatus(AircraftOperationStatus::decode(data)?),
_ => {
return Err(Error::new(
ErrorKind::InvalidData,
format!("unsupported ADSB type_code {}", type_code),
))
}
};
Ok(message)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AircraftIdentification {
type_code: u8,
emitter_category: u8,
wake_vortex_category: WakeVortexCategory,
callsign: String,
}
impl AircraftIdentification {
pub fn decode(type_code: u8, data: &[u8]) -> Result<Self> {
// Byte 0: [ TC(5 bits) | emitter_category (3 bits) ]
let emitter_category = data[0] & 0x07;
// 56 bit buffer for message
let mut bits: u64 = 0;
for &b in data {
bits = (bits << 8) | b as u64;
}
let mut callsign = String::with_capacity(8);
for i in 0..8 {
let shift = 48 - 6 * (i + 1);
let raw6 = ((bits >> shift) & 0x3F) as u8;
let ch = match raw6 {
1..=26 => (b'A' + (raw6 - 1)) as char,
48..=57 => (b'0' + (raw6 - 48)) as char,
32 => ' ',
_ => continue,
};
callsign.push(ch);
}
// trim any trailing spaces
let callsign = callsign.trim_end().to_string();
Ok(Self {
type_code,
emitter_category,
wake_vortex_category: WakeVortexCategory::from_tc_ca(type_code, emitter_category),
callsign,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WakeVortexCategory {
NoInfo,
SurfaceEmergencyVehicle,
SurfaceServiceVehicle,
GroundObstruction,
Glider,
LighterThanAir,
Parachutist,
Ultralight,
Reserved,
UnmannedAerialVehicle,
SpaceVehicle,
Light,
Medium1,
Medium2,
HighVortex,
Heavy,
HighPerformance,
Rotorcraft,
Unknown,
}
impl WakeVortexCategory {
pub fn from_tc_ca(type_code: u8, emitter_category: u8) -> Self {
match (type_code, emitter_category) {
(_, 0) => WakeVortexCategory::NoInfo,
(2, 1) => WakeVortexCategory::SurfaceEmergencyVehicle,
(2, 3) => WakeVortexCategory::SurfaceServiceVehicle,
(2, 4..=7) => WakeVortexCategory::GroundObstruction,
(3, 1) => WakeVortexCategory::Glider,
(3, 2) => WakeVortexCategory::LighterThanAir,
(3, 3) => WakeVortexCategory::Parachutist,
(3, 4) => WakeVortexCategory::Ultralight,
(3, 5) => WakeVortexCategory::Reserved,
(3, 6) => WakeVortexCategory::UnmannedAerialVehicle,
(3, 7) => WakeVortexCategory::SpaceVehicle,
(4, 1) => WakeVortexCategory::Light,
(4, 2) => WakeVortexCategory::Medium1,
(4, 3) => WakeVortexCategory::Medium2,
(4, 4) => WakeVortexCategory::HighVortex,
(4, 5) => WakeVortexCategory::Heavy,
(4, 6) => WakeVortexCategory::HighPerformance,
(4, 7) => WakeVortexCategory::Rotorcraft,
_ => WakeVortexCategory::Unknown,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SurfacePosition {}
impl SurfacePosition {
pub fn decode(_data: &[u8]) -> Result<Self> {
Ok(Self {})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AirbornePositionBaro {}
impl AirbornePositionBaro {
pub fn decode(_data: &[u8]) -> Result<Self> {
Ok(Self {})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AirborneVelocities {}
impl AirborneVelocities {
pub fn decode(_data: &[u8]) -> Result<Self> {
Ok(Self {})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AirbornePositionGNSS {}
impl AirbornePositionGNSS {
pub fn decode(_data: &[u8]) -> Result<Self> {
Ok(Self {})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AircraftStatus {}
impl AircraftStatus {
pub fn decode(_data: &[u8]) -> Result<Self> {
Ok(Self {})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TargetState {}
impl TargetState {
pub fn decode(_data: &[u8]) -> Result<Self> {
Ok(Self {})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AircraftOperationStatus {}
impl AircraftOperationStatus {
pub fn decode(_data: &[u8]) -> Result<Self> {
Ok(Self {})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_decode_df_17_aircraft_information() {
let input = [
0x8D, 0x48, 0x40, 0xD6, 0x20, 0x2C, 0xC3, 0x71, 0xC3, 0x1C, 0x32, 0xCE, 0x05, 0x76,
];
let frame = ADSBFrame::decode(&input).unwrap();
assert_eq!(frame.downlink_format, 17);
assert_eq!(frame.capability, Capability::Level2Airborne);
assert_eq!(frame.icao, "4840D6");
match frame.message {
ADSBMessage::AircraftIdentification(ref id) => {
assert_eq!(id.type_code, 4);
assert_eq!(id.emitter_category, 0);
assert_eq!(id.wake_vortex_category, WakeVortexCategory::NoInfo);
assert_eq!(id.callsign, "KLM10102");
}
_ => panic!("expected AircraftIdentification"),
}
assert_eq!(frame.parity, 13501814);
let input = [
0x8D, 0x48, 0x40, 0xD6, 0x20, 0x2C, 0xC3, 0x71, 0xC3, 0x2C, 0xE0, 0x57, 0x60, 0x98,
];
let frame = ADSBFrame::decode(&input).unwrap();
assert_eq!(frame.downlink_format, 17);
assert_eq!(frame.capability, Capability::Level2Airborne);
assert_eq!(frame.icao, "4840D6");
match frame.message {
ADSBMessage::AircraftIdentification(ref id) => {
assert_eq!(id.type_code, 4);
assert_eq!(id.emitter_category, 0);
assert_eq!(id.wake_vortex_category, WakeVortexCategory::NoInfo);
assert_eq!(id.callsign, "KLM1023");
}
_ => panic!("expected AircraftIdentification"),
}
assert_eq!(frame.parity, 5726360);
let input = [
0x8D, 0x7C, 0x71, 0x81, 0x21, 0x5D, 0x01, 0xA0, 0x82, 0x08, 0x20, 0x4D, 0x8B, 0xF1,
];
let frame = ADSBFrame::decode(&input).unwrap();
assert_eq!(frame.downlink_format, 17);
assert_eq!(frame.capability, Capability::Level2Airborne);
assert_eq!(frame.icao, "7C7181");
match frame.message {
ADSBMessage::AircraftIdentification(ref id) => {
assert_eq!(id.type_code, 4);
assert_eq!(id.emitter_category, 1);
assert_eq!(id.wake_vortex_category, WakeVortexCategory::Light);
assert_eq!(id.callsign, "WPF");
}
_ => panic!("expected AircraftIdentification"),
}
assert_eq!(frame.parity, 5082097);
let input = [
0x8D, 0x7C, 0x77, 0x45, 0x22, 0x61, 0x51, 0xA0, 0x82, 0x08, 0x20, 0x5C, 0xE9, 0xC2,
];
let frame = ADSBFrame::decode(&input).unwrap();
assert_eq!(frame.downlink_format, 17);
assert_eq!(frame.capability, Capability::Level2Airborne);
assert_eq!(frame.icao, "7C7745");
match frame.message {
ADSBMessage::AircraftIdentification(ref id) => {
assert_eq!(id.type_code, 4);
assert_eq!(id.emitter_category, 2);
assert_eq!(id.wake_vortex_category, WakeVortexCategory::Medium1);
assert_eq!(id.callsign, "XUF");
}
_ => panic!("expected AircraftIdentification"),
}
assert_eq!(frame.parity, 6089154);
let input = [
0x8D, 0x7C, 0x80, 0xAD, 0x23, 0x58, 0xF6, 0xB1, 0xE3, 0x5C, 0x60, 0xFF, 0x19, 0x25,
];
let frame = ADSBFrame::decode(&input).unwrap();
assert_eq!(frame.downlink_format, 17);
assert_eq!(frame.capability, Capability::Level2Airborne);
assert_eq!(frame.icao, "7C80AD");
match frame.message {
ADSBMessage::AircraftIdentification(ref id) => {
assert_eq!(id.type_code, 4);
assert_eq!(id.emitter_category, 3);
assert_eq!(id.wake_vortex_category, WakeVortexCategory::Medium2);
assert_eq!(id.callsign, "VOZ1851");
}
_ => panic!("expected AircraftIdentification"),
}
assert_eq!(frame.parity, 16718117);
let input = [
0x8D, 0x7C, 0x14, 0x65, 0x25, 0x44, 0x60, 0x74, 0xDF, 0x58, 0x20, 0x73, 0x8E, 0x90,
];
let frame = ADSBFrame::decode(&input).unwrap();
assert_eq!(frame.downlink_format, 17);
assert_eq!(frame.capability, Capability::Level2Airborne);
assert_eq!(frame.icao, "7C1465");
match frame.message {
ADSBMessage::AircraftIdentification(ref id) => {
assert_eq!(id.type_code, 4);
assert_eq!(id.emitter_category, 5);
assert_eq!(id.wake_vortex_category, WakeVortexCategory::Heavy);
assert_eq!(id.callsign, "QFA475");
}
_ => panic!("expected AircraftIdentification"),
}
assert_eq!(frame.parity, 7573136);
}
#[test]
fn test_decode_df_17_operation_status() {
let input = [
0x8D, 0x89, 0x65, 0xD2, 0xF8, 0x21, 0x00, 0x02, 0x00, 0x49, 0xB8, 0x94, 0xA4, 0x5F,
];
let frame = ADSBFrame::decode(&input).unwrap();
dbg!(frame);
}
}

View File

@@ -1,4 +1,6 @@
use std::io::Result;
use std::io::{Error, ErrorKind, Result};
pub mod adsb;
pub trait RtlDevice {
/// Send a control message to the device
@@ -26,10 +28,7 @@ pub fn run<S: RtlDevice>(device: &mut S) -> Result<()> {
device.control_send(0x04, &[1])?;
// Precompute the preamble pattern in “halfbit” units (16 samples)
let preamble_halfbit_pattern = [
1,1, 0,0, 1,1, 0,0,
1,1, 0,0, 0,0, 0,0
];
let preamble_halfbit_pattern = [1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0];
// Create a big buffer to hold raw I/Q bytes
let mut iq_buffer = [0u8; 16_384];
@@ -50,11 +49,7 @@ pub fn run<S: RtlDevice>(device: &mut S) -> Result<()> {
for pair in raw.chunks_exact(2) {
let i_sample = pair[0] as u16;
// Threshold at 200
halfbit_samples.push(if i_sample > 200 {
1
} else {
0
});
halfbit_samples.push(if i_sample > 200 { 1 } else { 0 });
}
// Scan for the 16-sample preamble
@@ -68,7 +63,7 @@ pub fn run<S: RtlDevice>(device: &mut S) -> Result<()> {
let data_start = match data_start_index {
Some(i) => i,
None => continue // No preamble found in this chunk
None => continue, // No preamble found in this chunk
};
// Collect 112 ADS-B bits, each manchester-encoded into 2 half-bits
@@ -116,4 +111,47 @@ pub fn run<S: RtlDevice>(device: &mut S) -> Result<()> {
}
println!();
}
}
}
pub fn hex_to_bytes(s: &str) -> Result<Vec<u8>> {
let bytes = s.as_bytes();
if bytes.len() % 2 != 0 {
return Err(Error::new(
ErrorKind::InvalidInput,
format!("hex string must have even length, got {}", bytes.len()),
));
}
let mut out = Vec::with_capacity(bytes.len() / 2);
for chunk in bytes.chunks(2) {
let hi = match hex_val(chunk[0]) {
Some(hi) => hi,
None => {
return Err(Error::new(
ErrorKind::InvalidInput,
format!("invalid hex char '{}'", chunk[0] as char),
))
}
};
let lo = match hex_val(chunk[1]) {
Some(lo) => lo,
None => {
return Err(Error::new(
ErrorKind::InvalidInput,
format!("invalid hex char '{}'", chunk[1] as char),
))
}
};
out.push((hi << 4) | lo);
}
Ok(out)
}
fn hex_val(b: u8) -> Option<u8> {
match b {
b'0'..=b'9' => Some(b - b'0'),
b'a'..=b'f' => Some(b - b'a' + 10),
b'A'..=b'F' => Some(b - b'A' + 10),
_ => None,
}
}