#![allow(clippy::suspicious_else_formatting)]
use deku::prelude::*;
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, DekuRead)]
pub struct TargetStateAndStatusInformation {
#[deku(bits = "2")]
#[serde(skip)]
pub subtype: u8,
#[deku(pad_bits_before = "1")]
#[serde(rename = "source")]
pub alt_source: AltSource,
#[deku(
bits = "11",
endian = "big",
map = "|altitude: u16| -> Result<_, DekuError> {
Ok(
if altitude > 1 {Some(((altitude - 1) * 32 + 16) / 100 * 100)}
else {None}
)
}"
)]
#[serde(skip_serializing_if = "Option::is_none")]
pub selected_altitude: Option<u16>,
#[deku(
bits = "9",
endian = "big",
map = "|qnh: u32| -> Result<_, DekuError> {
if qnh == 0 { Ok(None) }
else { Ok(Some(800.0 + ((qnh - 1) as f32) * 0.8)) }
}"
)]
#[serde(skip_serializing_if = "Option::is_none")]
pub barometric_setting: Option<f32>,
#[deku(bits = "1")]
#[serde(skip)]
pub heading_status: bool,
#[deku(
bits = "9",
endian = "big",
map = "|heading: u16| -> Result<_, DekuError> {
if *heading_status {Ok(Some(heading as f32 * 180.0 / 256.0))}
else {Ok(None)}
}"
)]
#[serde(skip_serializing_if = "Option::is_none")]
pub selected_heading: Option<f32>,
#[deku(bits = "4")]
#[serde(rename = "NACp")]
pub nac_p: u8,
#[deku(bits = "1")]
#[serde(skip)]
pub nic_baro: bool,
#[deku(bits = "2")]
#[serde(skip)]
pub sil: u8,
#[deku(bits = "1")]
#[serde(skip)]
pub mode_status: bool,
#[deku(
bits = "1",
map = "|val: bool| -> Result<_, DekuError> {
if *mode_status {Ok(Some(val))} else {Ok(None)}
}"
)]
#[serde(skip_serializing_if = "Option::is_none")]
pub autopilot: Option<bool>,
#[deku(
bits = "1",
map = "|val: bool| -> Result<_, DekuError> {
if *mode_status {Ok(Some(val))} else {Ok(None)}
}"
)]
#[serde(skip_serializing_if = "Option::is_none")]
pub vnav_mode: Option<bool>,
#[deku(
bits = "1",
map = "|val: bool| -> Result<_, DekuError> {
if *mode_status {Ok(Some(val))} else {Ok(None)}
}"
)]
#[serde(skip_serializing_if = "Option::is_none")]
pub alt_hold: Option<bool>,
#[deku(bits = "1")]
#[serde(skip)]
pub imf: bool,
#[deku(
bits = "1",
map = "|val: bool| -> Result<_, DekuError> {
if *mode_status {Ok(Some(val))} else {Ok(None)}
}"
)]
#[serde(skip_serializing_if = "Option::is_none")]
pub approach_mode: Option<bool>,
#[deku(bits = "1")]
pub tcas_operational: bool,
#[deku(
bits = "1",
map = "|val: bool| -> Result<_, DekuError> {
if *mode_status {Ok(Some(val))} else {Ok(None)}
}"
)]
#[serde(skip_serializing_if = "Option::is_none")]
#[deku(pad_bits_after = "2")]
pub lnav_mode: Option<bool>,
}
impl fmt::Display for TargetStateAndStatusInformation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, " Target state and status (BDS 6,2)")?;
if let Some(sel_alt) = &self.selected_altitude {
writeln!(
f,
" Selected alt: {} ft {}",
sel_alt, &self.alt_source
)?;
}
if let Some(sel_hdg) = &self.selected_heading {
writeln!(f, " Selected hdg: {sel_hdg:.1}°")?;
}
if let Some(qnh) = &self.barometric_setting {
writeln!(f, " QNH: {qnh:.1} mbar")?;
}
if self.mode_status {
write!(f, " Mode: ")?;
if let Some(value) = self.autopilot {
if value {
write!(f, " autopilot")?;
}
}
if let Some(value) = self.vnav_mode {
if value {
write!(f, " VNAV")?;
}
}
if let Some(value) = self.lnav_mode {
if value {
write!(f, " LNAV")?;
}
}
if let Some(value) = self.alt_hold {
if value {
write!(f, " alt_hold")?;
}
}
if let Some(value) = self.approach_mode {
if value {
write!(f, " approach")?;
}
}
writeln!(f)?;
}
writeln!(
f,
" TCAS: {}",
if self.tcas_operational { "on" } else { "off" }
)
}
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, DekuRead)]
#[deku(id_type = "u8", bits = "1")]
pub enum AltSource {
#[deku(id = "0")]
#[serde(rename = "MCP/FCU")]
MCP,
#[deku(id = "1")]
FMS,
}
impl fmt::Display for AltSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self {
Self::MCP => write!(f, "MCP/FCU"),
Self::FMS => write!(f, "FMS"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::prelude::*;
use approx::assert_relative_eq;
use hexlit::hex;
#[test]
fn test_surface_position() {
let bytes = hex!("8DA05629EA21485CBF3F8CADAEEB");
let (_, msg) = Message::from_bytes((&bytes, 0)).unwrap();
if let ExtendedSquitterADSB(adsb_msg) = msg.df {
if let ME::BDS62(TargetStateAndStatusInformation {
selected_altitude,
alt_source,
barometric_setting,
selected_heading,
mode_status,
autopilot,
vnav_mode,
lnav_mode,
alt_hold,
approach_mode,
tcas_operational,
..
}) = adsb_msg.message
{
assert_eq!(selected_altitude, Some(17000));
assert_eq!(alt_source, AltSource::MCP);
assert_eq!(barometric_setting, Some(1012.8));
assert_relative_eq!(
selected_heading.unwrap(),
66.8,
max_relative = 1e-2
);
assert!(mode_status);
assert_eq!(autopilot, Some(true));
assert_eq!(vnav_mode, Some(true));
assert_eq!(lnav_mode, Some(true));
assert_eq!(alt_hold, Some(false));
assert_eq!(approach_mode, Some(false));
assert!(tcas_operational);
}
return;
}
unreachable!();
}
#[test]
fn test_format_groundspeed() {
let bytes = hex!("8DA05629EA21485CBF3F8CADAEEB");
let (_, msg) = Message::from_bytes((&bytes, 0)).unwrap();
assert_eq!(
format!("{msg}"),
r#" DF17. Extended Squitter
Address: a05629
Air/Ground: airborne
Target state and status (BDS 6,2)
Selected alt: 17000 ft MCP/FCU
Selected hdg: 66.8°
QNH: 1012.8 mbar
Mode: autopilot VNAV LNAV
TCAS: on
"#
)
}
}