474 lines
14 KiB
Rust
474 lines
14 KiB
Rust
use crate::hex_to_bytes;
|
|
use std::fmt::Display;
|
|
use crate::error::{Result, Error};
|
|
|
|
#[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(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(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(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(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 ADS-B spec
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum Capability {
|
|
/// 0: Level 1 transponder
|
|
Level1,
|
|
/// 1-3: Reserved
|
|
Reserved(u8),
|
|
/// 4: Level 2+ transponder, ground (can set CA=7)
|
|
Level2OnGround,
|
|
/// 5: Level 2+ transponder, airborne (can set CA=7)
|
|
Level2Airborne,
|
|
/// 6: Level 2+ transponder, either ground 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(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),
|
|
AirbornePosition(AirbornePosition),
|
|
AirborneVelocities(AirborneVelocities),
|
|
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(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::AirbornePosition(AirbornePosition::decode(type_code, data)?),
|
|
19 => ADSBMessage::AirborneVelocities(AirborneVelocities::decode(data)?),
|
|
20..=22 => ADSBMessage::AirbornePosition(AirbornePosition::decode(type_code, 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(format!(
|
|
"unsupported ADS-B 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 AirbornePosition {}
|
|
|
|
impl AirbornePosition {
|
|
pub fn decode(_type_code: u8, _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 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);
|
|
}
|
|
}
|