commit 30543fd0ba760570089ae6aceeed7fecc7a6ea75 Author: Jan-Peter v. Hunnius Date: Mon Jun 1 15:13:13 2026 +0200 feat: initial rusty-telemetry UDP listener - Async UDP listener on configurable bind address (default 127.0.0.1:5005) - RUSTY_TELEMETRY_BIND env var for cross-machine deployment - Deserializes JSON telemetry frames with serde - deny_unknown_fields for schema drift detection - ReceiveStats tracker with packet rate and data rate - Locked stdout writes for efficient output - .gitignore for /target/ and .idea/ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..431845e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target/ +.idea/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..362bdab --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,535 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" + +[[package]] +name = "memchr" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rusty-telemetry" +version = "0.1.0" +dependencies = [ + "chrono", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a9d58d1 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "rusty-telemetry" +version = "0.1.0" +edition = "2021" +description = "Sim racing telemetry receiver and analyzer" + +[dependencies] +tokio = { version = "1", features = ["full"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +chrono = "0.4" diff --git a/README.md b/README.md new file mode 100644 index 0000000..8d93ec0 --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +# rusty-telemetry + +Sim racing telemetry receiver and analyzer. Phase 1 provides a simple UDP listener that receives JSON telemetry frames and prints them to stdout. + +## Build and Run + +```bash +cargo run +``` + +The listener binds to `0.0.0.0:5005` and expects UDP packets containing JSON with the following structure: + +```json +{ + "timestamp": 1716123456.789, + "speed_kmh": 245.3, + "rpm": 8750, + "gear": 6, + "throttle": 0.85, + "brake": 0.0, + "steering": 0.12, + "g_forces": { "x": 0.5, "y": -0.2, "z": 1.0 }, + "wheel_slip": [0.1, 0.1, 0.0, 0.0], + "suspension_travel": [0.15, 0.14, 0.16, 0.15], + "lap_time": 85.234, + "lap_count": 5 +} +``` + +Press Ctrl+C to stop. + +## Testing + +Send a test packet from another terminal: + +```bash +echo '{"timestamp":1.0,"speed_kmh":100.0,"rpm":5000,"gear":3,"throttle":0.5,"brake":0.0,"steering":0.0,"g_forces":{"x":0.0,"y":0.0,"z":1.0},"wheel_slip":[0.0,0.0,0.0,0.0],"suspension_travel":[0.1,0.1,0.1,0.1],"lap_time":60.0,"lap_count":1}' | nc -u -w0 127.0.0.1 5005 +``` + +## Future Plans + +- Telemetry parsers for multiple sim formats +- Persistent storage (SQLite or similar) +- Real-time dashboard / web API +- Lap analysis and comparison tools diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..7176009 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,78 @@ +mod stats; +mod telemetry; + +use stats::ReceiveStats; +use telemetry::TelemetryFrame; + +use std::io::Write; + +const DEFAULT_BIND_ADDR: &str = "127.0.0.1:5005"; +const BUFFER_SIZE: usize = 65535; + +fn print_banner(bind_addr: &str) { + println!("========================================"); + println!(" rusty-telemetry listener"); + println!(" Phase 1 - UDP JSON receiver"); + println!("========================================"); + println!(" Bind: {}", bind_addr); + println!("========================================"); + println!(); +} + +#[tokio::main] +async fn main() { + let bind_addr = + std::env::var("RUSTY_TELEMETRY_BIND").unwrap_or_else(|_| DEFAULT_BIND_ADDR.to_string()); + + print_banner(&bind_addr); + + let socket = match tokio::net::UdpSocket::bind(&bind_addr).await { + Ok(s) => { + let local_addr = s.local_addr().unwrap(); + println!("[INFO] Listening on {}", local_addr); + s + } + Err(e) => { + eprintln!("[ERROR] Failed to bind UDP socket: {}", e); + std::process::exit(1); + } + }; + + let mut buf = vec![0u8; BUFFER_SIZE]; + let mut stats = ReceiveStats::new(); + let stdout = std::io::stdout(); + + loop { + match socket.recv_from(&mut buf).await { + Ok((len, src)) => { + stats.record(len); + + let text = match std::str::from_utf8(&buf[..len]) { + Ok(s) => s, + Err(e) => { + eprintln!("[WARN] Invalid UTF-8 from {}: {}", src, e); + continue; + } + }; + + let frame: TelemetryFrame = match serde_json::from_str(text) { + Ok(f) => f, + Err(e) => { + eprintln!("[WARN] JSON parse error from {}: {}", src, e); + continue; + } + }; + + { + let mut handle = stdout.lock(); + let _ = writeln!(handle, "{}", frame); + let _ = writeln!(handle, " [{}] {}", src, stats); + let _ = writeln!(handle); + } + } + Err(e) => { + eprintln!("[ERROR] Receive error: {}", e); + } + } + } +} diff --git a/src/stats.rs b/src/stats.rs new file mode 100644 index 0000000..eb53e21 --- /dev/null +++ b/src/stats.rs @@ -0,0 +1,70 @@ +use std::fmt; +use std::time::Instant; + +pub struct ReceiveStats { + pub total_packets: u64, + pub total_bytes: u64, + pub start_time: Option, + pub last_packet_time: Option, +} + +impl ReceiveStats { + pub fn new() -> Self { + Self { + total_packets: 0, + total_bytes: 0, + start_time: None, + last_packet_time: None, + } + } + + pub fn record(&mut self, bytes_received: usize) { + let now = Instant::now(); + if self.start_time.is_none() { + self.start_time = Some(now); + } + self.last_packet_time = Some(now); + self.total_packets += 1; + self.total_bytes += bytes_received as u64; + } + + pub fn packets_per_second(&self) -> f64 { + match (self.start_time, self.last_packet_time) { + (Some(start), Some(last)) => { + let elapsed = last.duration_since(start).as_secs_f64(); + if elapsed > 0.0 { + self.total_packets as f64 / elapsed + } else { + 0.0 + } + } + _ => 0.0, + } + } + + pub fn data_rate_kbps(&self) -> f64 { + match self.start_time { + Some(start) => { + let elapsed = start.elapsed().as_secs_f64(); + if elapsed > 0.0 { + (self.total_bytes as f64 / 1024.0) / elapsed + } else { + 0.0 + } + } + _ => 0.0, + } + } +} + +impl fmt::Display for ReceiveStats { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Packets: {} | Rate: {:.1} pkt/s | Data: {:.1} KB/s", + self.total_packets, + self.packets_per_second(), + self.data_rate_kbps(), + ) + } +} diff --git a/src/telemetry.rs b/src/telemetry.rs new file mode 100644 index 0000000..ec0cc57 --- /dev/null +++ b/src/telemetry.rs @@ -0,0 +1,56 @@ +use serde::Deserialize; +use std::fmt; + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct TelemetryFrame { + pub timestamp: f64, + pub speed_kmh: f64, + pub rpm: f64, + pub gear: i32, + pub throttle: f64, + pub brake: f64, + pub steering: f64, + pub g_forces: GForces, + pub wheel_slip: [f64; 4], + pub suspension_travel: [f64; 4], + pub lap_time: f64, + pub lap_count: i32, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct GForces { + pub x: f64, + pub y: f64, + pub z: f64, +} + +impl fmt::Display for TelemetryFrame { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "[Frame @ {:.3}]\n Speed: {:.1} km/h | RPM: {:.0} | Gear: {}\n Throttle: {:.1}% | Brake: {:.1}% | Steering: {:.1}%\n G-Forces: X={:.2} Y={:.2} Z={:.2}\n Wheel Slip: [{:.2}, {:.2}, {:.2}, {:.2}]\n Suspension: [{:.2}, {:.2}, {:.2}, {:.2}]\n Lap: #{} | Lap Time: {:.3}s", + self.timestamp, + self.speed_kmh, + self.rpm, + self.gear, + self.throttle * 100.0, + self.brake * 100.0, + self.steering * 100.0, + self.g_forces.x, + self.g_forces.y, + self.g_forces.z, + self.wheel_slip[0], + self.wheel_slip[1], + self.wheel_slip[2], + self.wheel_slip[3], + self.suspension_travel[0], + self.suspension_travel[1], + self.suspension_travel[2], + self.suspension_travel[3], + self.lap_count, + self.lap_time, + ) + } +}