use crate::crc;
use crate::error::{AdsbError, FrameError};
use crate::message::AdsbMessage;
const DF17: u8 = 17;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Frame {
bytes: [u8; 14],
}
impl Frame {
pub fn parse(line: &str) -> Result<Self, FrameError> {
let line = line.trim();
let hex = line
.strip_prefix('*')
.and_then(|s| s.strip_suffix(';'))
.unwrap_or(line);
if hex.len() != 28 {
return Err(FrameError::WrongLength {
expected: 28,
actual: hex.len(),
});
}
let mut bytes = [0u8; 14];
for (i, byte) in bytes.iter_mut().enumerate() {
let pair = hex.get(i * 2..i * 2 + 2).ok_or(FrameError::InvalidHex)?;
*byte = u8::from_str_radix(pair, 16).map_err(|_| FrameError::InvalidHex)?;
}
let df = bytes[0] >> 3;
if df != DF17 {
return Err(FrameError::UnsupportedDownlinkFormat(df));
}
if !crc::verify(&bytes) {
return Err(FrameError::ChecksumMismatch);
}
Ok(Self { bytes })
}
#[must_use]
pub fn from_data(mut bytes: [u8; 14]) -> Self {
crc::write_parity(&mut bytes);
Self { bytes }
}
#[must_use]
pub const fn capability(&self) -> u8 {
self.bytes[0] & 0b111
}
#[must_use]
pub const fn icao_address(&self) -> u32 {
(self.bytes[1] as u32) << 16 | (self.bytes[2] as u32) << 8 | self.bytes[3] as u32
}
#[must_use]
pub const fn me(&self) -> &[u8] {
self.bytes.split_at(4).1.split_at(7).0
}
#[must_use]
pub const fn bytes(&self) -> &[u8; 14] {
&self.bytes
}
pub fn decode_message(&self) -> Result<AdsbMessage, AdsbError> {
Ok(AdsbMessage::decode(self.me())?)
}
#[must_use]
pub fn write_hex<'b>(&self, buf: &'b mut [u8; 28]) -> &'b str {
const HEX_DIGITS: &[u8; 16] = b"0123456789ABCDEF";
for (i, &byte) in self.bytes.iter().enumerate() {
buf[i * 2] = HEX_DIGITS[(byte >> 4) as usize];
buf[i * 2 + 1] = HEX_DIGITS[(byte & 0x0F) as usize];
}
core::str::from_utf8(buf).unwrap_or_default()
}
}
#[cfg(test)]
mod tests {
extern crate std;
use core::fmt::Write as _;
use std::format;
use std::string::String;
use super::*;
const GOOD: &str = "8D406B902015A678D4D220AA4BDA";
#[test]
fn parses_bare_hex() {
let f = Frame::parse(GOOD).unwrap();
assert_eq!(f.icao_address(), 0x0040_6B90);
assert_eq!(f.me().len(), 7);
}
#[test]
fn parses_dump1090_framing() {
let line = alloc_wrapped(GOOD);
let f = Frame::parse(&line).unwrap();
assert_eq!(f.icao_address(), 0x0040_6B90);
}
fn alloc_wrapped(hex: &str) -> String {
format!("*{hex};")
}
#[test]
fn rejects_wrong_length() {
assert_eq!(
Frame::parse("8D40").unwrap_err(),
FrameError::WrongLength {
expected: 28,
actual: 4
}
);
}
#[test]
fn rejects_bad_checksum() {
let mut bad = String::from(GOOD);
bad.replace_range(4..5, "0");
assert_eq!(
Frame::parse(&bad).unwrap_err(),
FrameError::ChecksumMismatch
);
}
#[test]
fn rejects_non_df17() {
let mut frame = [0u8; 14];
frame[0] = 11 << 3;
crc::write_parity(&mut frame);
let mut hex = String::new();
for b in frame {
write!(hex, "{b:02X}").unwrap();
}
assert_eq!(
Frame::parse(&hex).unwrap_err(),
FrameError::UnsupportedDownlinkFormat(11)
);
}
#[test]
fn from_data_computes_matching_parity() {
let mut bytes = [0u8; 14];
bytes[0] = DF17 << 3;
let f = Frame::from_data(bytes);
assert!(crc::verify(f.bytes()));
}
#[test]
fn write_hex_round_trips_through_parse() {
let original = Frame::parse(GOOD).unwrap();
let mut buf = [0u8; 28];
let hex = original.write_hex(&mut buf);
assert_eq!(hex, GOOD);
let reparsed = Frame::parse(hex).unwrap();
assert_eq!(reparsed, original);
}
}
#[cfg(test)]
mod proptests {
extern crate std;
use proptest::prelude::*;
use std::string::String;
use super::Frame;
proptest! {
#[test]
fn parse_never_panics(s: String) {
let _ = Frame::parse(&s);
}
}
}