pub mod bds05;
pub mod bds06;
pub mod bds08;
pub mod bds09;
pub mod bds10;
pub mod bds17;
pub mod bds18;
pub mod bds19;
pub mod bds20;
pub mod bds21;
pub mod bds30;
pub mod bds40;
pub mod bds44;
pub mod bds45;
pub mod bds50;
pub mod bds60;
pub mod bds61;
pub mod bds62;
pub mod bds65;
#[cfg(feature = "bds-infer")]
pub mod density;
#[cfg(feature = "bds-infer")]
pub mod penalty;
use self::bds05::AirbornePosition;
use self::bds06::SurfacePosition;
use self::bds08::AircraftIdentification as Bds08AircraftIdentification;
use self::bds09::AirborneVelocity;
use self::bds10::DataLinkCapability;
use self::bds17::CommonUsageGICBCapabilityReport;
use self::bds18::GICBCapabilityReportPart1;
use self::bds19::GICBCapabilityReportPart2;
use self::bds20::AircraftIdentification as Bds20AircraftIdentification;
use self::bds21::AircraftAndAirlineRegistrationMarkings;
use self::bds30::ACASResolutionAdvisory;
use self::bds40::SelectedVerticalIntention;
use self::bds44::MeteorologicalRoutineAirReport;
use self::bds45::MeteorologicalHazardReport;
use self::bds50::TrackAndTurnReport;
use self::bds60::HeadingAndSpeedReport;
use self::bds61::AircraftStatus;
use self::bds62::TargetStateAndStatusInformation;
use self::bds65::AircraftOperationStatus;
use deku::{DekuContainerRead, DekuError, DekuReader};
use serde::Serialize;
use std::fmt;
#[derive(Debug, Clone, PartialEq, Serialize)]
pub enum DecodingError {
InvalidBdsCode(u8),
TypeCodeMismatch {
bds_code: u8,
received_tc: u8,
expected_tc: String,
},
PayloadTooShort {
expected: usize,
received: usize,
},
DecodingFailed(String),
}
impl fmt::Display for DecodingError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DecodingError::InvalidBdsCode(code) => {
write!(f, "Invalid BDS code 0x{:02x}", code)
}
DecodingError::TypeCodeMismatch {
bds_code,
received_tc,
expected_tc,
} => {
write!(
f,
"Type Code mismatch for BDS 0x{:02x}: received TC={}, expected {}",
bds_code, received_tc, expected_tc
)
}
DecodingError::PayloadTooShort { expected, received } => {
write!(
f,
"Payload too short: expected {} bytes, got {}",
expected, received
)
}
DecodingError::DecodingFailed(msg) => {
write!(f, "Decoding failed: {}", msg)
}
}
}
}
impl std::error::Error for DecodingError {}
impl From<DekuError> for DecodingError {
fn from(err: DekuError) -> Self {
DecodingError::DecodingFailed(err.to_string())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum DecodedBds {
Bds05(AirbornePosition),
Bds06(SurfacePosition),
Bds08(Bds08AircraftIdentification),
Bds09(AirborneVelocity),
Bds10(DataLinkCapability),
Bds17(CommonUsageGICBCapabilityReport),
Bds18(GICBCapabilityReportPart1),
Bds19(GICBCapabilityReportPart2),
Bds20(Bds20AircraftIdentification),
Bds21(AircraftAndAirlineRegistrationMarkings),
Bds30(ACASResolutionAdvisory),
Bds40(SelectedVerticalIntention),
Bds44(MeteorologicalRoutineAirReport),
Bds45(MeteorologicalHazardReport),
Bds50(TrackAndTurnReport),
Bds60(HeadingAndSpeedReport),
Bds61(AircraftStatus),
Bds62(TargetStateAndStatusInformation),
Bds65(AircraftOperationStatus),
}
fn extract_tc(payload: &[u8]) -> u8 {
if payload.is_empty() {
return 0;
}
payload[0] >> 3
}
#[inline]
pub(crate) fn validate_score_gate(
decoded: &DecodedBds,
) -> Result<(), DecodingError> {
#[cfg(feature = "bds-infer")]
{
let mean = density::density_score(decoded);
let pen = penalty::penalty(decoded);
if mean.is_none() && pen == 0.0 {
return Ok(());
}
let score = mean.unwrap_or(0.0) + pen;
if score < density::DENSITY_THRESHOLD {
Err(DecodingError::DecodingFailed(format!(
"combined score {score:.2} below threshold {:.1}",
density::DENSITY_THRESHOLD
)))
} else {
Ok(())
}
}
#[cfg(not(feature = "bds-infer"))]
{
let _ = decoded;
Ok(())
}
}
fn apply_score_gate(
result: Result<DecodedBds, DecodingError>,
) -> Result<DecodedBds, DecodingError> {
match result {
Ok(decoded) => validate_score_gate(&decoded).map(|()| decoded),
Err(e) => Err(e),
}
}
pub fn decode_payload(
payload: &[u8],
bds_code: u8,
) -> Result<DecodedBds, DecodingError> {
let result = match bds_code {
0x05 => {
let tc = extract_tc(payload);
if !((9..=18).contains(&tc) || (20..=22).contains(&tc)) {
return Err(DecodingError::TypeCodeMismatch {
bds_code,
received_tc: tc,
expected_tc: "9-18, 20-22".to_string(),
});
}
let mut reader =
deku::reader::Reader::new(std::io::Cursor::new(payload));
reader.skip_bits(5, deku::ctx::Order::Msb0)?;
AirbornePosition::from_reader_with_ctx(&mut reader, tc)
.map(DecodedBds::Bds05)
.map_err(|e| DecodingError::DecodingFailed(e.to_string()))
}
0x06 => {
let tc = extract_tc(payload);
if !(5..=8).contains(&tc) {
return Err(DecodingError::TypeCodeMismatch {
bds_code,
received_tc: tc,
expected_tc: "5-8".to_string(),
});
}
let mut reader =
deku::reader::Reader::new(std::io::Cursor::new(payload));
reader.skip_bits(5, deku::ctx::Order::Msb0)?;
SurfacePosition::from_reader_with_ctx(&mut reader, tc)
.map(DecodedBds::Bds06)
.map_err(|e| DecodingError::DecodingFailed(e.to_string()))
}
0x08 => {
let tc = extract_tc(payload);
if !(1..=4).contains(&tc) {
return Err(DecodingError::TypeCodeMismatch {
bds_code,
received_tc: tc,
expected_tc: "1-4".to_string(),
});
}
let mut reader =
deku::reader::Reader::new(std::io::Cursor::new(payload));
reader.skip_bits(5, deku::ctx::Order::Msb0)?;
Bds08AircraftIdentification::from_reader_with_ctx(&mut reader, tc)
.map(DecodedBds::Bds08)
.map_err(|e| DecodingError::DecodingFailed(e.to_string()))
}
0x09 => {
let tc = extract_tc(payload);
if tc != 19 {
return Err(DecodingError::TypeCodeMismatch {
bds_code,
received_tc: tc,
expected_tc: "19".to_string(),
});
}
AirborneVelocity::from_bytes((payload, 5))
.map(|(_, decoded)| DecodedBds::Bds09(decoded))
.map_err(|e| DecodingError::DecodingFailed(e.to_string()))
}
0x10 => DataLinkCapability::from_bytes((payload, 0))
.map(|(_, decoded)| DecodedBds::Bds10(decoded))
.map_err(|e| DecodingError::DecodingFailed(e.to_string())),
0x17 => CommonUsageGICBCapabilityReport::from_bytes((payload, 0))
.map(|(_, decoded)| DecodedBds::Bds17(decoded))
.map_err(|e| DecodingError::DecodingFailed(e.to_string())),
0x18 => GICBCapabilityReportPart1::from_bytes((payload, 0))
.map(|(_, decoded)| DecodedBds::Bds18(decoded))
.map_err(|e| DecodingError::DecodingFailed(e.to_string())),
0x19 => GICBCapabilityReportPart2::from_bytes((payload, 0))
.map(|(_, decoded)| DecodedBds::Bds19(decoded))
.map_err(|e| DecodingError::DecodingFailed(e.to_string())),
0x20 => Bds20AircraftIdentification::from_bytes((payload, 0))
.map(|(_, decoded)| DecodedBds::Bds20(decoded))
.map_err(|e| DecodingError::DecodingFailed(e.to_string())),
0x21 => {
AircraftAndAirlineRegistrationMarkings::from_bytes((payload, 0))
.map(|(_, decoded)| DecodedBds::Bds21(decoded))
.map_err(|e| DecodingError::DecodingFailed(e.to_string()))
}
0x30 => ACASResolutionAdvisory::from_bytes((payload, 0))
.map(|(_, decoded)| DecodedBds::Bds30(decoded))
.map_err(|e| DecodingError::DecodingFailed(e.to_string())),
0x40 => SelectedVerticalIntention::from_bytes((payload, 0))
.map(|(_, decoded)| DecodedBds::Bds40(decoded))
.map_err(|e| DecodingError::DecodingFailed(e.to_string())),
0x44 => MeteorologicalRoutineAirReport::from_bytes((payload, 0))
.map(|(_, decoded)| DecodedBds::Bds44(decoded))
.map_err(|e| DecodingError::DecodingFailed(e.to_string())),
0x45 => MeteorologicalHazardReport::from_bytes((payload, 0))
.map(|(_, decoded)| DecodedBds::Bds45(decoded))
.map_err(|e| DecodingError::DecodingFailed(e.to_string())),
0x50 => TrackAndTurnReport::from_bytes((payload, 0))
.map(|(_, decoded)| DecodedBds::Bds50(decoded))
.map_err(|e| DecodingError::DecodingFailed(e.to_string())),
0x60 => HeadingAndSpeedReport::from_bytes((payload, 0))
.map(|(_, decoded)| DecodedBds::Bds60(decoded))
.map_err(|e| DecodingError::DecodingFailed(e.to_string())),
0x61 => {
let tc = extract_tc(payload);
if tc != 28 {
return Err(DecodingError::TypeCodeMismatch {
bds_code,
received_tc: tc,
expected_tc: "28".to_string(),
});
}
AircraftStatus::from_bytes((payload, 5))
.map(|(_, decoded)| DecodedBds::Bds61(decoded))
.map_err(|e| DecodingError::DecodingFailed(e.to_string()))
}
0x62 => {
let tc = extract_tc(payload);
if tc != 29 {
return Err(DecodingError::TypeCodeMismatch {
bds_code,
received_tc: tc,
expected_tc: "29".to_string(),
});
}
TargetStateAndStatusInformation::from_bytes((payload, 5))
.map(|(_, decoded)| DecodedBds::Bds62(decoded))
.map_err(|e| DecodingError::DecodingFailed(e.to_string()))
}
0x65 => {
let tc = extract_tc(payload);
if tc != 31 {
return Err(DecodingError::TypeCodeMismatch {
bds_code,
received_tc: tc,
expected_tc: "31".to_string(),
});
}
AircraftOperationStatus::from_bytes((payload, 5))
.map(|(_, decoded)| DecodedBds::Bds65(decoded))
.map_err(|e| DecodingError::DecodingFailed(e.to_string()))
}
_ => Err(DecodingError::InvalidBdsCode(bds_code)),
};
apply_score_gate(result)
}
pub fn decode_bds(
payload: &[u8],
bds_code: u8,
) -> Result<DecodedBds, DecodingError> {
if payload.is_empty() {
return Err(DecodingError::PayloadTooShort {
expected: 1,
received: 0,
});
}
let result = match bds_code {
0x05 => {
let tc = extract_tc(payload);
if !((9..=18).contains(&tc) || (20..=22).contains(&tc)) {
return Err(DecodingError::TypeCodeMismatch {
bds_code,
received_tc: tc,
expected_tc: "9-18, 20-22".to_string(),
});
}
let mut reader =
deku::reader::Reader::new(std::io::Cursor::new(payload));
reader.skip_bits(5, deku::ctx::Order::Msb0)?;
AirbornePosition::from_reader_with_ctx(&mut reader, tc)
.map(DecodedBds::Bds05)
.map_err(|e| DecodingError::DecodingFailed(e.to_string()))
}
0x06 => {
let tc = extract_tc(payload);
if !(5..=8).contains(&tc) {
return Err(DecodingError::TypeCodeMismatch {
bds_code,
received_tc: tc,
expected_tc: "5-8".to_string(),
});
}
let mut reader =
deku::reader::Reader::new(std::io::Cursor::new(payload));
reader.skip_bits(5, deku::ctx::Order::Msb0)?;
SurfacePosition::from_reader_with_ctx(&mut reader, tc)
.map(DecodedBds::Bds06)
.map_err(|e| DecodingError::DecodingFailed(e.to_string()))
}
0x08 => {
let tc = extract_tc(payload);
if !(1..=4).contains(&tc) {
return Err(DecodingError::TypeCodeMismatch {
bds_code,
received_tc: tc,
expected_tc: "1-4".to_string(),
});
}
let mut reader =
deku::reader::Reader::new(std::io::Cursor::new(payload));
reader.skip_bits(5, deku::ctx::Order::Msb0)?;
Bds08AircraftIdentification::from_reader_with_ctx(&mut reader, tc)
.map(DecodedBds::Bds08)
.map_err(|e| DecodingError::DecodingFailed(e.to_string()))
}
0x09 => {
let tc = extract_tc(payload);
if tc != 19 {
return Err(DecodingError::TypeCodeMismatch {
bds_code,
received_tc: tc,
expected_tc: "19".to_string(),
});
}
AirborneVelocity::from_bytes((payload, 5))
.map(|(_, decoded)| DecodedBds::Bds09(decoded))
.map_err(|e| DecodingError::DecodingFailed(e.to_string()))
}
0x10 => DataLinkCapability::from_bytes((payload, 0))
.map(|(_, decoded)| DecodedBds::Bds10(decoded))
.map_err(|e| DecodingError::DecodingFailed(e.to_string())),
0x17 => CommonUsageGICBCapabilityReport::from_bytes((payload, 0))
.map(|(_, decoded)| DecodedBds::Bds17(decoded))
.map_err(|e| DecodingError::DecodingFailed(e.to_string())),
0x18 => GICBCapabilityReportPart1::from_bytes((payload, 0))
.map(|(_, decoded)| DecodedBds::Bds18(decoded))
.map_err(|e| DecodingError::DecodingFailed(e.to_string())),
0x19 => GICBCapabilityReportPart2::from_bytes((payload, 0))
.map(|(_, decoded)| DecodedBds::Bds19(decoded))
.map_err(|e| DecodingError::DecodingFailed(e.to_string())),
0x20 => Bds20AircraftIdentification::from_bytes((payload, 0))
.map(|(_, decoded)| DecodedBds::Bds20(decoded))
.map_err(|e| DecodingError::DecodingFailed(e.to_string())),
0x21 => {
AircraftAndAirlineRegistrationMarkings::from_bytes((payload, 0))
.map(|(_, decoded)| DecodedBds::Bds21(decoded))
.map_err(|e| DecodingError::DecodingFailed(e.to_string()))
}
0x30 => ACASResolutionAdvisory::from_bytes((payload, 0))
.map(|(_, decoded)| DecodedBds::Bds30(decoded))
.map_err(|e| DecodingError::DecodingFailed(e.to_string())),
0x40 => SelectedVerticalIntention::from_bytes((payload, 0))
.map(|(_, decoded)| DecodedBds::Bds40(decoded))
.map_err(|e| DecodingError::DecodingFailed(e.to_string())),
0x44 => MeteorologicalRoutineAirReport::from_bytes((payload, 0))
.map(|(_, decoded)| DecodedBds::Bds44(decoded))
.map_err(|e| DecodingError::DecodingFailed(e.to_string())),
0x45 => MeteorologicalHazardReport::from_bytes((payload, 0))
.map(|(_, decoded)| DecodedBds::Bds45(decoded))
.map_err(|e| DecodingError::DecodingFailed(e.to_string())),
0x50 => TrackAndTurnReport::from_bytes((payload, 0))
.map(|(_, decoded)| DecodedBds::Bds50(decoded))
.map_err(|e| DecodingError::DecodingFailed(e.to_string())),
0x60 => HeadingAndSpeedReport::from_bytes((payload, 0))
.map(|(_, decoded)| DecodedBds::Bds60(decoded))
.map_err(|e| DecodingError::DecodingFailed(e.to_string())),
0x61 => {
let tc = extract_tc(payload);
if tc != 28 {
return Err(DecodingError::TypeCodeMismatch {
bds_code,
received_tc: tc,
expected_tc: "28".to_string(),
});
}
AircraftStatus::from_bytes((payload, 5))
.map(|(_, decoded)| DecodedBds::Bds61(decoded))
.map_err(|e| DecodingError::DecodingFailed(e.to_string()))
}
0x62 => {
let tc = extract_tc(payload);
if tc != 29 {
return Err(DecodingError::TypeCodeMismatch {
bds_code,
received_tc: tc,
expected_tc: "29".to_string(),
});
}
TargetStateAndStatusInformation::from_bytes((payload, 5))
.map(|(_, decoded)| DecodedBds::Bds62(decoded))
.map_err(|e| DecodingError::DecodingFailed(e.to_string()))
}
0x65 => {
let tc = extract_tc(payload);
if tc != 31 {
return Err(DecodingError::TypeCodeMismatch {
bds_code,
received_tc: tc,
expected_tc: "31".to_string(),
});
}
let subtype = payload[0] & 0b111;
if subtype != 0 {
return Err(DecodingError::DecodingFailed(format!(
"BDS 65 subtype {subtype} rejected (only Airborne / subtype 0 accepted)"
)));
}
AircraftOperationStatus::from_bytes((payload, 5))
.map(|(_, decoded)| DecodedBds::Bds65(decoded))
.map_err(|e| DecodingError::DecodingFailed(e.to_string()))
}
_ => Err(DecodingError::InvalidBdsCode(bds_code)),
};
apply_score_gate(result)
}
pub fn infer_bds(payload: &[u8], icao24: Option<u32>) -> Vec<DecodedBds> {
if payload.is_empty() {
return Vec::new();
}
let mut results = Vec::new();
let candidates: &[u8] = &[
0x05, 0x10, 0x17, 0x18, 0x19, 0x20, 0x21, 0x30, 0x40, 0x44, 0x45, 0x50,
0x60, 0x65,
];
for bds_code in candidates {
if let Ok(decoded) = decode_bds(payload, *bds_code) {
#[cfg(feature = "bds-infer")]
if let (DecodedBds::Bds21(ref bds21), Some(addr)) =
(&decoded, icao24)
{
if let Some(reg) = &bds21.aircraft_registration {
if !bds21::validate_registration(reg, addr) {
continue;
}
}
}
results.push(decoded);
}
}
#[cfg(feature = "bds-infer")]
{
let has_winner = results.iter().any(|d| {
matches!(
d,
DecodedBds::Bds10(_)
| DecodedBds::Bds20(_)
| DecodedBds::Bds30(_)
)
});
if has_winner {
results.retain(|d| {
matches!(
d,
DecodedBds::Bds10(_)
| DecodedBds::Bds20(_)
| DecodedBds::Bds30(_)
)
});
}
}
results
}