openigtlink_rust/protocol/
header.rs

1//! OpenIGTLink protocol header implementation
2//!
3//! The header is a fixed 58-byte structure that precedes every OpenIGTLink message.
4
5use crate::error::{IgtlError, Result};
6use bytes::{Buf, BufMut, BytesMut};
7
8/// Type-safe wrapper for message type name (12 bytes, null-padded)
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct TypeName([u8; 12]);
11
12impl TypeName {
13    /// Create a new TypeName from a string
14    pub fn new(name: &str) -> Result<Self> {
15        if name.len() > 12 {
16            return Err(IgtlError::InvalidHeader(format!(
17                "Type name too long: {} bytes (max: 12)",
18                name.len()
19            )));
20        }
21        let mut bytes = [0u8; 12];
22        bytes[..name.len()].copy_from_slice(name.as_bytes());
23        Ok(TypeName(bytes))
24    }
25
26    /// Get the type name as a string (trimming null bytes)
27    pub fn as_str(&self) -> Result<&str> {
28        let len = self.0.iter().position(|&b| b == 0).unwrap_or(12);
29        std::str::from_utf8(&self.0[..len])
30            .map_err(|_| IgtlError::InvalidHeader("Invalid UTF-8 in type name".to_string()))
31    }
32}
33
34impl From<[u8; 12]> for TypeName {
35    fn from(bytes: [u8; 12]) -> Self {
36        TypeName(bytes)
37    }
38}
39
40/// Type-safe wrapper for device name (20 bytes, null-padded)
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct DeviceName([u8; 20]);
43
44impl DeviceName {
45    /// Create a new DeviceName from a string
46    pub fn new(name: &str) -> Result<Self> {
47        if name.len() > 20 {
48            return Err(IgtlError::InvalidHeader(format!(
49                "Device name too long: {} bytes (max: 20)",
50                name.len()
51            )));
52        }
53        let mut bytes = [0u8; 20];
54        bytes[..name.len()].copy_from_slice(name.as_bytes());
55        Ok(DeviceName(bytes))
56    }
57
58    /// Get the device name as a string (trimming null bytes)
59    pub fn as_str(&self) -> Result<&str> {
60        let len = self.0.iter().position(|&b| b == 0).unwrap_or(20);
61        std::str::from_utf8(&self.0[..len])
62            .map_err(|_| IgtlError::InvalidHeader("Invalid UTF-8 in device name".to_string()))
63    }
64}
65
66impl From<[u8; 20]> for DeviceName {
67    fn from(bytes: [u8; 20]) -> Self {
68        DeviceName(bytes)
69    }
70}
71
72/// High-precision timestamp for OpenIGTLink messages
73///
74/// The timestamp field in OpenIGTLink is a 64-bit value where:
75/// - Upper 32 bits: seconds since Unix epoch (UTC)
76/// - Lower 32 bits: fractional seconds in nanoseconds / 2^32
77///
78/// This provides nanosecond-level precision, critical for real-time tracking
79/// at 1000 Hz (1ms intervals).
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub struct Timestamp {
82    /// Seconds since Unix epoch (1970-01-01 00:00:00 UTC)
83    pub seconds: u32,
84    /// Fractional seconds as a 32-bit value (nanoseconds * 2^32 / 1_000_000_000)
85    pub fraction: u32,
86}
87
88impl Timestamp {
89    /// Create a new timestamp from seconds and fraction
90    ///
91    /// # Arguments
92    /// * `seconds` - Seconds since Unix epoch
93    /// * `fraction` - Fractional seconds (0x00000000 to 0xFFFFFFFF represents 0.0 to ~1.0)
94    pub fn new(seconds: u32, fraction: u32) -> Self {
95        Timestamp { seconds, fraction }
96    }
97
98    /// Create a timestamp representing the current time
99    ///
100    /// # Examples
101    ///
102    /// ```
103    /// use openigtlink_rust::protocol::header::Timestamp;
104    ///
105    /// let ts = Timestamp::now();
106    /// assert!(ts.seconds > 0);
107    /// ```
108    pub fn now() -> Self {
109        let now = std::time::SystemTime::now()
110            .duration_since(std::time::UNIX_EPOCH)
111            .unwrap();
112
113        let seconds = now.as_secs() as u32;
114        // Convert nanoseconds to fraction: fraction = (nanoseconds * 2^32) / 10^9
115        let nanos = now.subsec_nanos();
116        let fraction = ((nanos as u64) * 0x1_0000_0000 / 1_000_000_000) as u32;
117
118        Timestamp { seconds, fraction }
119    }
120
121    /// Create a zero timestamp (no timestamp)
122    ///
123    /// # Examples
124    ///
125    /// ```
126    /// use openigtlink_rust::protocol::header::Timestamp;
127    ///
128    /// let ts = Timestamp::zero();
129    /// assert_eq!(ts.to_u64(), 0);
130    /// ```
131    pub fn zero() -> Self {
132        Timestamp {
133            seconds: 0,
134            fraction: 0,
135        }
136    }
137
138    /// Convert to OpenIGTLink wire format (u64)
139    ///
140    /// Upper 32 bits: seconds, Lower 32 bits: fraction
141    pub fn to_u64(self) -> u64 {
142        ((self.seconds as u64) << 32) | (self.fraction as u64)
143    }
144
145    /// Create from OpenIGTLink wire format (u64)
146    ///
147    /// Upper 32 bits: seconds, Lower 32 bits: fraction
148    pub fn from_u64(value: u64) -> Self {
149        Timestamp {
150            seconds: (value >> 32) as u32,
151            fraction: (value & 0xFFFFFFFF) as u32,
152        }
153    }
154
155    /// Convert to nanoseconds since Unix epoch
156    ///
157    /// # Examples
158    ///
159    /// ```
160    /// use openigtlink_rust::protocol::header::Timestamp;
161    ///
162    /// let ts = Timestamp::new(1000, 0x80000000); // 1000.5 seconds
163    /// assert_eq!(ts.to_nanos(), 1_000_500_000_000);
164    /// ```
165    pub fn to_nanos(self) -> u64 {
166        let sec_nanos = (self.seconds as u64) * 1_000_000_000;
167        let frac_nanos = ((self.fraction as u64) * 1_000_000_000) / 0x1_0000_0000;
168        sec_nanos + frac_nanos
169    }
170
171    /// Create from nanoseconds since Unix epoch
172    ///
173    /// # Examples
174    ///
175    /// ```
176    /// use openigtlink_rust::protocol::header::Timestamp;
177    ///
178    /// let ts = Timestamp::from_nanos(1_000_500_000_000); // 1000.5 seconds
179    /// assert_eq!(ts.seconds, 1000);
180    /// // Fraction should be approximately 0x80000000 (0.5)
181    /// assert!((ts.fraction as i64 - 0x80000000_i64).abs() < 100);
182    /// ```
183    pub fn from_nanos(nanos: u64) -> Self {
184        let seconds = (nanos / 1_000_000_000) as u32;
185        let remaining_nanos = (nanos % 1_000_000_000) as u32;
186        let fraction = ((remaining_nanos as u64) * 0x1_0000_0000 / 1_000_000_000) as u32;
187
188        Timestamp { seconds, fraction }
189    }
190
191    /// Convert to floating-point seconds
192    ///
193    /// # Examples
194    ///
195    /// ```
196    /// use openigtlink_rust::protocol::header::Timestamp;
197    ///
198    /// let ts = Timestamp::new(1000, 0x80000000); // 1000.5 seconds
199    /// assert!((ts.to_f64() - 1000.5).abs() < 0.0001);
200    /// ```
201    pub fn to_f64(self) -> f64 {
202        let frac_f64 = (self.fraction as f64) / (u32::MAX as f64 + 1.0);
203        (self.seconds as f64) + frac_f64
204    }
205}
206
207/// OpenIGTLink message header (58 bytes fixed size)
208///
209/// # Header Structure (all numerical values in big-endian)
210/// - Version: u16 (2 bytes)
211/// - Type: `char[12]` (12 bytes, null-padded)
212/// - Device Name: `char[20]` (20 bytes, null-padded)
213/// - Timestamp: u64 (8 bytes) - high 32 bits: seconds, low 32 bits: fraction
214/// - Body Size: u64 (8 bytes)
215/// - CRC: u64 (8 bytes)
216#[derive(Debug, Clone)]
217pub struct Header {
218    /// Protocol version number (2 for version 2 and 3)
219    pub version: u16,
220    /// Message type name
221    pub type_name: TypeName,
222    /// Unique device name
223    pub device_name: DeviceName,
224    /// High-precision timestamp (nanosecond resolution)
225    pub timestamp: Timestamp,
226    /// Size of the body in bytes
227    pub body_size: u64,
228    /// 64-bit CRC for body data
229    pub crc: u64,
230}
231
232impl Header {
233    /// Header size in bytes
234    pub const SIZE: usize = 58;
235
236    /// Decode a header from a byte slice
237    ///
238    /// # Arguments
239    /// * `buf` - Byte slice containing at least 58 bytes
240    ///
241    /// # Returns
242    /// Decoded header or error if buffer is too short
243    pub fn decode(buf: &[u8]) -> Result<Self> {
244        if buf.len() < Self::SIZE {
245            return Err(IgtlError::InvalidSize {
246                expected: Self::SIZE,
247                actual: buf.len(),
248            });
249        }
250
251        let mut cursor = std::io::Cursor::new(buf);
252
253        // Read version (2 bytes, big-endian)
254        let version = cursor.get_u16();
255
256        // Read type name (12 bytes)
257        let mut type_bytes = [0u8; 12];
258        cursor.copy_to_slice(&mut type_bytes);
259        let type_name = TypeName::from(type_bytes);
260
261        // Read device name (20 bytes)
262        let mut device_bytes = [0u8; 20];
263        cursor.copy_to_slice(&mut device_bytes);
264        let device_name = DeviceName::from(device_bytes);
265
266        // Read timestamp (8 bytes, big-endian) - convert from u64 to Timestamp
267        let timestamp_u64 = cursor.get_u64();
268        let timestamp = Timestamp::from_u64(timestamp_u64);
269
270        // Read body size (8 bytes, big-endian)
271        let body_size = cursor.get_u64();
272
273        // Read CRC (8 bytes, big-endian)
274        let crc = cursor.get_u64();
275
276        Ok(Header {
277            version,
278            type_name,
279            device_name,
280            timestamp,
281            body_size,
282            crc,
283        })
284    }
285
286    /// Encode the header into a byte vector
287    ///
288    /// # Returns
289    /// 58-byte vector containing the encoded header
290    pub fn encode(&self) -> Vec<u8> {
291        let mut buf = BytesMut::with_capacity(Self::SIZE);
292
293        // Write version (2 bytes, big-endian)
294        buf.put_u16(self.version);
295
296        // Write type name (12 bytes)
297        buf.put_slice(&self.type_name.0);
298
299        // Write device name (20 bytes)
300        buf.put_slice(&self.device_name.0);
301
302        // Write timestamp (8 bytes, big-endian) - convert Timestamp to u64
303        buf.put_u64(self.timestamp.to_u64());
304
305        // Write body size (8 bytes, big-endian)
306        buf.put_u64(self.body_size);
307
308        // Write CRC (8 bytes, big-endian)
309        buf.put_u64(self.crc);
310
311        buf.to_vec()
312    }
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318
319    #[test]
320    fn test_type_name_creation() {
321        let name = TypeName::new("TRANSFORM").unwrap();
322        assert_eq!(name.as_str().unwrap(), "TRANSFORM");
323    }
324
325    #[test]
326    fn test_type_name_too_long() {
327        let result = TypeName::new("VERY_LONG_TYPE_NAME");
328        assert!(result.is_err());
329    }
330
331    #[test]
332    fn test_device_name_creation() {
333        let name = DeviceName::new("TestDevice").unwrap();
334        assert_eq!(name.as_str().unwrap(), "TestDevice");
335    }
336
337    #[test]
338    fn test_header_size() {
339        assert_eq!(Header::SIZE, 58);
340    }
341
342    #[test]
343    fn test_timestamp_now() {
344        let ts = Timestamp::now();
345        assert!(ts.seconds > 0);
346        assert!(ts.to_f64() > 0.0);
347    }
348
349    #[test]
350    fn test_timestamp_zero() {
351        let ts = Timestamp::zero();
352        assert_eq!(ts.seconds, 0);
353        assert_eq!(ts.fraction, 0);
354        assert_eq!(ts.to_u64(), 0);
355    }
356
357    #[test]
358    fn test_timestamp_conversion() {
359        let ts = Timestamp::new(1000, 0x80000000); // 1000.5 seconds
360        assert_eq!(ts.seconds, 1000);
361        assert_eq!(ts.fraction, 0x80000000);
362
363        // Test to_nanos
364        let nanos = ts.to_nanos();
365        assert_eq!(nanos, 1_000_500_000_000);
366
367        // Test from_nanos roundtrip
368        let ts2 = Timestamp::from_nanos(nanos);
369        assert_eq!(ts2.seconds, ts.seconds);
370        // Allow small rounding error in fraction
371        assert!((ts2.fraction as i64 - ts.fraction as i64).abs() < 100);
372
373        // Test to_f64
374        let f = ts.to_f64();
375        assert!((f - 1000.5).abs() < 0.0001);
376    }
377
378    #[test]
379    fn test_timestamp_u64_roundtrip() {
380        let original = Timestamp::new(1234567890, 0xABCDEF12);
381        let u64_val = original.to_u64();
382        let restored = Timestamp::from_u64(u64_val);
383
384        assert_eq!(restored.seconds, original.seconds);
385        assert_eq!(restored.fraction, original.fraction);
386    }
387
388    #[test]
389    fn test_header_roundtrip() {
390        let original = Header {
391            version: 2,
392            type_name: TypeName::new("TRANSFORM").unwrap(),
393            device_name: DeviceName::new("TestDevice").unwrap(),
394            timestamp: Timestamp::new(1234567890, 0x12345678),
395            body_size: 48,
396            crc: 0xDEADBEEFCAFEBABE,
397        };
398
399        let encoded = original.encode();
400        assert_eq!(encoded.len(), Header::SIZE);
401
402        let decoded = Header::decode(&encoded).unwrap();
403        assert_eq!(decoded.version, original.version);
404        assert_eq!(decoded.type_name, original.type_name);
405        assert_eq!(decoded.device_name, original.device_name);
406        assert_eq!(decoded.timestamp, original.timestamp);
407        assert_eq!(decoded.body_size, original.body_size);
408        assert_eq!(decoded.crc, original.crc);
409    }
410
411    #[test]
412    fn test_header_decode_short_buffer() {
413        let short_buf = vec![0u8; 30];
414        let result = Header::decode(&short_buf);
415        assert!(matches!(result, Err(IgtlError::InvalidSize { .. })));
416    }
417
418    #[test]
419    fn test_big_endian_encoding() {
420        let header = Header {
421            version: 0x0102,
422            type_name: TypeName::new("TEST").unwrap(),
423            device_name: DeviceName::new("DEV").unwrap(),
424            timestamp: Timestamp::from_u64(0x0102030405060708),
425            body_size: 0x090A0B0C0D0E0F10,
426            crc: 0x1112131415161718,
427        };
428
429        let encoded = header.encode();
430
431        // Verify big-endian encoding of version
432        assert_eq!(encoded[0], 0x01);
433        assert_eq!(encoded[1], 0x02);
434
435        // Verify big-endian encoding of timestamp (at offset 34)
436        assert_eq!(encoded[34], 0x01);
437        assert_eq!(encoded[35], 0x02);
438        assert_eq!(encoded[36], 0x03);
439        assert_eq!(encoded[37], 0x04);
440    }
441}