use deku::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Serialize, Deserialize, DekuRead, Clone)]
#[serde(tag = "bds", rename = "50")]
pub struct TrackAndTurnReport {
#[deku(reader = "read_roll(deku::reader)")]
#[serde(rename = "roll")]
pub roll_angle: Option<f64>,
#[deku(reader = "read_track(deku::reader)")]
#[serde(rename = "track")]
pub track_angle: Option<f64>,
#[deku(reader = "read_groundspeed(deku::reader)")]
pub groundspeed: Option<u16>,
#[deku(reader = "read_rate(deku::reader)")]
pub track_rate: Option<f64>,
#[deku(reader = "read_tas(deku::reader)")]
#[serde(rename = "TAS")]
pub true_airspeed: Option<u16>,
}
fn read_roll<R: deku::no_std_io::Read + deku::no_std_io::Seek>(
reader: &mut Reader<R>,
) -> Result<Option<f64>, DekuError> {
let status = bool::from_reader_with_ctx(
reader,
(deku::ctx::Endian::Big, deku::ctx::BitSize(1)),
)?;
let sign = u8::from_reader_with_ctx(
reader,
(deku::ctx::Endian::Big, deku::ctx::BitSize(1)),
)?;
let value = u16::from_reader_with_ctx(
reader,
(deku::ctx::Endian::Big, deku::ctx::BitSize(9)),
)?;
if !status {
if (sign != 0) | (value != 0) {
return Err(DekuError::Assertion(
"Non-null value with invalid status: roll angle".into(),
));
} else {
return Ok(None);
}
}
let roll = if sign > 0 {
(value as f64 - 512.) * 45. / 256.
} else {
value as f64 * 45. / 256.
};
#[cfg(feature = "bds-infer")]
if roll.abs() > 45. {
return Err(DekuError::Assertion(
format!("Roll angle: abs({roll}) > 45").into(),
));
}
Ok(Some(roll))
}
fn read_track<R: deku::no_std_io::Read + deku::no_std_io::Seek>(
reader: &mut Reader<R>,
) -> Result<Option<f64>, DekuError> {
let status = bool::from_reader_with_ctx(
reader,
(deku::ctx::Endian::Big, deku::ctx::BitSize(1)),
)?;
let sign = u8::from_reader_with_ctx(
reader,
(deku::ctx::Endian::Big, deku::ctx::BitSize(1)),
)?;
let value = u16::from_reader_with_ctx(
reader,
(deku::ctx::Endian::Big, deku::ctx::BitSize(10)),
)?;
if !status {
if (sign != 0) | (value != 0) {
return Err(DekuError::Assertion(
"Non-null value with invalid status: track angle".into(),
));
} else {
return Ok(None);
}
}
let value = if sign == 1 {
value as i16 - 1024
} else {
value as i16
};
let mut track = value as f64 * 90. / 512.;
if track < 0. {
track += 360.
}
Ok(Some(track))
}
fn read_groundspeed<R: deku::no_std_io::Read + deku::no_std_io::Seek>(
reader: &mut Reader<R>,
) -> Result<Option<u16>, DekuError> {
let status = bool::from_reader_with_ctx(
reader,
(deku::ctx::Endian::Big, deku::ctx::BitSize(1)),
)?;
let value = u16::from_reader_with_ctx(
reader,
(deku::ctx::Endian::Big, deku::ctx::BitSize(10)),
)?;
if !status {
if value != 0 {
return Err(DekuError::Assertion(
"Non-null value with invalid status: groundspeed".into(),
));
} else {
return Ok(None);
}
}
let gs = value * 2;
#[cfg(feature = "bds-infer")]
if gs > 700 {
return Err(DekuError::Assertion(
format!("Groundspeed value: {gs} > 700").into(),
));
}
Ok(Some(gs))
}
fn read_rate<R: deku::no_std_io::Read + deku::no_std_io::Seek>(
reader: &mut Reader<R>,
) -> Result<Option<f64>, DekuError> {
let status = bool::from_reader_with_ctx(
reader,
(deku::ctx::Endian::Big, deku::ctx::BitSize(1)),
)?;
let sign = u8::from_reader_with_ctx(
reader,
(deku::ctx::Endian::Big, deku::ctx::BitSize(1)),
)?;
let value = u16::from_reader_with_ctx(
reader,
(deku::ctx::Endian::Big, deku::ctx::BitSize(9)),
)?;
if !status {
if (sign != 0) | (value != 0) {
return Err(DekuError::Assertion(
"Non-null value with invalid status: track rate".into(),
));
} else {
return Ok(None);
}
}
if value == 0b111111111 {
return Ok(None);
}
let value = if sign == 1 {
value as i16 - 512
} else {
value as i16
};
let rate = value as f64 * 8. / 256.;
Ok(Some(rate))
}
fn read_tas<R: deku::no_std_io::Read + deku::no_std_io::Seek>(
reader: &mut Reader<R>,
) -> Result<Option<u16>, DekuError> {
let status = bool::from_reader_with_ctx(
reader,
(deku::ctx::Endian::Big, deku::ctx::BitSize(1)),
)?;
let value = u16::from_reader_with_ctx(
reader,
(deku::ctx::Endian::Big, deku::ctx::BitSize(10)),
)?;
if !status {
if value != 0 {
return Err(DekuError::Assertion(
"Non-null value with invalid status: true air speed".into(),
));
} else {
return Ok(None);
}
}
let tas = value * 2;
#[cfg(feature = "bds-infer")]
if !(80..=600).contains(&tas) {
return Err(DekuError::Assertion(
format!("TAS value: {tas} outside [80, 600]").into(),
));
}
Ok(Some(tas))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::prelude::*;
use approx::assert_relative_eq;
use hexlit::hex;
#[test]
fn test_valid_bds50() {
let bytes = hex!("a000139381951536e024d4ccf6b5");
let (_, msg) = Message::from_bytes((&bytes, 0)).unwrap();
if let CommBAltitudeReply { bds, .. } = msg.df {
let TrackAndTurnReport {
roll_angle,
track_angle,
true_airspeed,
groundspeed,
track_rate,
} = bds.bds50.unwrap();
assert_relative_eq!(roll_angle.unwrap(), 2.1, max_relative = 1e-2);
assert_relative_eq!(
track_angle.unwrap(),
114.258,
max_relative = 1e-3
);
assert_eq!(groundspeed.unwrap(), 438);
assert_eq!(true_airspeed.unwrap(), 424);
assert_relative_eq!(
track_rate.unwrap(),
0.125,
max_relative = 1e-3
);
} else {
unreachable!();
}
}
#[test]
fn test_invalid_bds50() {
let bytes = hex!("a0000638fa81c10000000081a92f");
let (_, msg) = Message::from_bytes((&bytes, 0)).unwrap();
if let CommBAltitudeReply { bds, .. } = msg.df {
assert_eq!(bds.bds50, None);
} else {
unreachable!();
}
}
}