Cleanup before network implementation

This commit is contained in:
Valère Plantevin
2026-05-04 16:53:14 -04:00
parent 4ec5b98df4
commit cac6c9ac02
9 changed files with 286 additions and 14 deletions

View File

@@ -1,11 +1,126 @@
pub mod ecs;
mod server;
/// One sensor sample on the wire.
///
/// Fixed 38-byte little-endian layout — same on x86_64 and aarch64 (the two
/// evaluation hosts), so encode/decode is effectively a memcpy.
///
/// ```text
/// offset size field
/// ------ ---- --------------------------
/// 0 16 device_id (UUID)
/// 16 2 data_stream_id (u16)
/// 18 8 raw_value (f64)
/// 26 8 timestamp_us (u64)
/// 34 4 sequence_number (u32)
/// ```
#[derive(Debug, Clone, Default, Copy, PartialEq)]
pub struct QuicMessage{
pub struct QuicMessage {
pub device_id: uuid::Uuid,
pub data_stream_id: u16,
pub raw_value: f64,
pub timestamp_us: u64,
pub sequence_number: u32,
}
}
#[derive(Debug, thiserror::Error)]
pub enum WireError {
#[error("expected exactly {expected} bytes, got {got}")]
BadLength { expected: usize, got: usize },
}
impl QuicMessage {
/// Bytes on the wire — fixed-size, no length prefix.
pub const WIRE_SIZE: usize = 38;
pub fn encode_to(&self, buf: &mut [u8]) -> Result<(), WireError> {
if buf.len() != Self::WIRE_SIZE {
return Err(WireError::BadLength {
expected: Self::WIRE_SIZE,
got: buf.len(),
});
}
buf[0..16].copy_from_slice(self.device_id.as_bytes());
buf[16..18].copy_from_slice(&self.data_stream_id.to_le_bytes());
buf[18..26].copy_from_slice(&self.raw_value.to_le_bytes());
buf[26..34].copy_from_slice(&self.timestamp_us.to_le_bytes());
buf[34..38].copy_from_slice(&self.sequence_number.to_le_bytes());
Ok(())
}
pub fn to_bytes(&self) -> [u8; Self::WIRE_SIZE] {
let mut buf = [0u8; Self::WIRE_SIZE];
self.encode_to(&mut buf).expect("WIRE_SIZE buffer is exactly sized");
buf
}
pub fn decode(buf: &[u8]) -> Result<Self, WireError> {
if buf.len() != Self::WIRE_SIZE {
return Err(WireError::BadLength {
expected: Self::WIRE_SIZE,
got: buf.len(),
});
}
let mut id_bytes = [0u8; 16];
id_bytes.copy_from_slice(&buf[0..16]);
Ok(Self {
device_id: uuid::Uuid::from_bytes(id_bytes),
data_stream_id: u16::from_le_bytes(buf[16..18].try_into().unwrap()),
raw_value: f64::from_le_bytes(buf[18..26].try_into().unwrap()),
timestamp_us: u64::from_le_bytes(buf[26..34].try_into().unwrap()),
sequence_number: u32::from_le_bytes(buf[34..38].try_into().unwrap()),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wire_size_matches_fields() {
assert_eq!(QuicMessage::WIRE_SIZE, 16 + 2 + 8 + 8 + 4);
}
#[test]
fn roundtrip_preserves_all_fields() {
let msg = QuicMessage {
device_id: uuid::Uuid::from_u128(0x0123456789abcdef_fedcba9876543210),
data_stream_id: 0xBEEF,
raw_value: -273.15,
timestamp_us: 1_700_000_000_000_001,
sequence_number: 42,
};
let bytes = msg.to_bytes();
assert_eq!(bytes.len(), QuicMessage::WIRE_SIZE);
let decoded = QuicMessage::decode(&bytes).unwrap();
assert_eq!(msg, decoded);
}
#[test]
fn decode_rejects_wrong_length() {
assert!(matches!(
QuicMessage::decode(&[0u8; 37]),
Err(WireError::BadLength { expected: 38, got: 37 })
));
assert!(matches!(
QuicMessage::decode(&[0u8; 39]),
Err(WireError::BadLength { expected: 38, got: 39 })
));
}
#[test]
fn encode_layout_is_little_endian() {
let msg = QuicMessage {
device_id: uuid::Uuid::nil(),
data_stream_id: 0x0102,
raw_value: 0.0,
timestamp_us: 0,
sequence_number: 0x04030201,
};
let bytes = msg.to_bytes();
assert_eq!(&bytes[16..18], &[0x02, 0x01]);
assert_eq!(&bytes[34..38], &[0x01, 0x02, 0x03, 0x04]);
}
}