pub mod cpr;
use crate::bodies::aircraft::MetersPerSecond;
use crate::formats::{FileLocation, FormatError};
use crate::qtty::{Degrees, Meters};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AdsbFrame {
bytes: [u8; 14],
}
impl AdsbFrame {
#[inline]
pub fn downlink_format(&self) -> u8 {
self.bytes[0] >> 3
}
#[inline]
pub fn icao24(&self) -> u32 {
((self.bytes[1] as u32) << 16) | ((self.bytes[2] as u32) << 8) | (self.bytes[3] as u32)
}
#[inline]
pub fn me(&self) -> [u8; 7] {
let mut me = [0u8; 7];
me.copy_from_slice(&self.bytes[4..11]);
me
}
#[inline]
pub fn type_code(&self) -> u8 {
self.bytes[4] >> 3
}
}
const GENERATOR: u32 = 0x00FF_F409;
fn crc24(data: &[u8]) -> u32 {
let mut crc: u32 = 0;
for &b in data {
crc ^= (b as u32) << 16;
for _ in 0..8 {
crc <<= 1;
if crc & 0x0100_0000 != 0 {
crc ^= GENERATOR;
}
}
}
crc & 0x00FF_FFFF
}
pub fn parse_frame(hex: &str) -> Result<AdsbFrame, FormatError> {
if hex.len() != 28 {
return Err(FormatError::located(
"ADS-B DO-260B §A.1.8",
FileLocation::default(),
format!("expected 28 hex chars, got {}", hex.len()),
));
}
let mut bytes = [0u8; 14];
for (i, chunk) in hex.as_bytes().chunks(2).enumerate() {
let hi = hex_nibble(chunk[0]).ok_or_else(|| {
FormatError::located(
"ADS-B DO-260B §A.1.8",
FileLocation::default(),
format!("invalid hex character at offset {}", i * 2),
)
})?;
let lo = hex_nibble(chunk[1]).ok_or_else(|| {
FormatError::located(
"ADS-B DO-260B §A.1.8",
FileLocation::default(),
format!("invalid hex character at offset {}", i * 2 + 1),
)
})?;
bytes[i] = (hi << 4) | lo;
}
if crc24(&bytes) != 0 {
return Err(FormatError::located(
"ADS-B DO-260B §A.1.8.2",
FileLocation::default(),
"CRC-24 check failed",
));
}
let df = bytes[0] >> 3;
if df != 17 && df != 18 {
return Err(FormatError::located(
"ADS-B DO-260B §2.2.3",
FileLocation::default(),
format!("expected DF17 or DF18, got DF{df}"),
));
}
Ok(AdsbFrame { bytes })
}
#[inline]
fn hex_nibble(c: u8) -> Option<u8> {
match c {
b'0'..=b'9' => Some(c - b'0'),
b'a'..=b'f' => Some(c - b'a' + 10),
b'A'..=b'F' => Some(c - b'A' + 10),
_ => None,
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum AdsbMessage {
Identification(IdentificationMessage),
AirbornePosition(AirbornePositionMessage),
AirborneVelocity(AirborneVelocityMessage),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct IdentificationMessage {
pub category: u8,
pub callsign: String,
}
#[derive(Clone, Debug, PartialEq)]
pub struct AirbornePositionMessage {
pub surveillance_status: u8,
pub single_antenna: bool,
pub encoded_altitude: u16,
pub cpr_odd: bool,
pub cpr_lat: u32,
pub cpr_lon: u32,
}
#[derive(Clone, Debug, PartialEq)]
pub struct AirborneVelocityMessage {
pub heading: Option<Degrees>,
pub ground_speed_mps: Option<MetersPerSecond>,
pub vertical_rate_mps: Option<MetersPerSecond>,
}
pub fn decode_identification(frame: &AdsbFrame) -> Result<IdentificationMessage, FormatError> {
let tc = frame.type_code();
if !(1..=4).contains(&tc) {
return Err(FormatError::located(
"ADS-B DO-260B §2.2.3.2.3",
FileLocation::default(),
format!("expected TC 1–4 for identification, got TC{tc}"),
));
}
let me = frame.me();
let category = me[0] & 0x07;
const CHARSET: &[u8] = b"#ABCDEFGHIJKLMNOPQRSTUVWXYZ##### ###############0123456789######";
let mut callsign = String::with_capacity(8);
let bits: u64 = ((me[1] as u64) << 40)
| ((me[2] as u64) << 32)
| ((me[3] as u64) << 24)
| ((me[4] as u64) << 16)
| ((me[5] as u64) << 8)
| (me[6] as u64);
for i in 0..8 {
let idx = ((bits >> (42 - i * 6)) & 0x3F) as usize;
let ch = CHARSET[idx];
if ch == b'#' {
return Err(FormatError::located(
"ADS-B DO-260B §2.2.3.2.3.1",
FileLocation::default(),
format!("reserved callsign character (index {idx}) at position {i}"),
));
}
callsign.push(ch as char);
}
let callsign = callsign.trim_end_matches(' ').to_owned();
Ok(IdentificationMessage { category, callsign })
}
pub fn decode_airborne_position(frame: &AdsbFrame) -> Result<AirbornePositionMessage, FormatError> {
let tc = frame.type_code();
if !(9..=18).contains(&tc) {
return Err(FormatError::located(
"ADS-B DO-260B §2.2.3.2.6",
FileLocation::default(),
format!("expected TC 9–18 for airborne position, got TC{tc}"),
));
}
let me = frame.me();
let surveillance_status = (me[0] >> 1) & 0x03;
let single_antenna = (me[0] & 0x01) != 0;
let encoded_altitude = ((me[1] as u16) << 4) | ((me[2] as u16) >> 4);
let cpr_odd = (me[2] & 0x04) != 0;
let cpr_lat = ((me[2] as u32 & 0x03) << 15) | ((me[3] as u32) << 7) | (me[4] as u32 >> 1);
let cpr_lon = ((me[4] as u32 & 0x01) << 16) | ((me[5] as u32) << 8) | (me[6] as u32);
Ok(AirbornePositionMessage {
surveillance_status,
single_antenna,
encoded_altitude,
cpr_odd,
cpr_lat,
cpr_lon,
})
}
pub fn decode_airborne_velocity(frame: &AdsbFrame) -> Result<AirborneVelocityMessage, FormatError> {
let tc = frame.type_code();
if tc != 19 {
return Err(FormatError::located(
"ADS-B DO-260B §2.2.3.2.7",
FileLocation::default(),
format!("expected TC19 for airborne velocity, got TC{tc}"),
));
}
let me = frame.me();
let sub_type = me[0] & 0x07;
if sub_type == 1 || sub_type == 2 {
let sign_ew: f64 = if (me[1] & 0x04) != 0 { -1.0 } else { 1.0 };
let v_ew = (((me[1] as u16 & 0x03) << 8) | me[2] as u16) as f64 - 1.0;
let sign_ns: f64 = if (me[3] & 0x80) != 0 { -1.0 } else { 1.0 };
let v_ns = (((me[3] as u16 & 0x7F) << 3) | (me[4] >> 5) as u16) as f64 - 1.0;
let scale: f64 = if sub_type == 2 { 4.0 } else { 1.0 };
let vew_mps = sign_ew * v_ew * scale * 0.514_444;
let vns_mps = sign_ns * v_ns * scale * 0.514_444;
let gs = (vew_mps * vew_mps + vns_mps * vns_mps).sqrt();
let heading = if v_ew == 0.0 && v_ns == 0.0 {
None
} else {
let track_deg = vew_mps.atan2(vns_mps).to_degrees().rem_euclid(360.0);
Some(Degrees::new(track_deg))
};
let vr_sign: f64 = if (me[4] & 0x08) != 0 { -1.0 } else { 1.0 };
let vr_raw = (((me[4] as u16 & 0x07) << 6) | (me[5] >> 2) as u16) as f64 - 1.0;
let vr_fpm = vr_sign * vr_raw * 64.0; let vr_mps = vr_fpm * 0.00508;
return Ok(AirborneVelocityMessage {
heading,
ground_speed_mps: Some(MetersPerSecond::new(gs)),
vertical_rate_mps: Some(MetersPerSecond::new(vr_mps)),
});
}
Err(FormatError::located(
"ADS-B DO-260B §2.2.3.2.7",
FileLocation::default(),
format!("unsupported airborne velocity sub-type {sub_type}"),
))
}
pub fn decode_altitude(encoded: u16) -> Option<Meters> {
if encoded == 0 {
return None;
}
let q_bit = (encoded >> 4) & 1;
if q_bit == 1 {
let n = ((((encoded >> 5) << 4) | (encoded & 0x0F)) as i32) * 25 - 1_000;
let alt_m = n as f64 * 0.304_8;
Some(Meters::new(alt_m))
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
const POSITION_FRAME: &str = "8D40621D58C382D690C8AC2863A7";
#[test]
fn parse_frame_valid() {
let frame = parse_frame(POSITION_FRAME).expect("valid frame should parse");
assert_eq!(frame.downlink_format(), 17);
assert_eq!(frame.icao24(), 0x40621D);
assert_eq!(frame.type_code(), 11);
}
#[test]
fn parse_frame_too_short() {
assert!(parse_frame("8D4840D620").is_err());
}
#[test]
fn parse_frame_bad_crc() {
let bad = "8D4840D6202CC371C32CE0576099";
assert!(parse_frame(bad).is_err());
}
#[test]
fn parse_frame_wrong_df() {
let short = "1A";
assert!(parse_frame(short).is_err());
}
#[test]
fn airborne_position_decode() {
let frame = parse_frame(POSITION_FRAME).unwrap();
let msg = decode_airborne_position(&frame).unwrap();
assert!(!msg.cpr_odd); }
#[test]
fn decode_altitude_q_bit_set() {
let encoded: u16 = 888;
let alt = decode_altitude(encoded).unwrap();
assert!(
(alt.value() - 3_048.0).abs() < 0.01,
"alt = {}",
alt.value()
);
}
#[test]
fn decode_altitude_zero_not_available() {
assert!(decode_altitude(0).is_none());
}
#[test]
fn crc24_all_zeros_no_data_bytes() {
assert_eq!(crc24(&[]), 0);
}
const IDENT_FRAME: &str = "8D4840D6232CC371CB3D2048E9A0";
const VELOCITY_FRAME: &str = "8D4840D69900651920400034CDB3";
#[test]
fn decode_identification_succeeds() {
let frame = parse_frame(IDENT_FRAME).unwrap();
assert_eq!(frame.type_code(), 4);
let id = decode_identification(&frame).unwrap();
assert!(!id.callsign.is_empty());
}
#[test]
fn decode_identification_wrong_tc_is_error() {
let frame = parse_frame(POSITION_FRAME).unwrap();
assert!(decode_identification(&frame).is_err());
}
#[test]
fn decode_airborne_velocity_succeeds() {
let frame = parse_frame(VELOCITY_FRAME).unwrap();
assert_eq!(frame.type_code(), 19);
let vel = decode_airborne_velocity(&frame).unwrap();
assert!(vel.ground_speed_mps.is_some());
}
#[test]
fn decode_airborne_velocity_wrong_tc_is_error() {
let frame = parse_frame(POSITION_FRAME).unwrap();
assert!(decode_airborne_velocity(&frame).is_err());
}
#[test]
fn decode_altitude_q_bit_zero_returns_none() {
let encoded: u16 = 1; assert!(decode_altitude(encoded).is_none());
}
}