refactor: TUI multi-feed monitor with binary parsers

- Add ratatui/crossterm TUI replacing stdout data dump
- Add binary parsers for KSUDP RTCarInfo (port 9996) and Telemetry Tool (port 10101)
- Add shared binary_utils module for safe packet reading
- Add ring-buffer frequency calculation and feed health state machine
- Add parse_first_driver() to avoid unnecessary multi-driver allocation
- Add 27 unit tests (14 feed_state + 6 ksudp + 7 telemetry_tool)
- Move parsing outside mutex lock to minimize TUI contention
- Replace expect() on env var with graceful error handling
- Remove unused serde dependency
- Delete obsolete stats.rs and telemetry.rs
This commit is contained in:
Jan-Peter von Hunnius 2026-06-04 08:48:35 +02:00
parent 1731495197
commit 7a1c37fc86
11 changed files with 2308 additions and 307 deletions

97
src/binary_utils.rs Normal file
View file

@ -0,0 +1,97 @@
//! Shared binary reading helpers for UDP packet parsers.
//!
//! All helpers use `get()` for safe bounds checking — they return a zero/empty
//! fallback instead of panicking when the offset is out of bounds.
use crate::telemetry_frame::NUM_WHEELS;
/// Read a `bool` (1 byte) from the data at the given offset.
/// Returns `false` if the offset is out of bounds.
pub fn read_bool(data: &[u8], offset: usize) -> bool {
data.get(offset).map(|&b| b != 0).unwrap_or(false)
}
/// Read a little-endian `u32` from the data at the given offset.
/// Returns 0 if the offset is out of bounds (safe fallback for malformed packets).
pub fn read_u32(data: &[u8], offset: usize) -> u32 {
let buf: [u8; 4] = data
.get(offset..offset + 4)
.and_then(|s| s.try_into().ok())
.unwrap_or([0u8; 4]);
u32::from_le_bytes(buf)
}
/// Read a little-endian `i32` from the data at the given offset.
/// Returns 0 if the offset is out of bounds (safe fallback for malformed packets).
pub fn read_i32(data: &[u8], offset: usize) -> i32 {
let buf: [u8; 4] = data
.get(offset..offset + 4)
.and_then(|s| s.try_into().ok())
.unwrap_or([0u8; 4]);
i32::from_le_bytes(buf)
}
/// Read a little-endian `i16` from the data at the given offset.
/// Returns 0 if the offset is out of bounds (safe fallback for malformed packets).
pub fn read_i16(data: &[u8], offset: usize) -> i16 {
let buf: [u8; 2] = data
.get(offset..offset + 2)
.and_then(|s| s.try_into().ok())
.unwrap_or([0u8; 2]);
i16::from_le_bytes(buf)
}
/// Read a little-endian `f32` from the data at the given offset.
/// Returns 0.0 if the offset is out of bounds (safe fallback for malformed packets).
pub fn read_f32(data: &[u8], offset: usize) -> f32 {
let buf: [u8; 4] = data
.get(offset..offset + 4)
.and_then(|s| s.try_into().ok())
.unwrap_or([0u8; 4]);
f32::from_le_bytes(buf)
}
/// Read a little-endian `f64` from the data at the given offset.
/// Returns 0.0 if the offset is out of bounds (safe fallback for malformed packets).
pub fn read_f64(data: &[u8], offset: usize) -> f64 {
let buf: [u8; 8] = data
.get(offset..offset + 8)
.and_then(|s| s.try_into().ok())
.unwrap_or([0u8; 8]);
f64::from_le_bytes(buf)
}
/// Read an array of 4 little-endian `f32` from the data at the given offset.
pub fn read_f32_4(data: &[u8], offset: usize) -> [f32; NUM_WHEELS] {
let mut arr = [0.0f32; NUM_WHEELS];
for i in 0..NUM_WHEELS {
arr[i] = read_f32(data, offset + i * 4);
}
arr
}
/// Read an array of 2 little-endian `f32` from the data at the given offset.
pub fn read_f32_2(data: &[u8], offset: usize) -> [f32; 2] {
[read_f32(data, offset), read_f32(data, offset + 4)]
}
/// Read an array of 3 little-endian `f32` from the data at the given offset.
pub fn read_f32_3(data: &[u8], offset: usize) -> [f32; 3] {
[read_f32(data, offset), read_f32(data, offset + 4), read_f32(data, offset + 8)]
}
/// Read an array of 3 little-endian `i32` from the data at the given offset.
pub fn read_i32_3(data: &[u8], offset: usize) -> [i32; 3] {
[read_i32(data, offset), read_i32(data, offset + 4), read_i32(data, offset + 8)]
}
/// Read a null-terminated/padded ASCII string.
/// Returns an empty string if the offset is out of bounds.
pub fn read_string(data: &[u8], offset: usize, len: usize) -> String {
let slice = match data.get(offset..offset + len) {
Some(s) => s,
None => return String::new(),
};
let end = slice.iter().position(|&b| b == 0).unwrap_or(len);
String::from_utf8_lossy(&slice[..end]).to_string()
}