#![doc = include_str!("../README.md")]
#![cfg_attr(not(feature = "std"), no_std)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc(html_logo_url = "https://raw.githubusercontent.com/rsadsb/adsb_deku/master/media/logo.png")]
extern crate alloc;
#[cfg(feature = "alloc")]
use alloc::{collections::BTreeMap, fmt, string::String, vec, vec::Vec};
#[cfg(feature = "alloc")]
use core::{
clone::Clone, default::Default, fmt::Debug, marker::Copy, prelude::rust_2021::derive,
result::Result::Ok, writeln,
};
#[cfg(feature = "std")]
use std::time::SystemTime;
use adsb_deku::adsb::{AirborneVelocity, Identification, ME};
use adsb_deku::{cpr, Altitude, CPRFormat, Frame, DF, ICAO};
use tracing::{debug, info, warn};
const MAX_AIRCRAFT_DISTANCE: f64 = 100.0;
#[derive(Debug, PartialEq, Eq)]
pub enum Added {
No,
Yes,
}
impl From<bool> for Added {
fn from(other: bool) -> Self {
match other {
true => Self::Yes,
false => Self::No,
}
}
}
#[cfg_attr(feature = "serde", serde_with::serde_as)]
#[derive(Debug, Default, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Airplanes(
#[cfg_attr(
feature = "serde",
serde(with = "serde_with::As::<Vec<(serde_with::DisplayFromStr, serde_with::Same)>>")
)]
BTreeMap<ICAO, AirplaneState>,
);
impl fmt::Display for Airplanes {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for key in self.0.keys() {
let value = self.aircraft_details(*key);
if let Some(value) = value {
writeln!(f, "{key}: {value:?}")?;
}
}
Ok(())
}
}
// public
impl Airplanes {
#[must_use]
pub fn new() -> Self {
Self(BTreeMap::new())
}
/// Tuple `iter()` of all `(ICAO, AirplanesState)`
///
/// equivalent [`BTreeMap::iter`]
pub fn iter(&self) -> alloc::collections::btree_map::Iter<'_, ICAO, AirplaneState> {
self.0.iter()
}
/// Get all `ICAO` keys
///
/// equivalent [`BTreeMap::keys`]
pub fn keys(&self) -> alloc::collections::btree_map::Keys<'_, ICAO, AirplaneState> {
self.0.keys()
}
/// From `ICAO`, get `AirplaneState`
///
/// equivalent [`BTreeMap::get`]
#[must_use]
pub fn get(&self, key: ICAO) -> Option<&AirplaneState> {
self.0.get(&key)
}
/// Amount of currently tracked airplanes
///
/// equivalent [`BTreeMap::len`]
#[must_use]
pub fn len(&self) -> usize {
self.0.len()
}
/// equivalent [`BTreeMap::is_empty`]
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
/// Update `Airplanes` with new `Frame`
///
/// Take parsed `Frame` and read the `DF::ADSB` type and act upon the parsed message. This
/// updates the field that the `ME` value equates to within [`Self`]. This also adds
/// airplanes (`ICAO` and `AirplaneState`) when a new aircraft is detected.
///
/// `lat_long`: (latitude, longitude) of current receiver location
///
/// `max_range`: max range of the receiver
///
/// Return true if entry was added into `Airplanes`
pub fn action(&mut self, frame: Frame, lat_long: (f64, f64), max_rang: f64) -> Added {
let mut airplane_added = Added::No;
match frame.df {
DF::ADSB(ref adsb) => {
airplane_added = match &adsb.me {
ME::AircraftIdentification { id: _id, identification } => {
self.add_identification(adsb.icao, identification)
}
ME::AirborneVelocity(vel) => self.add_airborne_velocity(adsb.icao, vel),
ME::AirbornePositionGNSSAltitude { id: _id, altitude }
| ME::AirbornePositionBaroAltitude { id: _id, altitude } => {
self.update_position(adsb.icao, altitude, lat_long, max_rang)
}
_ => Added::No,
};
let incr_airplane_added = self.incr_messages(adsb.icao);
airplane_added =
if incr_airplane_added == Added::Yes || airplane_added == Added::Yes {
Added::Yes
} else {
Added::No
};
}
DF::TisB { cf, pi } => {
info!("TISB: {cf:?}, {pi:?}");
airplane_added = match cf.me {
ME::AircraftIdentification { id: _id, identification } => {
self.add_identification(pi, &identification)
}
ME::AirborneVelocity(vel) => self.add_airborne_velocity(pi, &vel),
ME::AirbornePositionGNSSAltitude { id: _id, altitude }
| ME::AirbornePositionBaroAltitude { id: _id, altitude } => {
self.update_position(pi, &altitude, lat_long, max_rang)
}
_ => Added::No,
};
let incr_airplane_added = self.incr_messages(pi);
airplane_added =
if incr_airplane_added == Added::Yes || airplane_added == Added::Yes {
Added::Yes
} else {
Added::No
};
}
_ => (),
}
airplane_added
}
/// from `ICAO` return details on that airplane
///
/// position, altitude, and `kilo_distance` are required to be set to Some(value) in order for
/// this function to return any values from that `ICAO`. Other values from that `ICAO` are
/// optional and can be None. See [`AirplaneDetails`] for all the values this function returns.
#[must_use]
pub fn aircraft_details(&self, icao: ICAO) -> Option<AirplaneDetails> {
match self.get(icao) {
Some(airplane_state) => {
let track = &airplane_state.track;
let coor = &airplane_state.coords;
if let (Some(position), Some(altitude), Some(kilo_distance)) =
(&coor.position, coor.altitude(), coor.kilo_distance)
{
Some(AirplaneDetails {
position: *position,
altitude,
kilo_distance,
heading: airplane_state.heading,
track: track.clone(),
})
} else {
None
}
}
None => None,
}
}
/// Return all aircraft that currently have a [`cpr::Position`]
#[must_use]
pub fn all_position(&self) -> Vec<(ICAO, cpr::Position)> {
let mut all_lat_long = vec![];
for (key, airplane_state) in self.iter() {
let coor = &airplane_state.coords;
if let Some(position) = &coor.position {
all_lat_long.push((*key, *position));
}
}
all_lat_long
}
/// Remove airplanes that have not been seen since `filter_time` seconds
#[cfg(feature = "std")]
pub fn prune(&mut self, filter_time: u64) {
self.0.retain(|k, v| {
if let Ok(time) = v.last_time.elapsed() {
if time < std::time::Duration::from_secs(filter_time) {
true
} else {
info!("[{k}] non-active, removing");
false
}
} else {
info!("[{k}] non-active(time error), removing");
false
}
});
}
}
// private
impl Airplanes {
// Return (matching state from icao, true if airplane added)
fn entry_or_insert(&mut self, icao: ICAO) -> (&mut AirplaneState, Added) {
let entry = self.0.entry(icao);
let airplane_added =
Added::from(matches!(entry, alloc::collections::btree_map::Entry::Vacant(_)));
if Added::Yes == airplane_added {
info!("[{icao}] now tracking");
}
(entry.or_default(), airplane_added)
}
/// Increment message count of `ICAO`. If feature: `std`, set `last_time` to current time.
///
/// Return true if entry was added into `Airplanes`
pub fn incr_messages(&mut self, icao: ICAO) -> Added {
let (state, airplane_added) = self.entry_or_insert(icao);
state.num_messages += 1;
#[cfg(feature = "std")]
{
state.last_time = std::time::SystemTime::now();
}
airplane_added
}
/// update from `ME::AircraftIdentification`
///
/// Return true if entry was added into `Airplanes`
fn add_identification(&mut self, icao: ICAO, identification: &Identification) -> Added {
let (state, airplane_added) = self.entry_or_insert(icao);
state.callsign = Some(identification.cn.clone());
info!("[{icao}] with identification: {}", identification.cn);
airplane_added
}
/// update from `ME::AirborneVelocity`
///
/// Return true if entry was added into `Airplanes`
fn add_airborne_velocity(&mut self, icao: ICAO, vel: &AirborneVelocity) -> Added {
let (state, airplane_added) = self.entry_or_insert(icao);
if let Some((heading, ground_speed, vert_speed)) = vel.calculate() {
info!("[{icao}] with airborne velocity: heading: {heading}, speed: {ground_speed}, vertical speed: {vert_speed}");
state.heading = Some(heading);
state.speed = Some(ground_speed as f32);
state.vert_speed = Some(vert_speed);
}
airplane_added
}
/// update from `ME::AirbornePosition{GNSSAltitude, BaroAltitude}`
///
/// Return true if entry was added into `Airplanes`
fn update_position(
&mut self,
icao: ICAO,
altitude: &Altitude,
lat_long: (f64, f64),
max_range: f64,
) -> Added {
let (state, airplane_added) = self.entry_or_insert(icao);
info!(
"[{icao}] with: {:?}, cpr lat: {}, cpr long: {}",
altitude.alt, altitude.lat_cpr, altitude.lon_cpr
);
let mut temp_coords = match altitude.odd_flag {
CPRFormat::Odd => AirplaneCoor {
altitudes: [state.coords.altitudes[0], Some(*altitude)],
..state.coords
},
CPRFormat::Even => AirplaneCoor {
altitudes: [Some(*altitude), state.coords.altitudes[1]],
..state.coords
},
};
// update the position from the new even/odd message if it's a good new position
if temp_coords.update_position(lat_long, max_range) {
// don't bother updating if it's the same coords
if state.coords != temp_coords {
// update track
if let Some(track) = &mut state.track {
track.push(state.coords);
} else {
state.track = Some(vec![state.coords]);
}
// update new position
state.coords = temp_coords;
}
} else {
// clear record
state.coords = AirplaneCoor::default();
}
airplane_added
}
}
/// Generated by `Airplanes::aircraft_details()`
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AirplaneDetails {
pub position: cpr::Position,
pub altitude: u16,
pub kilo_distance: f64,
pub heading: Option<f32>,
pub track: Option<Vec<AirplaneCoor>>,
}
/// Value in `BTreeMap` of `Airplanes`
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AirplaneState {
// TODO: rename to coor
pub coords: AirplaneCoor,
pub squawk: Option<u32>,
pub callsign: Option<String>,
/// heading from `adsb::AirborneVelocity::calculate()`
///
/// 0 = Straight up
/// 90 = Right, and so on
pub heading: Option<f32>,
/// ground_speed from `adsb::AirborneVelocity::calculate()`
///
/// Stored as a f64 in that library but we store as f32 for size reasons in this library
pub speed: Option<f32>,
/// vert_speed from `adsb::AirborneVelocity::calculate()`
pub vert_speed: Option<i16>,
pub on_ground: Option<bool>,
pub num_messages: u32,
#[cfg(feature = "std")]
pub last_time: SystemTime,
pub track: Option<Vec<AirplaneCoor>>,
}
impl Default for AirplaneState {
fn default() -> Self {
Self {
coords: AirplaneCoor::default(),
squawk: None,
callsign: None,
heading: None,
speed: None,
vert_speed: None,
on_ground: None,
num_messages: 0,
#[cfg(feature = "std")]
last_time: SystemTime::now(),
track: None,
}
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AirplaneCoor {
/// [odd, even]
pub altitudes: [Option<Altitude>; 2],
/// lat/long
pub position: Option<cpr::Position>,
/// last good time
#[cfg(feature = "std")]
pub last_time: Option<SystemTime>,
/// distance from receiver lat/long
pub kilo_distance: Option<f64>,
}
impl AirplaneCoor {
/// After checking the range of the new lat / long, new position from last position, update the
/// position of an aircraft
fn update_position(&mut self, lat_long: (f64, f64), max_range: f64) -> bool {
if let [Some(odd), Some(even)] = self.altitudes {
let test_position = cpr::get_position((&odd, &even));
// Check kilometer range from receiver
if let Some(test_position) = test_position {
let kilo_distance = Self::haversine_distance(
lat_long,
(test_position.latitude, test_position.longitude),
);
if kilo_distance > max_range {
warn!("range: {kilo_distance} - old: {lat_long:?} new: {test_position:?}");
return false;
}
self.kilo_distance = Some(kilo_distance);
debug!("range: {kilo_distance}");
}
// if previous position, check against for range. This is a non-great way of doing
// this, but maybe in the future we can check against the speed of the aircraft
if let (Some(current_position), Some(test_position)) = (self.position, test_position) {
let distance = Self::haversine_distance_position(current_position, test_position);
if distance > MAX_AIRCRAFT_DISTANCE {
warn!("distance: {distance} old: {current_position:?}, invalid: {test_position:?}");
return false;
}
debug!("distance: {distance}");
}
// Good new position!
self.position = test_position;
debug!("update_position: odd: (lat: {}, long: {}), even: (lat: {}, long: {}), position: {:?}",
odd.lat_cpr,
odd.lon_cpr,
even.lat_cpr,
even.lat_cpr,
self.position);
#[cfg(feature = "std")]
{
self.last_time = Some(SystemTime::now());
}
}
true
}
/// Return altitude from Odd Altitude
fn altitude(&self) -> Option<u16> {
if let Some(odd) = self.altitudes[0] {
if let Some(alt) = odd.alt {
return Some(alt);
}
}
None
}
/// Calculate the kilometers between two lat/long points
fn haversine_distance_position(position: cpr::Position, other: cpr::Position) -> f64 {
let lat1 = position.latitude;
let lat2 = other.latitude;
let long1 = position.longitude;
let long2 = other.longitude;
Self::haversine_distance((lat1, long1), (lat2, long2))
}
// https://en.wikipedia.org/wiki/Haversine_formula
fn haversine_distance(s: (f64, f64), other: (f64, f64)) -> f64 {
// kilometers
let lat1_rad = s.0.to_radians();
let lat2_rad = other.0.to_radians();
let long1_rad = s.1.to_radians();
let long2_rad = other.1.to_radians();
let x_lat = libm::sin((lat2_rad - lat1_rad) / 2.00);
let x_long = libm::sin((long2_rad - long1_rad) / 2.00);
// this clippy lint will dis-allow mul_add, this isn't available for `no_std`
#[allow(clippy::suboptimal_flops)]
let a = x_lat * x_lat
+ libm::cos(lat1_rad)
* libm::cos(lat2_rad)
* f64::from(libm::powf(libm::sin(x_long) as f32, 2.0));
let c = 2.0 * libm::atan2(libm::sqrt(a), libm::sqrt(1.0 - a));
let r = 6371.00;
r * c
}
}