use super::bds::{
bds08::callsign_read, decode_payload, DecodedBds, DecodingError,
};
use super::ICAO;
use deku::prelude::*;
use serde::{Serialize, Serializer};
fn serialize_u8_as_hex<S: Serializer>(v: &u8, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&format!("{:02x}", v))
}
#[derive(Debug, Clone, PartialEq, DekuRead, Serialize)]
pub struct Cat48Record {
#[deku(assert_eq = "48")]
pub cat: u8,
#[deku(endian = "big")]
#[serde(skip)]
pub length: u16,
#[deku(reader = "Self::read_fspec(deku::reader)")]
#[serde(skip)]
pub fspec: Vec<u8>,
#[deku(cond = "Self::has_frn(&fspec, 1)")]
#[serde(flatten, skip_serializing_if = "Option::is_none")]
pub data_source_id: Option<DataSourceIdentifier>,
#[deku(
cond = "Self::has_frn(&fspec, 2)",
endian = "big",
bits = 24,
map = "|v: Option<u32>| -> Result<_, DekuError> { Ok(v.map(|t| t as f64 / 128.0)) }"
)]
#[serde(rename = "time_of_day", skip_serializing_if = "Option::is_none")]
pub time_of_day: Option<f64>,
#[deku(cond = "Self::has_frn(&fspec, 3)")]
#[serde(flatten, skip_serializing_if = "Option::is_none")]
pub target_report_descriptor: Option<TargetReportDescriptor>,
#[deku(cond = "Self::has_frn(&fspec, 4)")]
#[serde(skip_serializing_if = "Option::is_none")]
pub measured_position: Option<MeasuredPosition>,
#[deku(
cond = "Self::has_frn(&fspec, 5)",
endian = "big",
map = "|v: Option<u16>| -> Result<_, DekuError> { Ok(v.and_then(|code| { if code & 0x8000 != 0 { return None; } Some(format!(\"{:04o}\", code & 0x0FFF)) })) }"
)]
#[serde(rename = "squawk", skip_serializing_if = "Option::is_none")]
pub mode_3a_code: Option<String>,
#[deku(
cond = "Self::has_frn(&fspec, 6)",
endian = "big",
map = "|v: Option<u16>| -> Result<_, DekuError> { Ok(v.and_then(|fl| { if fl & 0x8000 != 0 { return None; } Some((((fl as i16) << 2 >> 2) as i32) * 25) })) }"
)]
#[serde(rename = "altitude", skip_serializing_if = "Option::is_none")]
pub flight_level: Option<i32>,
#[deku(cond = "Self::has_frn(&fspec, 7)")]
#[serde(skip_serializing_if = "Option::is_none")]
pub radar_plot_characteristics: Option<RadarPlotCharacteristics>,
#[deku(cond = "Self::has_frn(&fspec, 8)")]
#[serde(rename = "icao24", skip_serializing_if = "Option::is_none")]
pub aircraft_address: Option<ICAO>,
#[deku(cond = "Self::has_frn(&fspec, 9)")]
#[serde(flatten, skip_serializing_if = "Option::is_none")]
pub aircraft_id: Option<AircraftIdentification>,
#[deku(cond = "Self::has_frn(&fspec, 10)")]
#[serde(rename = "mode_s", skip_serializing_if = "Option::is_none")]
pub mode_s_mb_data: Option<ModeSMbData>,
#[deku(cond = "Self::has_frn(&fspec, 11)", endian = "big")]
#[serde(skip_serializing_if = "Option::is_none")]
pub track_number: Option<u16>,
#[deku(cond = "Self::has_frn(&fspec, 12)")]
#[serde(skip_serializing_if = "Option::is_none")]
pub calculated_position: Option<CalculatedPosition>,
#[deku(cond = "Self::has_frn(&fspec, 13)")]
#[serde(skip_serializing_if = "Option::is_none")]
pub track_velocity: Option<TrackVelocity>,
#[deku(cond = "Self::has_frn(&fspec, 14)")]
#[serde(skip_serializing_if = "Option::is_none")]
pub track_status: Option<TrackStatus>,
#[deku(cond = "Self::has_frn(&fspec, 15)")]
#[serde(skip_serializing_if = "Option::is_none")]
pub track_quality: Option<TrackQuality>,
#[deku(cond = "Self::has_frn(&fspec, 16)")]
#[serde(skip_serializing_if = "Option::is_none")]
pub warning_error: Option<WarningError>,
#[deku(cond = "Self::has_frn(&fspec, 17)", endian = "big")]
#[serde(skip_serializing_if = "Option::is_none")]
pub mode_3a_confidence: Option<u16>,
#[deku(cond = "Self::has_frn(&fspec, 18)", endian = "big")]
#[serde(skip_serializing_if = "Option::is_none")]
pub mode_c_code: Option<u32>,
#[deku(cond = "Self::has_frn(&fspec, 19)", endian = "big")]
#[serde(skip_serializing_if = "Option::is_none")]
pub height_3d: Option<u16>,
#[deku(cond = "Self::has_frn(&fspec, 20)")]
#[serde(skip_serializing_if = "Option::is_none")]
pub radial_doppler_speed: Option<RadialDopplerSpeed>,
#[deku(cond = "Self::has_frn(&fspec, 21)", endian = "big")]
#[serde(skip_serializing_if = "Option::is_none")]
pub comms_acas: Option<u16>,
#[deku(cond = "Self::has_frn(&fspec, 22)")]
#[serde(skip_serializing_if = "Option::is_none")]
pub acas_ra: Option<AcasResolutionAdvisory>,
#[deku(cond = "Self::has_frn(&fspec, 23)")]
#[serde(skip_serializing_if = "Option::is_none")]
pub mode_1_code: Option<u8>,
#[deku(cond = "Self::has_frn(&fspec, 24)", endian = "big")]
#[serde(skip_serializing_if = "Option::is_none")]
pub mode_2_code: Option<u16>,
#[deku(cond = "Self::has_frn(&fspec, 25)")]
#[serde(skip_serializing_if = "Option::is_none")]
pub mode_1_confidence: Option<u8>,
#[deku(cond = "Self::has_frn(&fspec, 26)", endian = "big")]
#[serde(skip_serializing_if = "Option::is_none")]
pub mode_2_confidence: Option<u16>,
#[deku(
cond = "Self::has_frn(&fspec, 27)",
reader = "Self::read_explicit_field(deku::reader)"
)]
#[serde(skip_serializing_if = "Option::is_none")]
pub special_purpose: Option<Vec<u8>>,
#[deku(
cond = "Self::has_frn(&fspec, 28)",
reader = "Self::read_explicit_field(deku::reader)"
)]
#[serde(skip_serializing_if = "Option::is_none")]
pub reserved_expansion: Option<Vec<u8>>,
}
#[derive(Debug, Clone, PartialEq, DekuRead, Serialize)]
pub struct DataSourceIdentifier {
#[serde(serialize_with = "serialize_u8_as_hex")]
pub sac: u8,
#[serde(serialize_with = "serialize_u8_as_hex")]
pub sic: u8,
}
#[derive(Debug, Clone, PartialEq, DekuRead, Serialize)]
pub struct TargetReportDescriptor {
pub typ: DetectionType,
#[deku(bits = "1")]
#[serde(skip)]
pub sim: bool,
#[deku(bits = "1")]
#[serde(skip)]
pub rdp: u8,
#[deku(bits = "1")]
#[serde(skip)]
pub spi: bool,
#[deku(bits = "1")]
#[serde(skip)]
pub rab: bool,
#[deku(bits = "1")]
#[serde(skip)]
fx1: bool,
#[deku(cond = "*fx1")]
#[serde(skip)]
pub extension1: Option<TrdExtension1>,
}
#[derive(Debug, Clone, PartialEq, DekuRead, Serialize)]
pub struct TrdExtension1 {
#[deku(bits = "1", pad_bits_after = "1")]
pub tst: bool,
#[deku(bits = "1")]
pub xpp: bool,
#[deku(bits = "1")]
pub me: bool,
#[deku(bits = "1")]
pub mi: bool,
pub foe_fri: FoeFri,
#[deku(bits = "1")]
#[serde(skip)]
fx: bool,
#[deku(cond = "*fx")]
pub extension2: Option<TrdExtension2>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, DekuRead, Serialize)]
pub struct ExtensionIndicator {
#[deku(bits = "1")]
pub populated: bool,
#[deku(bits = "1")]
pub available: bool,
}
#[derive(Debug, Clone, PartialEq, DekuRead, Serialize)]
pub struct TrdExtension2 {
pub adsb: ExtensionIndicator,
pub scn: ExtensionIndicator,
pub pai: ExtensionIndicator,
#[deku(bits = "1")]
fx: bool,
#[deku(bits = "1", pad_bits_after = "0")]
_spare: u8,
#[deku(cond = "*fx", reader = "TrdExtension2::skip_fx_tail(deku::reader)")]
#[serde(skip)]
_fx_tail: Option<()>,
}
impl TrdExtension2 {
fn skip_fx_tail<R: std::io::Read + std::io::Seek>(
reader: &mut deku::reader::Reader<R>,
) -> Result<Option<()>, DekuError> {
loop {
let byte = u8::from_reader_with_ctx(reader, ())?;
if byte & 0x01 == 0 {
break;
}
}
Ok(Some(()))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, DekuRead, Serialize)]
#[deku(id_type = "u8", bits = "3")]
pub enum DetectionType {
#[deku(id = "0")]
NoDetection,
#[deku(id = "1")]
SinglePsr,
#[deku(id = "2")]
SingleSsr,
#[deku(id = "3")]
SsrPsr,
#[deku(id = "4")]
ModeSAllCall,
#[deku(id = "5")]
ModeSRollCall,
#[deku(id = "6")]
ModeSAllCallPsr,
#[deku(id = "7")]
ModeSRollCallPsr,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, DekuRead, Serialize)]
#[deku(id_type = "u8", bits = "2")]
pub enum FoeFri {
#[deku(id = "0")]
NoMode4,
#[deku(id = "1")]
Friendly,
#[deku(id = "2")]
Unknown,
#[deku(id = "3")]
NoReply,
}
#[derive(Debug, Clone, PartialEq, DekuRead, Serialize)]
pub struct MeasuredPosition {
#[deku(
endian = "big",
map = "|v: u16| -> Result<_, DekuError> { Ok(v as f64 / 256.0) }"
)]
pub rho: f64,
#[deku(
endian = "big",
map = "|v: u16| -> Result<_, DekuError> { Ok(v as f64 * 360.0 / 65536.0) }"
)]
pub theta: f64,
}
#[derive(Debug, Clone, PartialEq, DekuRead, Serialize)]
pub struct AircraftIdentification {
#[deku(reader = "callsign_read(deku::reader)")]
pub callsign: String,
}
#[derive(Debug, Clone, PartialEq, DekuRead, Serialize)]
pub struct ModeSMbData {
pub count: u8,
#[deku(count = "count")]
pub records: Vec<BdsRecord>,
}
fn serialize_decode_result<S>(
result: &Result<DecodedBds, DecodingError>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match result {
Ok(decoded) => decoded.serialize(serializer),
Err(err) => {
use serde::ser::SerializeMap;
let mut map = serializer.serialize_map(Some(1))?;
map.serialize_entry("error", &err.to_string())?;
map.end()
}
}
}
#[derive(Debug, Clone, PartialEq, DekuRead, Serialize)]
pub struct BdsRecord {
#[deku(count = "7")]
#[serde(serialize_with = "super::as_hex")]
pub payload: Vec<u8>,
#[serde(rename = "bds", serialize_with = "serialize_u8_as_hex")]
pub bds_code: u8,
#[deku(skip, default = "decode_payload(&payload, *bds_code)")]
#[serde(serialize_with = "serialize_decode_result")]
pub decoded: Result<DecodedBds, DecodingError>,
#[deku(skip, default = "super::bds::infer_bds(&payload)")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub inferred: Vec<DecodedBds>,
}
impl BdsRecord {
pub fn payload_hex(&self) -> String {
self.payload.iter().map(|b| format!("{:02x}", b)).collect()
}
pub fn bds_string(&self) -> String {
format!("{:02x}", self.bds_code)
}
}
#[derive(Debug, Clone, PartialEq, DekuRead, Serialize)]
pub struct CalculatedPosition {
#[deku(
endian = "big",
map = "|v: i16| -> Result<_, DekuError> { Ok(v as f64 / 128.0) }"
)]
pub x: f64,
#[deku(
endian = "big",
map = "|v: i16| -> Result<_, DekuError> { Ok(v as f64 / 128.0) }"
)]
pub y: f64,
}
#[derive(Debug, Clone, PartialEq, DekuRead, Serialize)]
pub struct TrackVelocity {
#[deku(
endian = "big",
map = "|v: u16| -> Result<_, DekuError> { Ok(v as f64 * 3600.0 / 16384.0) }"
)]
pub ground_speed: f64,
#[deku(
endian = "big",
map = "|v: u16| -> Result<_, DekuError> { Ok(v as f64 * 360.0 / 65536.0) }"
)]
pub heading: f64,
}
#[derive(Debug, Clone, PartialEq, DekuRead, Serialize)]
pub struct RadarPlotCharacteristics {
#[serde(skip)]
indicator: u8,
#[deku(
cond = "*indicator & 0x80 != 0",
map = "|v: Option<u8>| -> Result<_, DekuError> { Ok(v.map(|b| b as f64 * 0.0439453125)) }"
)]
#[serde(skip_serializing_if = "Option::is_none")]
pub srl: Option<f64>,
#[deku(cond = "*indicator & 0x40 != 0")]
#[serde(skip_serializing_if = "Option::is_none")]
pub srr: Option<u8>,
#[deku(
cond = "*indicator & 0x20 != 0",
map = "|v: Option<u8>| -> Result<_, DekuError> { Ok(v.map(|b| b as i8)) }"
)]
#[serde(skip_serializing_if = "Option::is_none")]
pub sam: Option<i8>,
#[deku(
cond = "*indicator & 0x10 != 0",
map = "|v: Option<u8>| -> Result<_, DekuError> { Ok(v.map(|b| b as f64 * 0.0439453125)) }"
)]
#[serde(skip_serializing_if = "Option::is_none")]
pub prl: Option<f64>,
#[deku(
cond = "*indicator & 0x08 != 0",
map = "|v: Option<u8>| -> Result<_, DekuError> { Ok(v.map(|b| b as i8)) }"
)]
#[serde(skip_serializing_if = "Option::is_none")]
pub pam: Option<i8>,
#[deku(
cond = "*indicator & 0x04 != 0",
map = "|v: Option<u8>| -> Result<_, DekuError> { Ok(v.map(|b| b as i8 as f64 * 0.00390625)) }"
)]
#[serde(skip_serializing_if = "Option::is_none")]
pub rpd: Option<f64>,
#[deku(
cond = "*indicator & 0x02 != 0",
map = "|v: Option<u8>| -> Result<_, DekuError> { Ok(v.map(|b| b as i8 as f64 * 0.02197265625)) }"
)]
#[serde(skip_serializing_if = "Option::is_none")]
pub apd: Option<f64>,
#[deku(
cond = "*indicator & 0x01 != 0",
reader = "RadarPlotCharacteristics::skip_fx_tail(deku::reader)"
)]
#[serde(skip)]
_fx_tail: Option<()>,
}
impl RadarPlotCharacteristics {
fn skip_fx_tail<R: std::io::Read + std::io::Seek>(
reader: &mut deku::reader::Reader<R>,
) -> Result<Option<()>, DekuError> {
loop {
let byte = u8::from_reader_with_ctx(reader, ())?;
if byte & 0x01 == 0 {
break;
}
}
Ok(Some(()))
}
}
#[derive(Debug, Clone, PartialEq, DekuRead, Serialize)]
pub struct TrackStatus {
#[deku(
bits = "1",
map = "|cnf: bool| -> Result<_, DekuError> { Ok(!cnf) }"
)]
pub confirmed: bool,
pub sensor_type: SensorType,
#[deku(bits = "1")]
pub doubtful: bool,
#[deku(bits = "1")]
pub manoeuvring: bool,
pub climb_mode: ClimbMode,
#[deku(bits = "1")]
#[serde(skip)]
fx: bool,
#[deku(cond = "*fx")]
#[serde(flatten, skip_serializing_if = "Option::is_none")]
pub extension: Option<TrackStatusExtension>,
}
#[derive(Debug, Clone, PartialEq, DekuRead, Serialize)]
pub struct TrackStatusExtension {
#[deku(bits = "1")]
pub track_end: bool,
#[deku(bits = "1")]
pub ghost: bool,
#[deku(bits = "1")]
pub supplementary: bool,
#[deku(bits = "1", pad_bits_after = "3")]
pub tcc_slant: bool,
#[deku(bits = "1")]
#[serde(skip)]
fx: bool,
#[deku(
cond = "*fx",
reader = "TrackStatusExtension::skip_fx_tail(deku::reader)"
)]
#[serde(skip)]
_fx_tail: Option<()>,
}
impl TrackStatusExtension {
fn skip_fx_tail<R: std::io::Read + std::io::Seek>(
reader: &mut deku::reader::Reader<R>,
) -> Result<Option<()>, DekuError> {
loop {
let byte = u8::from_reader_with_ctx(reader, ())?;
if byte & 0x01 == 0 {
break;
}
}
Ok(Some(()))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, DekuRead, Serialize)]
#[deku(id_type = "u8", bits = "2")]
pub enum SensorType {
#[deku(id = "0")]
Combined,
#[deku(id = "1")]
PsrOnly,
#[deku(id = "2")]
SsrModeS,
#[deku(id = "3")]
Invalid,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, DekuRead, Serialize)]
#[deku(id_type = "u8", bits = "2")]
pub enum ClimbMode {
#[deku(id = "0")]
Maintaining,
#[deku(id = "1")]
Climbing,
#[deku(id = "2")]
Descending,
#[deku(id = "3")]
Unknown,
}
#[derive(Debug, Clone, PartialEq, DekuRead, Serialize)]
pub struct TrackQuality {
#[deku(map = "|v: u8| -> Result<_, DekuError> { Ok(v as f64 / 128.0) }")]
pub sigma_x: f64,
#[deku(map = "|v: u8| -> Result<_, DekuError> { Ok(v as f64 / 128.0) }")]
pub sigma_y: f64,
#[deku(
map = "|v: u8| -> Result<_, DekuError> { Ok(v as f64 * 3600.0 / 16384.0) }"
)]
pub sigma_v: f64,
#[deku(
map = "|v: u8| -> Result<_, DekuError> { Ok(v as f64 * 360.0 / 4096.0) }"
)]
pub sigma_h: f64,
}
#[derive(Debug, Clone, PartialEq, DekuRead, Serialize)]
pub struct WarningError {
#[deku(reader = "Self::read_codes(deku::reader)")]
pub codes: Vec<WarningErrorCode>,
}
impl WarningError {
fn read_codes<R: std::io::Read + std::io::Seek>(
reader: &mut deku::reader::Reader<R>,
) -> Result<Vec<WarningErrorCode>, DekuError> {
let mut codes = Vec::new();
loop {
let entry = WarningErrorEntry::from_reader_with_ctx(reader, ())?;
let has_fx = entry.fx;
codes.push(entry.code);
if !has_fx {
break;
}
}
Ok(codes)
}
}
#[derive(Debug, Clone, PartialEq, DekuRead)]
struct WarningErrorEntry {
code: WarningErrorCode,
#[deku(bits = "1")]
fx: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, DekuRead, Serialize)]
#[deku(id_type = "u8", bits = "7")]
pub enum WarningErrorCode {
#[deku(id = "0")]
NotDefined,
#[deku(id = "1")]
MultipathReply,
#[deku(id = "2")]
SidelobeReply,
#[deku(id = "3")]
SplitPlot,
#[deku(id = "4")]
SecondTimeAround,
#[deku(id = "5")]
Angel,
#[deku(id = "6")]
SlowMovingTarget,
#[deku(id = "7")]
FixedPsrPlot,
#[deku(id = "8")]
SlowPsrTarget,
#[deku(id = "9")]
LowQualityPsrPlot,
#[deku(id = "10")]
PhantomSsrPlot,
#[deku(id = "11")]
NonMatchingMode3A,
#[deku(id = "12")]
AbnormalModeC,
#[deku(id = "13")]
TargetInClutter,
#[deku(id = "14")]
MaxDopplerInZeroFilter,
#[deku(id = "15")]
TransponderAnomaly,
#[deku(id = "16")]
DuplicatedModeS,
#[deku(id = "17")]
ModeSErrorCorrected,
#[deku(id = "18")]
UndecodableModeC,
#[deku(id_pat = "_")]
Unknown,
}
#[derive(Debug, Clone, PartialEq, DekuRead, Serialize)]
pub struct RadialDopplerSpeed {
#[serde(skip)]
indicator: u8,
#[deku(cond = "*indicator & 0x80 != 0")]
#[serde(skip_serializing_if = "Option::is_none")]
pub calculated: Option<CalculatedDopplerSpeed>,
#[deku(
cond = "*indicator & 0x40 != 0",
reader = "RadialDopplerSpeed::read_raw_doppler(deku::reader)"
)]
#[serde(skip_serializing_if = "Option::is_none")]
pub raw: Option<Vec<RawDopplerSpeed>>,
}
impl RadialDopplerSpeed {
fn read_raw_doppler<R: std::io::Read + std::io::Seek>(
reader: &mut deku::reader::Reader<R>,
) -> Result<Option<Vec<RawDopplerSpeed>>, DekuError> {
let rep = u8::from_reader_with_ctx(reader, ())?;
let mut records = Vec::with_capacity(rep as usize);
for _ in 0..rep {
records.push(RawDopplerSpeed::from_reader_with_ctx(reader, ())?);
}
Ok(Some(records))
}
}
#[derive(Debug, Clone, PartialEq, DekuRead, Serialize)]
pub struct CalculatedDopplerSpeed {
#[deku(bits = "1", pad_bits_after = "5")]
pub doubtful: bool,
#[deku(bits = "10")]
pub speed_ms: i16,
}
#[derive(Debug, Clone, PartialEq, DekuRead, Serialize)]
pub struct RawDopplerSpeed {
#[deku(endian = "big")]
pub doppler: u16,
#[deku(endian = "big")]
pub ambiguity: u16,
#[deku(endian = "big")]
pub frequency: u16,
}
#[derive(Debug, Clone, PartialEq, DekuRead, Serialize)]
pub struct AcasResolutionAdvisory {
#[deku(bits = "4")]
pub message_type: u8,
#[deku(bits = "4")]
pub sub_type: u8,
#[deku(bits = "14", endian = "big")]
pub ara: u16,
#[deku(bits = "4")]
pub rac: u8,
#[deku(bits = "1")]
pub ra_terminated: bool,
#[deku(bits = "1")]
pub multiple_threat: bool,
pub threat_type: ThreatType,
#[deku(bits = "26", endian = "big")]
pub threat_id: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, DekuRead, Serialize)]
#[deku(id_type = "u8", bits = "2")]
pub enum ThreatType {
#[deku(id = "0")]
NoIdentity,
#[deku(id = "1")]
ModeS,
#[deku(id = "2")]
AltitudeRangeBearing,
#[deku(id = "3")]
Reserved,
}
impl Cat48Record {
fn read_fspec<R: std::io::Read + std::io::Seek>(
reader: &mut deku::reader::Reader<R>,
) -> Result<Vec<u8>, DekuError> {
let mut fspec = Vec::new();
loop {
let byte = u8::from_reader_with_ctx(reader, ())?;
let has_fx = (byte & 0x01) != 0;
fspec.push(byte);
if !has_fx {
break;
}
}
Ok(fspec)
}
fn read_explicit_field<R: std::io::Read + std::io::Seek>(
reader: &mut deku::reader::Reader<R>,
) -> Result<Option<Vec<u8>>, DekuError> {
let len = u8::from_reader_with_ctx(reader, ())?;
let mut data = vec![len];
for _ in 1..len {
let byte = u8::from_reader_with_ctx(reader, ())?;
data.push(byte);
}
Ok(Some(data))
}
fn has_frn(fspec: &[u8], frn: u8) -> bool {
if frn == 0 {
return false;
}
let byte_idx = ((frn - 1) / 7) as usize;
let bit_pos = 7 - ((frn - 1) % 7);
if byte_idx >= fspec.len() {
return false;
}
(fspec[byte_idx] & (1 << bit_pos)) != 0
}
pub fn sac(&self) -> Option<u8> {
self.data_source_id.as_ref().map(|d| d.sac)
}
pub fn sic(&self) -> Option<u8> {
self.data_source_id.as_ref().map(|d| d.sic)
}
pub fn range_nm(&self) -> Option<f64> {
self.measured_position.as_ref().map(|p| p.rho)
}
pub fn azimuth_deg(&self) -> Option<f64> {
self.measured_position.as_ref().map(|p| p.theta)
}
pub fn callsign(&self) -> Option<&str> {
self.aircraft_id.as_ref().map(|id| id.callsign.as_str())
}
pub fn track_num(&self) -> Option<u16> {
self.track_number.map(|t| t & 0x0FFF)
}
pub fn ground_speed_kt(&self) -> Option<f64> {
self.track_velocity.as_ref().map(|v| v.ground_speed)
}
pub fn heading_deg(&self) -> Option<f64> {
self.track_velocity.as_ref().map(|v| v.heading)
}
pub fn target_type(&self) -> Option<DetectionType> {
self.target_report_descriptor.as_ref().map(|trd| trd.typ)
}
pub fn is_simulated(&self) -> bool {
self.target_report_descriptor
.as_ref()
.map(|trd| trd.sim)
.unwrap_or(false)
}
pub fn has_bds_data(&self) -> bool {
self.mode_s_mb_data
.as_ref()
.map(|mb| mb.count > 0)
.unwrap_or(false)
}
pub fn bds_records(&self) -> Option<&[BdsRecord]> {
self.mode_s_mb_data.as_ref().map(|mb| mb.records.as_slice())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_record_with_bds() {
let data = hex::decode(
"300045fda30301b834010d407fa1003ff612e087a105a0780a89\
0458b984034a980805d9ca2933e00ffe60803a6b300004f650\
c6500030a400004000002040210d00919cc2",
)
.unwrap();
let (_, record) = Cat48Record::from_bytes((&data, 0))
.expect("Failed to parse record");
assert_eq!(record.cat, 48);
assert_eq!(record.length, 69);
assert_eq!(record.fspec, vec![0xFD, 0xA3, 0x03, 0x01, 0xB8]);
assert_eq!(record.sac(), Some(52));
assert_eq!(record.sic(), Some(1));
assert!((record.time_of_day.unwrap() - 6784.992).abs() < 0.001);
let trd = record.target_report_descriptor.as_ref().unwrap();
assert_eq!(trd.typ, DetectionType::ModeSRollCall);
assert!(!trd.sim);
assert_eq!(trd.rdp, 0);
assert!(!trd.spi);
assert!(!trd.rab);
assert!(trd.extension1.is_some());
assert_eq!(record.target_type(), Some(DetectionType::ModeSRollCall));
assert!((record.range_nm().unwrap() - 63.961).abs() < 0.01);
assert!((record.azimuth_deg().unwrap() - 26.543).abs() < 0.01);
assert!(record.mode_3a_code.is_none());
assert_eq!(record.flight_level, Some(36000));
assert_eq!(
format!("{}", record.aircraft_address.as_ref().unwrap()),
"780a89"
);
assert!(record.has_bds_data());
let bds = record.bds_records().unwrap();
assert_eq!(bds.len(), 4);
assert_eq!(bds[0].bds_code, 0x05);
assert_eq!(bds[1].bds_code, 0x60);
assert_eq!(bds[2].bds_code, 0x50);
assert_eq!(bds[3].bds_code, 0x40);
let ts = record.track_status.as_ref().unwrap();
assert!(ts.confirmed);
assert_eq!(ts.sensor_type, SensorType::Combined);
assert!(!ts.doubtful);
assert!(!ts.manoeuvring);
assert_eq!(ts.climb_mode, ClimbMode::Maintaining);
assert!(ts.extension.is_none());
assert_eq!(record.comms_acas, Some(0x0020));
}
#[test]
fn test_parse_record_without_bds() {
let data = hex::decode(
"300024fd830301b834010d40b1a100bf821cb48c1a0640c058c000002040210d00919cc9",
)
.unwrap();
let (_, record) = Cat48Record::from_bytes((&data, 0))
.expect("Failed to parse record without BDS");
assert_eq!(record.cat, 48);
assert_eq!(record.length, 36);
assert_eq!(record.fspec, vec![0xFD, 0x83, 0x03, 0x01, 0xB8]);
assert_eq!(record.sac(), Some(0x34));
assert_eq!(record.sic(), Some(0x01));
assert!((record.time_of_day.unwrap() - 6785.38).abs() < 0.01);
assert!((record.range_nm().unwrap() - 191.508).abs() < 0.01);
assert!((record.azimuth_deg().unwrap() - 40.364).abs() < 0.01);
assert_eq!(record.flight_level, Some(40000));
assert_eq!(
format!("{}", record.aircraft_address.as_ref().unwrap()),
"c058c0"
);
assert!(!record.has_bds_data());
}
#[test]
fn test_parse_croatia_control_sample() {
let data = hex::decode(
"300030fdf70219c9356d4da0c5aff1e00200052\
83c660c10c236d41820\
01c0780031bc000040\
0deb07b9582e41002\
0f5",
)
.unwrap();
let (_, record) = Cat48Record::from_bytes((&data, 0))
.expect("Failed to parse CroatiaControl sample");
assert_eq!(record.cat, 48);
assert_eq!(record.length, 48);
assert_eq!(record.fspec, vec![0xFD, 0xF7, 0x02]);
assert_eq!(record.sac(), Some(25));
assert_eq!(record.sic(), Some(201));
assert!((record.time_of_day.unwrap() - 27354.602).abs() < 0.01);
let trd = record.target_report_descriptor.as_ref().unwrap();
assert_eq!(trd.typ, DetectionType::ModeSRollCall);
assert!(!trd.sim);
assert!(trd.extension1.is_none()); assert_eq!(record.target_type(), Some(DetectionType::ModeSRollCall));
assert!((record.range_nm().unwrap() - 197.684).abs() < 0.01);
assert!((record.azimuth_deg().unwrap() - 340.137).abs() < 0.01);
assert_eq!(record.mode_3a_code, Some("1000".to_string()));
assert_eq!(record.flight_level, Some(33000));
assert_eq!(
format!("{}", record.aircraft_address.as_ref().unwrap()),
"3c660c"
);
assert!(record.aircraft_id.is_some());
assert!(record.has_bds_data());
let bds = record.bds_records().unwrap();
assert_eq!(bds.len(), 1);
assert_eq!(bds[0].bds_code, 0x40);
assert_eq!(bds[0].payload_hex(), "c0780031bc0000");
assert_eq!(record.track_num(), Some(3563));
assert!((record.ground_speed_kt().unwrap() - 434.4).abs() < 1.0);
assert!((record.heading_deg().unwrap() - 124.0).abs() < 0.1);
let ts = record.track_status.as_ref().unwrap();
assert!(ts.confirmed); assert_eq!(ts.sensor_type, SensorType::SsrModeS); assert!(!ts.doubtful);
assert!(!ts.manoeuvring);
assert_eq!(ts.climb_mode, ClimbMode::Maintaining); let ext = ts.extension.as_ref().unwrap();
assert!(!ext.track_end); assert!(!ext.ghost); assert!(!ext.supplementary); assert!(!ext.tcc_slant);
assert_eq!(record.comms_acas, Some(0x20F5));
}
#[test]
fn test_json_serialization() {
let data = hex::decode(
"300030fdf70219c9356d4da0c5aff1e00200052\
83c660c10c236d41820\
01c0780031bc000040\
0deb07b9582e41002\
0f5",
)
.unwrap();
let (_, record) = Cat48Record::from_bytes((&data, 0))
.expect("Failed to parse CroatiaControl sample");
let json = serde_json::to_string_pretty(&record).unwrap();
println!("{}", json);
let json_value: serde_json::Value =
serde_json::from_str(&json).unwrap();
let mp = &json_value["measured_position"];
assert!((mp["rho"].as_f64().unwrap() - 197.684).abs() < 0.01);
assert!((mp["theta"].as_f64().unwrap() - 340.137).abs() < 0.01);
let tv = &json_value["track_velocity"];
assert!((tv["ground_speed"].as_f64().unwrap() - 434.4).abs() < 1.0);
assert!((tv["heading"].as_f64().unwrap() - 124.0).abs() < 0.1);
let ts = &json_value["track_status"];
assert!(ts["confirmed"].as_bool().unwrap()); assert_eq!(ts["sensor_type"].as_str().unwrap(), "SsrModeS");
assert_eq!(json_value["icao24"].as_str().unwrap(), "3c660c");
assert!(json_value["callsign"].as_str().is_some());
}
#[test]
fn test_trd_extension2_parsing() {
let data = hex::decode("300009a0000121016c").unwrap();
let (_, record) = Cat48Record::from_bytes((&data, 0))
.expect("Failed to parse CAT48 with Extension 2");
assert_eq!(record.sac(), Some(0x00));
assert_eq!(record.sic(), Some(0x01));
assert_eq!(
record.target_report_descriptor.as_ref().unwrap().typ,
DetectionType::SinglePsr
);
let ext1 = record
.target_report_descriptor
.as_ref()
.unwrap()
.extension1
.as_ref();
assert!(ext1.is_some(), "Extension 1 should be present");
let ext2 = ext1.unwrap().extension2.as_ref();
assert!(ext2.is_some(), "Extension 2 should be present");
let ext2_data = ext2.unwrap();
assert!(!ext2_data.adsb.populated);
assert!(ext2_data.adsb.available);
assert!(ext2_data.scn.populated);
assert!(!ext2_data.scn.available);
assert!(ext2_data.pai.populated);
assert!(ext2_data.pai.available);
}
}