use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum SourceClass {
Iq,
Events,
Frames,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum Bearer {
#[serde(alias = "acars", alias = "vhf")]
Vhf,
#[serde(alias = "vdl", alias = "vdl2")]
Vdl2,
#[serde(alias = "hf", alias = "hfdl")]
Hfdl,
Decoded,
#[serde(other)]
#[default]
Unknown,
}
impl Bearer {
pub fn as_str(&self) -> &'static str {
match self {
Bearer::Vhf => "vhf",
Bearer::Vdl2 => "vdl2",
Bearer::Hfdl => "hfdl",
Bearer::Decoded => "decoded",
Bearer::Unknown => "unknown",
}
}
}
impl std::fmt::Display for Bearer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DecodedEvent {
pub event: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub timestamp: Option<f64>,
pub bearer: Bearer,
pub source: SourceMetadata,
#[serde(skip_serializing_if = "Option::is_none")]
pub receiver: Option<ReceiverMetadata>,
#[serde(skip_serializing_if = "Option::is_none")]
pub aircraft: Option<Aircraft>,
#[serde(skip_serializing_if = "Option::is_none")]
pub kinematics: Option<acars::decode::compact::Kinematics>,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_frame_hex: Option<String>,
pub message: ProtocolMessage,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", content = "data", rename_all = "snake_case")]
pub enum ProtocolMessage {
Airframes(Box<AirframesMessage>),
Avlc(Box<acars::decode::avlc::AvlcFrame>),
Acars(Box<acars::decode::acars::AcarsMessage>),
Hfdl(Box<acars::decode::hfdl::HfdlMessage>),
App(Box<acars::decode::payload::AcarsAppPayload>),
}
impl ProtocolMessage {
pub fn kinematics(&self) -> Option<acars::decode::compact::Kinematics> {
use acars::decode::compact::ExtractKinematics;
match self {
Self::Airframes(msg) => {
if let Some(app) = &msg.app {
app.kinematics()
} else {
msg.payload.kinematics()
}
}
Self::Avlc(frame) => frame.kinematics(),
Self::Acars(msg) => msg.kinematics(),
Self::Hfdl(msg) => msg.kinematics(),
Self::App(app) => app.kinematics(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Aircraft {
#[serde(skip_serializing_if = "Option::is_none")]
pub icao24: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub aircraft_id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub registration: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SourceMetadata {
pub id: String,
pub name: String,
pub class: SourceClass,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReceiverMetadata {
pub bearer: Bearer,
#[serde(skip_serializing_if = "Option::is_none")]
pub channel_hz: Option<u32>,
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct AirframesPayload {
pub label: Option<String>,
pub text: Option<String>,
pub from_hex: Option<String>,
pub to_hex: Option<String>,
pub latitude: Option<f64>,
pub longitude: Option<f64>,
pub altitude: Option<f64>,
pub track: Option<f64>,
#[serde(default)]
pub source_type: Bearer,
#[serde(deserialize_with = "deserialize_timestamp", default)]
pub timestamp: Option<f64>,
#[serde(deserialize_with = "deserialize_timestamp", default)]
pub created_at: Option<f64>,
pub frequency: Option<f64>,
pub id: Option<String>,
pub airframe_id: Option<u64>,
pub flight_id: Option<u64>,
pub tail: Option<String>,
pub link_direction: Option<String>,
pub airframe: Option<AirframesAirframe>,
pub flight: Option<AirframesFlight>,
}
impl AirframesPayload {
pub fn kinematics(&self) -> Option<acars::decode::compact::Kinematics> {
let payload_kinematics = kinematics_from_airframes_position(
self.latitude,
self.longitude,
self.altitude,
self.track,
"airframes_payload",
);
let flight_kinematics = self.flight.as_ref().and_then(|flight| {
kinematics_from_airframes_position(
flight.latitude,
flight.longitude,
flight.altitude,
flight.track,
"airframes_flight",
)
});
match (payload_kinematics, flight_kinematics) {
(Some(payload), Some(flight)) => Some(payload.merge(flight)),
(Some(payload), None) => Some(payload),
(None, Some(flight)) => Some(flight),
(None, None) => None,
}
}
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct AirframesAirframe {
pub icao: Option<String>,
pub tail: Option<String>,
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct AirframesFlight {
pub latitude: Option<f64>,
pub longitude: Option<f64>,
pub altitude: Option<f64>,
pub track: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AirframesMessage {
pub payload: AirframesPayload,
#[serde(skip_serializing_if = "Option::is_none")]
pub src: Option<AirframesAddr>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dst: Option<AirframesAddr>,
#[serde(skip_serializing_if = "Option::is_none")]
pub app: Option<acars::decode::payload::AcarsAppPayload>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AirframesAddr {
pub icao24: String,
pub addr_type: AirframesAddrType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AirframesAddrType {
Aircraft,
GroundStation,
Unknown,
}
fn kinematics_from_airframes_position(
latitude: Option<f64>,
longitude: Option<f64>,
altitude: Option<f64>,
track: Option<f64>,
derived_from: &str,
) -> Option<acars::decode::compact::Kinematics> {
let (Some(latitude), Some(longitude)) = (latitude, longitude) else {
return None;
};
if latitude == 0.0 && longitude == 0.0 {
return None;
}
Some(acars::decode::compact::Kinematics {
position: Some(acars::decode::compact::Position {
latitude,
longitude,
}),
altitude_ft: normalize_airframes_altitude(altitude),
track, ground_speed_knots: None,
derived_from: Some(derived_from.to_string()),
})
}
fn normalize_airframes_altitude(altitude: Option<f64>) -> Option<i32> {
altitude.and_then(|value| {
let rounded = value.round();
((i32::MIN as f64)..=(i32::MAX as f64))
.contains(&rounded)
.then_some(rounded as i32)
})
}
fn deserialize_timestamp<'de, D>(deserializer: D) -> Result<Option<f64>, D::Error>
where
D: serde::Deserializer<'de>,
{
struct TsVisitor;
impl<'de> serde::de::Visitor<'de> for TsVisitor {
type Value = Option<f64>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a float or a timestamp string")
}
fn visit_f64<E: serde::de::Error>(self, value: f64) -> Result<Self::Value, E> {
Ok(Some(value))
}
fn visit_u64<E: serde::de::Error>(self, value: u64) -> Result<Self::Value, E> {
Ok(Some(value as f64))
}
fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
if let Ok(n) = value.parse::<f64>() {
Ok(Some(n))
} else if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(value) {
Ok(Some(dt.timestamp_micros() as f64 / 1_000_000.0))
} else {
Ok(None)
}
}
fn visit_none<E: serde::de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_some<D2: serde::Deserializer<'de>>(
self,
deserializer: D2,
) -> Result<Self::Value, D2::Error> {
deserializer.deserialize_any(self)
}
}
deserializer.deserialize_any(TsVisitor)
}