openigtlink_rust/protocol/types/
tdata.rs

1//! TDATA (TrackingData) message type implementation
2//!
3//! The TDATA message type is used to transfer 3D positions and orientations
4//! of surgical tools, markers, etc. using transformation matrices.
5//!
6//! # Use Cases
7//!
8//! - **Surgical Tool Tracking** - Real-time tracking of multiple instruments (scalpel, probe, drill)
9//! - **Optical Tracking Systems** - NDI Polaris, Atracsys, or similar optical trackers
10//! - **Electromagnetic Tracking** - Aurora or Ascension electromagnetic trackers
11//! - **Surgical Navigation** - Displaying tool positions relative to patient anatomy
12//! - **Multi-Tool Coordination** - Tracking multiple tools simultaneously in robotic surgery
13//!
14//! # TDATA vs TRANSFORM
15//!
16//! - **TDATA**: Array of named transforms, efficient for tracking multiple tools
17//! - **TRANSFORM**: Single 4x4 matrix, simpler but requires multiple messages
18//!
19//! Use TDATA when tracking ≥2 tools to reduce network overhead.
20//!
21//! # Examples
22//!
23//! ## Tracking Multiple Surgical Instruments
24//!
25//! ```no_run
26//! use openigtlink_rust::protocol::types::{TDataMessage, TrackingDataElement, TrackingInstrumentType};
27//! use openigtlink_rust::protocol::message::IgtlMessage;
28//! use openigtlink_rust::io::ClientBuilder;
29//!
30//! let mut client = ClientBuilder::new()
31//!     .tcp("127.0.0.1:18944")
32//!     .sync()
33//!     .build()?;
34//!
35//! // Tool 1: Scalpel
36//! let scalpel = TrackingDataElement::with_translation(
37//!     "Scalpel",
38//!     TrackingInstrumentType::Instrument5D,
39//!     100.0,  // X translation: 100mm
40//!     50.0,   // Y translation: 50mm
41//!     200.0   // Z translation: 200mm
42//! );
43//!
44//! // Tool 2: Probe
45//! let probe = TrackingDataElement::with_translation(
46//!     "Probe",
47//!     TrackingInstrumentType::Instrument6D,
48//!     150.0,
49//!     75.0,
50//!     180.0
51//! );
52//!
53//! let tdata = TDataMessage::new(vec![scalpel, probe]);
54//! let msg = IgtlMessage::new(tdata, "OpticalTracker")?;
55//! client.send(&msg)?;
56//! # Ok::<(), openigtlink_rust::IgtlError>(())
57//! ```
58//!
59//! ## Receiving Tracking Data at 60Hz
60//!
61//! ```no_run
62//! use openigtlink_rust::io::IgtlServer;
63//! use openigtlink_rust::protocol::types::TDataMessage;
64//!
65//! let server = IgtlServer::bind("0.0.0.0:18944")?;
66//! let mut client_conn = server.accept()?;
67//!
68//! loop {
69//!     let message = client_conn.receive::<TDataMessage>()?;
70//!
71//!     for element in &message.content.elements {
72//!         let x = element.matrix[0][3];
73//!         let y = element.matrix[1][3];
74//!         let z = element.matrix[2][3];
75//!
76//!         println!("{}: position = ({:.2}, {:.2}, {:.2}) mm",
77//!                  element.name, x, y, z);
78//!     }
79//! }
80//! # Ok::<(), openigtlink_rust::IgtlError>(())
81//! ```
82
83use crate::protocol::message::Message;
84use crate::error::{IgtlError, Result};
85use bytes::{Buf, BufMut};
86
87/// Instrument type for tracking data
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89#[repr(u8)]
90pub enum TrackingInstrumentType {
91    Tracker = 1,
92    Instrument6D = 2,
93    Instrument3D = 3,
94    Instrument5D = 4,
95}
96
97impl TrackingInstrumentType {
98    fn from_u8(value: u8) -> Result<Self> {
99        match value {
100            1 => Ok(TrackingInstrumentType::Tracker),
101            2 => Ok(TrackingInstrumentType::Instrument6D),
102            3 => Ok(TrackingInstrumentType::Instrument3D),
103            4 => Ok(TrackingInstrumentType::Instrument5D),
104            _ => Err(IgtlError::InvalidHeader(format!(
105                "Invalid tracking instrument type: {}",
106                value
107            ))),
108        }
109    }
110}
111
112/// Tracking data element with name, type, and transformation matrix
113#[derive(Debug, Clone, PartialEq)]
114pub struct TrackingDataElement {
115    /// Name/ID of the instrument/tracker (max 20 chars)
116    pub name: String,
117    /// Type of instrument
118    pub instrument_type: TrackingInstrumentType,
119    /// Upper 3x4 portion of 4x4 transformation matrix (row-major)
120    /// Last row [0, 0, 0, 1] is implicit
121    pub matrix: [[f32; 4]; 3],
122}
123
124impl TrackingDataElement {
125    /// Create a new tracking data element
126    pub fn new(
127        name: impl Into<String>,
128        instrument_type: TrackingInstrumentType,
129        matrix: [[f32; 4]; 3],
130    ) -> Self {
131        TrackingDataElement {
132            name: name.into(),
133            instrument_type,
134            matrix,
135        }
136    }
137
138    /// Create an identity transformation
139    pub fn identity(name: impl Into<String>, instrument_type: TrackingInstrumentType) -> Self {
140        TrackingDataElement {
141            name: name.into(),
142            instrument_type,
143            matrix: [
144                [1.0, 0.0, 0.0, 0.0],
145                [0.0, 1.0, 0.0, 0.0],
146                [0.0, 0.0, 1.0, 0.0],
147            ],
148        }
149    }
150
151    /// Create with translation only
152    pub fn with_translation(
153        name: impl Into<String>,
154        instrument_type: TrackingInstrumentType,
155        x: f32,
156        y: f32,
157        z: f32,
158    ) -> Self {
159        TrackingDataElement {
160            name: name.into(),
161            instrument_type,
162            matrix: [
163                [1.0, 0.0, 0.0, x],
164                [0.0, 1.0, 0.0, y],
165                [0.0, 0.0, 1.0, z],
166            ],
167        }
168    }
169}
170
171/// TDATA message containing multiple tracking data elements
172///
173/// # OpenIGTLink Specification
174/// - Message type: "TDATA"
175/// - Each element: NAME (char[20]) + TYPE (uint8) + Reserved (uint8) + MATRIX (float32[12])
176/// - Element size: 20 + 1 + 1 + 48 = 70 bytes
177#[derive(Debug, Clone, PartialEq)]
178pub struct TDataMessage {
179    /// List of tracking data elements
180    pub elements: Vec<TrackingDataElement>,
181}
182
183impl TDataMessage {
184    /// Create a new TDATA message with elements
185    pub fn new(elements: Vec<TrackingDataElement>) -> Self {
186        TDataMessage { elements }
187    }
188
189    /// Create an empty TDATA message
190    pub fn empty() -> Self {
191        TDataMessage {
192            elements: Vec::new(),
193        }
194    }
195
196    /// Add a tracking element
197    pub fn add_element(&mut self, element: TrackingDataElement) {
198        self.elements.push(element);
199    }
200
201    /// Get number of tracking elements
202    pub fn len(&self) -> usize {
203        self.elements.len()
204    }
205
206    /// Check if message has no elements
207    pub fn is_empty(&self) -> bool {
208        self.elements.is_empty()
209    }
210}
211
212impl Message for TDataMessage {
213    fn message_type() -> &'static str {
214        "TDATA"
215    }
216
217    fn encode_content(&self) -> Result<Vec<u8>> {
218        let mut buf = Vec::with_capacity(self.elements.len() * 70);
219
220        for element in &self.elements {
221            // Encode NAME (char[20])
222            let mut name_bytes = [0u8; 20];
223            let name_str = element.name.as_bytes();
224            let copy_len = name_str.len().min(19);
225            name_bytes[..copy_len].copy_from_slice(&name_str[..copy_len]);
226            buf.extend_from_slice(&name_bytes);
227
228            // Encode TYPE (uint8)
229            buf.put_u8(element.instrument_type as u8);
230
231            // Encode Reserved (uint8)
232            buf.put_u8(0);
233
234            // Encode MATRIX (float32[12]) - upper 3x4 portion
235            for row in &element.matrix {
236                for &val in row {
237                    buf.put_f32(val);
238                }
239            }
240        }
241
242        Ok(buf)
243    }
244
245    fn decode_content(mut data: &[u8]) -> Result<Self> {
246        let mut elements = Vec::new();
247
248        while data.len() >= 70 {
249            // Decode NAME (char[20])
250            let name_bytes = &data[..20];
251            data.advance(20);
252
253            let name_len = name_bytes.iter().position(|&b| b == 0).unwrap_or(20);
254            let name = String::from_utf8(name_bytes[..name_len].to_vec())?;
255
256            // Decode TYPE (uint8)
257            let instrument_type = TrackingInstrumentType::from_u8(data.get_u8())?;
258
259            // Decode Reserved (uint8)
260            let _reserved = data.get_u8();
261
262            // Decode MATRIX (float32[12])
263            let mut matrix = [[0.0f32; 4]; 3];
264            for row in &mut matrix {
265                for val in row {
266                    *val = data.get_f32();
267                }
268            }
269
270            elements.push(TrackingDataElement {
271                name,
272                instrument_type,
273                matrix,
274            });
275        }
276
277        if !data.is_empty() {
278            return Err(IgtlError::InvalidSize {
279                expected: 0,
280                actual: data.len(),
281            });
282        }
283
284        Ok(TDataMessage { elements })
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291
292    #[test]
293    fn test_message_type() {
294        assert_eq!(TDataMessage::message_type(), "TDATA");
295    }
296
297    #[test]
298    fn test_instrument_type() {
299        assert_eq!(TrackingInstrumentType::Tracker as u8, 1);
300        assert_eq!(TrackingInstrumentType::Instrument6D as u8, 2);
301        assert_eq!(TrackingInstrumentType::Instrument3D as u8, 3);
302        assert_eq!(TrackingInstrumentType::Instrument5D as u8, 4);
303    }
304
305    #[test]
306    fn test_empty() {
307        let msg = TDataMessage::empty();
308        assert!(msg.is_empty());
309        assert_eq!(msg.len(), 0);
310    }
311
312    #[test]
313    fn test_identity() {
314        let elem = TrackingDataElement::identity("Tool1", TrackingInstrumentType::Instrument6D);
315        assert_eq!(elem.matrix[0], [1.0, 0.0, 0.0, 0.0]);
316        assert_eq!(elem.matrix[1], [0.0, 1.0, 0.0, 0.0]);
317        assert_eq!(elem.matrix[2], [0.0, 0.0, 1.0, 0.0]);
318    }
319
320    #[test]
321    fn test_with_translation() {
322        let elem = TrackingDataElement::with_translation(
323            "Tool1",
324            TrackingInstrumentType::Tracker,
325            10.0,
326            20.0,
327            30.0,
328        );
329        assert_eq!(elem.matrix[0][3], 10.0);
330        assert_eq!(elem.matrix[1][3], 20.0);
331        assert_eq!(elem.matrix[2][3], 30.0);
332    }
333
334    #[test]
335    fn test_add_element() {
336        let mut msg = TDataMessage::empty();
337        msg.add_element(TrackingDataElement::identity(
338            "Tool1",
339            TrackingInstrumentType::Tracker,
340        ));
341        assert_eq!(msg.len(), 1);
342    }
343
344    #[test]
345    fn test_encode_single_element() {
346        let elem = TrackingDataElement::identity("Test", TrackingInstrumentType::Instrument6D);
347        let msg = TDataMessage::new(vec![elem]);
348        let encoded = msg.encode_content().unwrap();
349
350        assert_eq!(encoded.len(), 70);
351        // Check TYPE field
352        assert_eq!(encoded[20], 2); // Instrument6D
353        // Check Reserved field
354        assert_eq!(encoded[21], 0);
355    }
356
357    #[test]
358    fn test_roundtrip_single() {
359        let matrix = [
360            [1.0, 0.0, 0.0, 10.0],
361            [0.0, 1.0, 0.0, 20.0],
362            [0.0, 0.0, 1.0, 30.0],
363        ];
364
365        let original = TDataMessage::new(vec![TrackingDataElement::new(
366            "Tracker1",
367            TrackingInstrumentType::Tracker,
368            matrix,
369        )]);
370
371        let encoded = original.encode_content().unwrap();
372        let decoded = TDataMessage::decode_content(&encoded).unwrap();
373
374        assert_eq!(decoded.elements.len(), 1);
375        assert_eq!(decoded.elements[0].name, "Tracker1");
376        assert_eq!(
377            decoded.elements[0].instrument_type,
378            TrackingInstrumentType::Tracker
379        );
380        assert_eq!(decoded.elements[0].matrix, matrix);
381    }
382
383    #[test]
384    fn test_roundtrip_multiple() {
385        let original = TDataMessage::new(vec![
386            TrackingDataElement::identity("Tool1", TrackingInstrumentType::Instrument6D),
387            TrackingDataElement::with_translation(
388                "Tool2",
389                TrackingInstrumentType::Instrument3D,
390                5.0,
391                10.0,
392                15.0,
393            ),
394        ]);
395
396        let encoded = original.encode_content().unwrap();
397        let decoded = TDataMessage::decode_content(&encoded).unwrap();
398
399        assert_eq!(decoded.elements.len(), 2);
400        assert_eq!(decoded.elements[0].name, "Tool1");
401        assert_eq!(decoded.elements[1].name, "Tool2");
402    }
403
404    #[test]
405    fn test_name_truncation() {
406        let long_name = "ThisIsAVeryLongNameThatExceedsTwentyCharacters";
407        let elem = TrackingDataElement::identity(long_name, TrackingInstrumentType::Tracker);
408        let msg = TDataMessage::new(vec![elem]);
409
410        let encoded = msg.encode_content().unwrap();
411        let decoded = TDataMessage::decode_content(&encoded).unwrap();
412
413        assert!(decoded.elements[0].name.len() <= 19);
414    }
415
416    #[test]
417    fn test_empty_message() {
418        let msg = TDataMessage::empty();
419        let encoded = msg.encode_content().unwrap();
420        let decoded = TDataMessage::decode_content(&encoded).unwrap();
421
422        assert_eq!(decoded.elements.len(), 0);
423        assert_eq!(encoded.len(), 0);
424    }
425
426    #[test]
427    fn test_decode_invalid_size() {
428        let data = vec![0u8; 69]; // One byte short
429        let result = TDataMessage::decode_content(&data);
430        assert!(result.is_err());
431    }
432}