diff --git a/Cargo.lock b/Cargo.lock index 8bdb54f..e4ecb39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,25 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "adsb" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9245ecd631f63f1995269dc9f88fdda8fc8e9a308c4196315cfdedd1ccfccc07" +dependencies = [ + "lazy_static", + "nom", +] + +[[package]] +name = "approx" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0e60b75072ecd4168020818c0107f2857bb6c4e64252d8d3983f6263b40a5c3" +dependencies = [ + "num-traits", +] + [[package]] name = "approx" version = "0.4.0" @@ -11,6 +30,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + [[package]] name = "as-slice" version = "0.1.5" @@ -46,12 +71,33 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +[[package]] +name = "beast" +version = "0.1.0" +dependencies = [ + "adsb", + "futures", + "tokio", +] + [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitvec" +version = "0.19.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8942c8d352ae1838c9dda0b0ca2ab657696ef2232a20147cf1b30ae1a9cb4321" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blake2" version = "0.9.2" @@ -81,9 +127,9 @@ checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" [[package]] name = "bstr" -version = "0.2.17" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +checksum = "a40b47ad93e1a5404e6c18dec46b628214fee441c70f4ab5d6942142cc268a3d" dependencies = [ "lazy_static", "memchr", @@ -91,6 +137,16 @@ dependencies = [ "serde", ] +[[package]] +name = "buf_redux" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f" +dependencies = [ + "memchr", + "safemem", +] + [[package]] name = "bumpalo" version = "3.8.0" @@ -131,6 +187,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "collectd-stats" +version = "0.1.0" +dependencies = [ + "beast", + "nav-types", + "tokio", +] + [[package]] name = "core-foundation" version = "0.9.2" @@ -265,6 +330,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "funty" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" + [[package]] name = "futf" version = "0.1.4" @@ -417,7 +488,7 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8bd2e95dd9f5c8ff74159ed9205ad7fd239a9569173a550863976421b45d2bb" dependencies = [ - "approx", + "approx 0.4.0", "num-traits", "rstar", ] @@ -487,6 +558,31 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +[[package]] +name = "headers" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c4eb0471fcb85846d8b0690695ef354f9afb11cb03cac2e1d7c9253351afb0" +dependencies = [ + "base64", + "bitflags", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha-1", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http", +] + [[package]] name = "heapless" version = "0.6.1" @@ -575,6 +671,18 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-json" +version = "0.1.0" +dependencies = [ + "adsb", + "beast", + "serde", + "serde_json", + "tokio", + "warp", +] + [[package]] name = "httparse" version = "1.5.1" @@ -645,6 +753,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "input_buffer" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f97967975f448f1a7ddb12b0bc41069d09ed6a1c161a92687e057325db35d413" +dependencies = [ + "bytes", +] + [[package]] name = "instant" version = "0.1.12" @@ -708,6 +825,19 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lexical-core" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" +dependencies = [ + "arrayvec", + "bitflags", + "cfg-if", + "ryu", + "static_assertions", +] + [[package]] name = "libc" version = "0.2.105" @@ -780,10 +910,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" [[package]] -name = "memchr" -version = "2.4.1" +name = "matrixmultiply" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +checksum = "916806ba0031cd542105d916a97c8572e1fa6dd79c9c51e7eb43a09ec2dd84c1" +dependencies = [ + "rawpointer", +] + +[[package]] +name = "memchr" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" [[package]] name = "mime" @@ -791,6 +930,16 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "mime_guess" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minidom" version = "0.13.0" @@ -822,6 +971,42 @@ dependencies = [ "winapi", ] +[[package]] +name = "multipart" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050aeedc89243f5347c3e237e3e13dc76fbe4ae3742a57b94dc14f69acf76d4" +dependencies = [ + "buf_redux", + "httparse", + "log", + "mime", + "mime_guess", + "quick-error", + "rand 0.7.3", + "safemem", + "tempfile", + "twoway", +] + +[[package]] +name = "nalgebra" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "500430fe18b836c0099c22e716139eae37210ecf8e1ae3931b73078e9a2de7a9" +dependencies = [ + "approx 0.3.2", + "generic-array 0.13.3", + "matrixmultiply", + "num-complex", + "num-rational", + "num-traits", + "rand 0.7.3", + "rand_distr", + "simba", + "typenum", +] + [[package]] name = "native-tls" version = "0.2.8" @@ -840,12 +1025,34 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nav-types" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a761b66b2e27aba4945f8e82ad20aef2dac0f47f9659ecbbdf54095c79828a37" +dependencies = [ + "nalgebra", +] + [[package]] name = "new_debug_unreachable" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" +[[package]] +name = "nom" +version = "6.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c5c51b9083a3c620fa67a2a635d1ce7d95b897e957d6b28ff9a5da960a103a6" +dependencies = [ + "bitvec", + "funty", + "lexical-core", + "memchr", + "version_check", +] + [[package]] name = "ntapi" version = "0.3.6" @@ -855,6 +1062,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-complex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95" +dependencies = [ + "autocfg", + "num-traits", +] + [[package]] name = "num-integer" version = "0.1.44" @@ -865,6 +1082,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.14" @@ -954,6 +1182,25 @@ dependencies = [ "winapi", ] +[[package]] +name = "paste" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45ca20c77d80be666aef2b45486da86238fabe33e38306bd3118fe4af33fa880" +dependencies = [ + "paste-impl", + "proc-macro-hack", +] + +[[package]] +name = "paste-impl" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a7db200b97ef370c8e6de0088252f7e0dfff7d047a28528e47456c0fc98b6" +dependencies = [ + "proc-macro-hack", +] + [[package]] name = "pbkdf2" version = "0.6.0" @@ -1022,6 +1269,26 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pin-project" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "576bc800220cc65dac09e99e97b08b358cfab6e17078de8dc5fee223bd2d0c08" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e8fe8163d14ce7f0cdac2e040116f22eac817edabff0be91e8aff7e9accf389" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.7" @@ -1097,6 +1364,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" + [[package]] name = "rand" version = "0.7.3" @@ -1161,6 +1434,15 @@ dependencies = [ "getrandom 0.2.3", ] +[[package]] +name = "rand_distr" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96977acbdd3a6576fb1d27391900035bf3863d4a16422973a409b488cf29ffb2" +dependencies = [ + "rand 0.7.3", +] + [[package]] name = "rand_hc" version = "0.2.0" @@ -1188,6 +1470,12 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + [[package]] name = "redox_syscall" version = "0.2.10" @@ -1290,6 +1578,12 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + [[package]] name = "sasl" version = "0.5.0" @@ -1314,6 +1608,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "scoped-tls" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" + [[package]] name = "scopeguard" version = "1.1.0" @@ -1451,6 +1751,18 @@ dependencies = [ "libc", ] +[[package]] +name = "simba" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051b345c9ec09734622cbb49e20dfcf72c9b55e473f0ca6a8025409e37c25fb8" +dependencies = [ + "approx 0.3.2", + "num-complex", + "num-traits", + "paste", +] + [[package]] name = "siphasher" version = "0.3.7" @@ -1496,6 +1808,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "string_cache" version = "0.8.2" @@ -1539,6 +1857,12 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tempfile" version = "3.2.0" @@ -1661,6 +1985,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-tungstenite" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1a5f475f1b9d077ea1017ecbc60890fda8e54942d680ca0b1d2b47cfa2d861b" +dependencies = [ + "futures-util", + "log", + "pin-project", + "tokio", + "tungstenite", +] + [[package]] name = "tokio-util" version = "0.6.8" @@ -1711,6 +2048,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105" dependencies = [ "cfg-if", + "log", "pin-project-lite", "tracing-core", ] @@ -1775,6 +2113,34 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +[[package]] +name = "tungstenite" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ada8297e8d70872fa9a551d93250a9f407beb9f37ef86494eb20012a2ff7c24" +dependencies = [ + "base64", + "byteorder", + "bytes", + "http", + "httparse", + "input_buffer", + "log", + "rand 0.8.4", + "sha-1", + "url", + "utf-8", +] + +[[package]] +name = "twoway" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1" +dependencies = [ + "memchr", +] + [[package]] name = "typenum" version = "1.14.0" @@ -1787,6 +2153,15 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.7" @@ -1854,6 +2229,35 @@ dependencies = [ "try-lock", ] +[[package]] +name = "warp" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "332d47745e9a0c38636dbd454729b147d16bd1ed08ae67b3ab281c4506771054" +dependencies = [ + "bytes", + "futures", + "headers", + "http", + "hyper", + "log", + "mime", + "mime_guess", + "multipart", + "percent-encoding", + "pin-project", + "scoped-tls", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-stream", + "tokio-tungstenite", + "tokio-util", + "tower-service", + "tracing", +] + [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" @@ -1988,6 +2392,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "wyz" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" + [[package]] name = "xml5ever" version = "0.16.2" diff --git a/Cargo.toml b/Cargo.toml index 49e45fa..c0fcfff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,7 @@ [workspace] members = [ "heliwatch", + "beast", + "http-json", + "collectd-stats", ] diff --git a/beast/Cargo.toml b/beast/Cargo.toml new file mode 100644 index 0000000..5b55283 --- /dev/null +++ b/beast/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "beast" +description = "Receive ADS-B data from readsb" +version = "0.1.0" +edition = "2021" + +[dependencies] +futures = "0.3" +tokio = { version = "1", features = ["full"] } +adsb = "0.3" diff --git a/beast/src/aircrafts.rs b/beast/src/aircrafts.rs new file mode 100644 index 0000000..f5302c9 --- /dev/null +++ b/beast/src/aircrafts.rs @@ -0,0 +1,153 @@ +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; +use std::time::{Duration, Instant}; +use futures::stream::StreamExt; +use tokio::sync::mpsc::channel; +use adsb::ICAOAddress; +use super::beast; + +#[derive(Default)] +pub struct Entry { + pub emitter_category: Option, + pub callsign: Option, + pub altitude: Option, + cpr1: Option, + cpr2: Option, + pub heading: Option, + pub ground_speed: Option, + pub vertical_rate: Option, + pub vertical_rate_source: Option, + last_update: Option, +} + +impl Entry { + const MAX_AGE: u64 = 300; + + fn update(&mut self, kind: adsb::ADSBMessageKind) { + match kind { + adsb::ADSBMessageKind::AirbornePosition { + altitude, cpr_frame + } => { + self.altitude = Some(altitude); + if self.cpr2.as_ref().map(|cpr2| cpr2.parity != cpr_frame.parity).unwrap_or(false) { + self.cpr1 = self.cpr2.take(); + } + self.cpr2 = Some(cpr_frame); + } + adsb::ADSBMessageKind::AirborneVelocity { + heading, ground_speed, + vertical_rate, vertical_rate_source, + } => { + self.heading = Some(heading); + self.ground_speed = Some(ground_speed); + self.vertical_rate = Some(vertical_rate); + self.vertical_rate_source = Some(vertical_rate_source); + } + adsb::ADSBMessageKind::AircraftIdentification { + emitter_category, callsign + } => { + self.emitter_category = Some(emitter_category); + self.callsign = Some(callsign); + } + } + + self.last_update = Some(Instant::now()); + } + + pub fn location(&self) -> Option { + match (&self.cpr1, &self.cpr2) { + (Some(cpr1), Some(cpr2)) => { + let pos = adsb::cpr::get_position((cpr1, cpr2))?; + if pos.latitude >= -90.0 && pos.latitude <= 90.0 && + pos.longitude >= -180.0 && pos.longitude <= 180.0 + { + Some(pos) + } else { + None + } + } + _ => + None + } + } +} + +#[derive(Clone)] +pub struct Aircrafts { + state: Arc>>>, +} + +impl Aircrafts { + pub fn new() -> Self { + Aircrafts { + state: Arc::new(RwLock::new(HashMap::new())), + } + } + + pub fn read(&self) -> std::sync::RwLockReadGuard>> { + self.state.read().unwrap() + } + + pub fn connect(&self, host: &'static str, port: u16) { + // buffering channel because readsb is very sensitive + let (tx, mut rx) = channel(16 * 1024); + + // network input + tokio::spawn(async move { + loop { + let mut stream; + if let Ok(stream_) = beast::connect(host, port).await { + stream = Box::pin(stream_); + } else { + tokio::time::sleep(Duration::from_secs(1)).await; + // Retry + continue; + } + + while let Some(frame) = stream.next().await { + let _ = tx.send(frame).await; + } + } + }); + + // state update + let state = self.state.clone(); + tokio::spawn(async move { + while let Some(frame) = rx.recv().await { + match frame.parse_adsb() { + Some(adsb::Message { + kind: adsb::MessageKind::ADSBMessage { + icao_address, + kind, + crc, + .. }, + .. }) if crc => + { + state.write().unwrap() + .entry(icao_address) + .or_default() + .write().unwrap() + .update(kind); + } + _ => {} + } + } + }); + + // discard old states + let state = self.state.clone(); + tokio::spawn(async move { + loop { + state.write().unwrap(). + retain(|_, entry| { + entry.read().unwrap() + .last_update.map(|last_update| { + last_update + Duration::from_secs(Entry::MAX_AGE) > Instant::now() + }) + .unwrap_or(false) + }); + tokio::time::sleep(Duration::from_secs(1)).await; + } + }); + } +} diff --git a/beast/src/beast.rs b/beast/src/beast.rs new file mode 100644 index 0000000..725ddae --- /dev/null +++ b/beast/src/beast.rs @@ -0,0 +1,154 @@ +use futures::stream; +use tokio::io::AsyncReadExt; +use tokio::net::TcpStream; + +const DEFRAMER_DEFAULT_CAPACITY: usize = 22; + +struct Deframer { + buffer: Vec, + escapes: usize, +} + +impl Deframer { + pub fn new() -> Self { + Deframer { + buffer: Vec::with_capacity(DEFRAMER_DEFAULT_CAPACITY), + escapes: 0, + } + } + + pub fn push(&mut self, c: u8) -> Option { + let mut result = None; + if c == 0x1a { + self.escapes += 1; + } else { + if self.escapes > 0 { + for _ in 0..(self.escapes / 2) { + self.buffer.push(0x1a); + } + if self.escapes & 1 == 1 { + let new_buffer = Vec::with_capacity(DEFRAMER_DEFAULT_CAPACITY); + let buffer = std::mem::replace(&mut self.buffer, new_buffer); + if buffer.len() > 0 { + result = Frame::new(buffer); + } + } + self.escapes = 0; + } + + self.buffer.push(c); + } + result + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(u8)] +pub enum FrameType { + ModeAC = '1' as u8, + ModeSShort = '2' as u8, + ModeSLong = '3' as u8, + Config = '4' as u8, +} + +#[derive(Debug)] +pub struct Frame { + data: Vec, +} + +impl Frame { + pub fn new(data: Vec) -> Option { + if data.len() < 1 { + return None; + } + + let this = Frame { data }; + match (this.frame_type(), this.data.len()) { + (Some(FrameType::ModeAC), 10) => + Some(this), + (Some(FrameType::ModeSShort), 15) => + Some(this), + (Some(FrameType::ModeSLong), 22) => + Some(this), + _ => + None // don't care + } + } + + pub fn frame_type(&self) -> Option { + match self.data[0] as char { + '1' => Some(FrameType::ModeAC), + '2' => Some(FrameType::ModeSShort), + '3' => Some(FrameType::ModeSLong), + '4' => Some(FrameType::Config), + _ => None, + } + } + + pub fn timestamp(&self) -> u64 { + match self.frame_type() { + Some(_) => { + let mut result = 0; + for i in 1..=6 { + result |= u64::from(self.data[i]) << (8 * (6 - i)); + } + result + } + None => 0 + } + } + + pub fn signal(&self) -> u8 { + match self.frame_type() { + Some(FrameType::ModeAC) | + Some(FrameType::ModeSShort) | + Some(FrameType::ModeSLong) => { + self.data[7] + } + _ => 255 + } + } + + pub fn parse_adsb(&self) -> Option { + match self.frame_type() { + Some(FrameType::ModeSShort) | Some(FrameType::ModeSLong) => { + if let Ok((msg, _rest)) = adsb::parse_binary(&self.data[8..]) { + Some(msg) + } else { + eprintln!("adsb decode error"); + None + } + } + _ => None + } + } +} + +pub async fn connect(host: &str, port: u16) -> Result, Box> { + println!("Connecting to {}:{}", host, port); + let conn = TcpStream::connect((host, port)).await?; + println!("Connected to {}:{}", host, port); + let stream = stream::unfold((conn, vec![], Deframer::new()), |(mut conn, mut buf, mut deframe)| async { + loop { + for i in 0..buf.len() { + if let Some(frame) = deframe.push(buf[i]) { + buf = buf.split_off(i + 1); + return Some((frame, (conn, buf, deframe))); + } + } // buf consumed + + if buf.len() < DEFRAMER_DEFAULT_CAPACITY { + buf = vec![0; 1024]; + } + match conn.read(&mut buf).await { + Ok(0) | Err(_) => { + println!("Disconnected"); + return None; + } + Ok(len) => + buf.truncate(len), + } + } + }); + Ok(stream) +} diff --git a/beast/src/lib.rs b/beast/src/lib.rs new file mode 100644 index 0000000..f6c931d --- /dev/null +++ b/beast/src/lib.rs @@ -0,0 +1,2 @@ +pub mod beast; +pub mod aircrafts; diff --git a/collectd-stats/Cargo.toml b/collectd-stats/Cargo.toml new file mode 100644 index 0000000..e4502f2 --- /dev/null +++ b/collectd-stats/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "collectd-stats" +description = "ADS-B statistics in collectd" +version = "0.1.0" +edition = "2021" + +[dependencies] +tokio = { version = "1", features = ["full"] } +nav-types = "0.5" +beast = { path = "../beast" } diff --git a/collectd-stats/src/main.rs b/collectd-stats/src/main.rs new file mode 100644 index 0000000..84fe663 --- /dev/null +++ b/collectd-stats/src/main.rs @@ -0,0 +1,91 @@ +use std::time::Duration; +use tokio::time::sleep; +use nav_types::WGS84; + +const INTERVAL: u64 = 10; + +fn update_min(value: &Option, target: &mut Option) { + match (value, target) { + (Some(value), target) if target.is_none() => + *target = Some(value.clone()), + (Some(value), Some(target)) if value < target => + *target = value.clone(), + _ => {} + }; +} + +fn update_max(value: &Option, target: &mut Option) { + match (value, target) { + (Some(value), target) if target.is_none() => + *target = Some(value.clone()), + (Some(value), Some(target)) if value > target => + *target = value.clone(), + _ => {} + }; +} + +#[tokio::main] +async fn main() { + let home = WGS84::from_degrees_and_meters(51.081, 13.728, 0.0); + + let aircrafts = beast::aircrafts::Aircrafts::new(); + aircrafts.connect("radiobert.serv.zentralwerk.org", 30005); + let hostname_line = std::fs::read_to_string("/proc/sys/kernel/hostname") + .unwrap(); + let hostname = hostname_line.split(char::is_whitespace) + .next() + .unwrap(); + + loop { + sleep(Duration::from_secs(INTERVAL)).await; + + let mut count = 0; + let mut min_altitude = None; + let mut max_altitude = None; + let mut min_speed = None; + let mut max_speed = None; + let mut min_distance = None; + let mut max_distance = None; + let aircrafts = aircrafts.read(); + for (_, entry) in aircrafts.iter() { + let entry = entry.read().unwrap(); + count += 1; + + update_min(&entry.altitude, &mut min_altitude); + update_max(&entry.altitude, &mut max_altitude); + + update_min(&entry.ground_speed, &mut min_speed); + update_max(&entry.ground_speed, &mut max_speed); + + if let Some(pos) = entry.location() { + let altitude_m = entry.altitude.unwrap_or(0) as f64 * 0.3048; + let distance_km = WGS84::from_degrees_and_meters(pos.latitude, pos.longitude, altitude_m) + .distance(&home) / 1000.0; + update_min(&Some(distance_km), &mut min_distance); + update_max(&Some(distance_km), &mut max_distance); + } + } + println!("PUTVAL \"{}/beast-aircrafts/records\" interval={} N:{}", hostname, INTERVAL, count); + + min_altitude.map(|min_altitude| { + println!("PUTVAL \"{}/beast-altitude/current-min\" interval={} N:{}", hostname, INTERVAL, min_altitude); + }); + max_altitude.map(|max_altitude| { + println!("PUTVAL \"{}/beast-altitude/current-max\" interval={} N:{}", hostname, INTERVAL, max_altitude); + }); + + min_speed.map(|min_speed| { + println!("PUTVAL \"{}/beast-speed/current-min\" interval={} N:{:.3}", hostname, INTERVAL, min_speed); + }); + max_speed.map(|max_speed| { + println!("PUTVAL \"{}/beast-speed/current-max\" interval={} N:{:.3}", hostname, INTERVAL, max_speed); + }); + + min_distance.map(|min_distance| { + println!("PUTVAL \"{}/beast-distance/current-min\" interval={} N:{:.3}", hostname, INTERVAL, min_distance); + }); + max_distance.map(|max_distance| { + println!("PUTVAL \"{}/beast-distance/current-max\" interval={} N:{:.3}", hostname, INTERVAL, max_distance); + }); + } +} diff --git a/flake.nix b/flake.nix index f22a56a..386ea13 100644 --- a/flake.nix +++ b/flake.nix @@ -21,16 +21,26 @@ cargo = rust; rustc = rust; }; - in rec { - # `nix build` - packages.heliwatch = naersk-lib.buildPackage { - pname = "heliwatch"; + + buildRustPackage = args: naersk-lib.buildPackage ({ root = ./.; src = ./heliwatch; nativeBuildInputs = with pkgs; [ pkg-config ]; buildInputs = with pkgs; [ openssl ]; + } // args); + in rec { + # `nix build` + packages.heliwatch = buildRustPackage { + pname = "heliwatch"; + }; + packages.http-json = buildRustPackage { + pname = "http-json"; + }; + packages.collectd-stats = buildRustPackage { + pname = "collectd-stats"; }; defaultPackage = packages.heliwatch; + checks = packages; # `nix run` apps.heliwatch = utils.lib.mkApp { diff --git a/http-json/Cargo.toml b/http-json/Cargo.toml new file mode 100644 index 0000000..52ca2f5 --- /dev/null +++ b/http-json/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "http-json" +description = "Serves http://:8080/data.json for dump1090's HTML map" +version = "0.1.0" +edition = "2021" + +[dependencies] +tokio = { version = "1", features = ["full"] } +beast = { path = "../beast" } +warp = "0.3" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +adsb = "0.3" diff --git a/http-json/src/main.rs b/http-json/src/main.rs new file mode 100644 index 0000000..9f1e78f --- /dev/null +++ b/http-json/src/main.rs @@ -0,0 +1,58 @@ +use warp::{http::Response, Filter}; +use adsb::ICAOAddress; +use beast::aircrafts::Entry; +use serde::Serialize; + +#[derive(Serialize)] +pub struct Aircraft { + hex: String, + flight: Option, + category: Option, + lat: Option, + lon: Option, + /// ft + altitude: Option, + track: Option, + /// kts + speed: Option, +} + +impl Aircraft { + pub fn new(icao_address: &ICAOAddress, entry: &Entry) -> Self { + let pos = entry.location(); + Aircraft { + hex: format!("{}", icao_address), + flight: entry.callsign.clone(), + category: entry.emitter_category.clone(), + lat: pos.as_ref().map(|pos| pos.latitude), + lon: pos.as_ref().map(|pos| pos.longitude), + altitude: entry.altitude.clone(), + track: entry.heading.map(|heading| heading as i16), + speed: entry.ground_speed.map(|speed| speed as u16), + } + } +} + +#[tokio::main] +async fn main() { + let aircrafts = beast::aircrafts::Aircrafts::new(); + aircrafts.connect("radiobert.serv.zentralwerk.org", 30005); + + let hello = warp::path!("data.json") + .map(move || { + let data = aircrafts.read() + .iter() + .map(|(icao_address, entry)| Aircraft::new(icao_address, &entry.read().unwrap())) + .collect::>(); + let json = serde_json::ser::to_vec_pretty(&data) + .unwrap_or_else(|_| vec![]); + + Response::builder() + .header("Content-Type", "application/json") + .body(json) + }); + + warp::serve(hello) + .run(([0, 0, 0, 0], 8080)) + .await; +}