heliwatch/heliwatch/src/adsb.rs

188 lines
5.6 KiB
Rust

use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::mpsc::{channel, Receiver};
use super::location::Locations;
/// ft
const MAX_ALTITUDE: u32 = 5_000;
/// s
const STATE_TIMEOUT: u64 = 180;
const IGNORED_CATEGORIES: &[u8] = &[
// Small (15,500..75,000) lbs
2,
// Large (75,000..300,000 lbs)
3,
// High Vortex Large
4,
// Heavy (> 300,000 lbs)
5,
];
#[derive(Debug, Clone)]
pub struct Event {
pub action: Action,
pub info: Info,
pub location: Arc<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Action {
Appeared,
Disappeared,
Moved,
Ignored,
}
#[derive(Debug, Clone)]
pub struct Info {
pub hex: String,
pub flight: Option<String>,
pub altitude_m: Option<f64>,
}
struct State {
info: Info,
position: Option<adsb_deku::cpr::Position>,
location: Option<Arc<String>>,
last: Instant,
}
impl State {
pub fn new(hex: String) -> Self {
State {
info: Info {
hex,
flight: None,
altitude_m: None,
},
position: None,
location: None,
last: Instant::now(),
}
}
pub fn update(&mut self, entry: &beast::aircrafts::Entry, locations: &Locations) -> Option<Event> {
self.last = Instant::now();
if let Some(flight) = entry.flight() {
self.info.flight = Some(flight.to_string());
}
if let Some(altitude_m) = entry.altitude_m() {
self.info.altitude_m = Some(altitude_m);
}
let pos = entry.position()?;
let coord = geo::Coordinate { x: pos.longitude, y: pos.latitude };
if let Some(old_pos) = self.position.replace(pos.clone()) {
if old_pos.longitude != pos.longitude || old_pos.latitude != pos.latitude {
let location = locations.find(&coord);
if location != self.location {
if let Some(location) = location {
self.location = Some(location.clone());
Some(Event {
action: Action::Moved,
location,
info: self.info.clone(),
})
} else {
// move to no location
self.location = None;
None
}
} else {
// pos moved, but no new location
None
}
} else {
// pos not moved
None
}
} else {
self.location = locations.find(&coord);
Some(Event {
action: Action::Appeared,
info: self.info.clone(),
location: self.location.clone()
.unwrap_or_else(|| Arc::new("irgendwo".to_owned())),
})
}
}
}
pub fn run(host: &'static str, port: u16, locations: Locations) -> Receiver<Event> {
let (tx, rx) = channel(1);
tokio::spawn(async move {
let source = beast::aircrafts::Aircrafts::new();
source.connect(host, port);
let mut states = HashMap::new();
// ignore anything above MAX_ALTITUDE permanently
let mut ignored = HashSet::new();
loop {
let mut events = vec![];
for (icao_address, entry) in source.read().iter() {
let hex = format!("{}", icao_address);
let entry = entry.read().unwrap();
if entry.altitude.is_none() {
continue;
}
if entry.altitude.map(|altitude| altitude > MAX_ALTITUDE).unwrap_or(false) ||
entry.category.as_ref().map(|category| IGNORED_CATEGORIES.contains(&category.1)).unwrap_or(false)
{
if !ignored.contains(&hex) {
ignored.insert(hex.clone());
if let Some(_) = states.remove(&hex) {
events.push(Event {
action: Action::Ignored,
info: Info {
hex,
flight: entry.flight().map(|s| s.to_string()),
altitude_m: entry.altitude_m(),
},
location: Arc::new("ignoriert".to_owned()),
});
}
}
continue;
}
if ignored.contains(&hex) {
continue;
}
if let Some(event) = states.entry(hex.clone())
.or_insert_with(|| State::new(hex.clone()))
.update(&entry, &locations) {
events.push(event);
}
}
states.retain(|_, state| {
if state.last + Duration::from_secs(STATE_TIMEOUT) < Instant::now() {
events.push(Event {
action: Action::Disappeared,
info: state.info
.clone(),
location: Arc::new("weg".to_owned()),
});
false
} else {
true
}
});
for event in events {
tx.send(event).await.unwrap();
}
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
});
rx
}