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::error::{IgtlError, Result};
84use crate::protocol::message::Message;
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: [[1.0, 0.0, 0.0, x], [0.0, 1.0, 0.0, y], [0.0, 0.0, 1.0, z]],
163        }
164    }
165}
166
167/// TDATA message containing multiple tracking data elements
168///
169/// # OpenIGTLink Specification
170/// - Message type: "TDATA"
171/// - Each element: NAME (`char[20]`) + TYPE (uint8) + Reserved (uint8) + MATRIX (`float32[12]`)
172/// - Element size: 20 + 1 + 1 + 48 = 70 bytes
173#[derive(Debug, Clone, PartialEq)]
174pub struct TDataMessage {
175    /// List of tracking data elements
176    pub elements: Vec<TrackingDataElement>,
177}
178
179impl TDataMessage {
180    /// Create a new TDATA message with elements
181    pub fn new(elements: Vec<TrackingDataElement>) -> Self {
182        TDataMessage { elements }
183    }
184
185    /// Create an empty TDATA message
186    pub fn empty() -> Self {
187        TDataMessage {
188            elements: Vec::new(),
189        }
190    }
191
192    /// Add a tracking element
193    pub fn add_element(&mut self, element: TrackingDataElement) {
194        self.elements.push(element);
195    }
196
197    /// Get number of tracking elements
198    pub fn len(&self) -> usize {
199        self.elements.len()
200    }
201
202    /// Check if message has no elements
203    pub fn is_empty(&self) -> bool {
204        self.elements.is_empty()
205    }
206}
207
208impl Message for TDataMessage {
209    fn message_type() -> &'static str {
210        "TDATA"
211    }
212
213    fn encode_content(&self) -> Result<Vec<u8>> {
214        let mut buf = Vec::with_capacity(self.elements.len() * 70);
215
216        for element in &self.elements {
217            // Encode NAME (`char[20]`)
218            let mut name_bytes = [0u8; 20];
219            let name_str = element.name.as_bytes();
220            let copy_len = name_str.len().min(19);
221            name_bytes[..copy_len].copy_from_slice(&name_str[..copy_len]);
222            buf.extend_from_slice(&name_bytes);
223
224            // Encode TYPE (uint8)
225            buf.put_u8(element.instrument_type as u8);
226
227            // Encode Reserved (uint8)
228            buf.put_u8(0);
229
230            // Encode MATRIX (`float32[12]`) - upper 3x4 portion
231            for row in &element.matrix {
232                for &val in row {
233                    buf.put_f32(val);
234                }
235            }
236        }
237
238        Ok(buf)
239    }
240
241    fn decode_content(mut data: &[u8]) -> Result<Self> {
242        let mut elements = Vec::new();
243
244        while data.len() >= 70 {
245            // Decode NAME (`char[20]`)
246            let name_bytes = &data[..20];
247            data.advance(20);
248
249            let name_len = name_bytes.iter().position(|&b| b == 0).unwrap_or(20);
250            let name = String::from_utf8(name_bytes[..name_len].to_vec())?;
251
252            // Decode TYPE (uint8)
253            let instrument_type = TrackingInstrumentType::from_u8(data.get_u8())?;
254
255            // Decode Reserved (uint8)
256            let _reserved = data.get_u8();
257
258            // Decode MATRIX (`float32[12]`)
259            let mut matrix = [[0.0f32; 4]; 3];
260            for row in &mut matrix {
261                for val in row {
262                    *val = data.get_f32();
263                }
264            }
265
266            elements.push(TrackingDataElement {
267                name,
268                instrument_type,
269                matrix,
270            });
271        }
272
273        if !data.is_empty() {
274            return Err(IgtlError::InvalidSize {
275                expected: 0,
276                actual: data.len(),
277            });
278        }
279
280        Ok(TDataMessage { elements })
281    }
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287
288    #[test]
289    fn test_message_type() {
290        assert_eq!(TDataMessage::message_type(), "TDATA");
291    }
292
293    #[test]
294    fn test_instrument_type() {
295        assert_eq!(TrackingInstrumentType::Tracker as u8, 1);
296        assert_eq!(TrackingInstrumentType::Instrument6D as u8, 2);
297        assert_eq!(TrackingInstrumentType::Instrument3D as u8, 3);
298        assert_eq!(TrackingInstrumentType::Instrument5D as u8, 4);
299    }
300
301    #[test]
302    fn test_empty() {
303        let msg = TDataMessage::empty();
304        assert!(msg.is_empty());
305        assert_eq!(msg.len(), 0);
306    }
307
308    #[test]
309    fn test_identity() {
310        let elem = TrackingDataElement::identity("Tool1", TrackingInstrumentType::Instrument6D);
311        assert_eq!(elem.matrix[0], [1.0, 0.0, 0.0, 0.0]);
312        assert_eq!(elem.matrix[1], [0.0, 1.0, 0.0, 0.0]);
313        assert_eq!(elem.matrix[2], [0.0, 0.0, 1.0, 0.0]);
314    }
315
316    #[test]
317    fn test_with_translation() {
318        let elem = TrackingDataElement::with_translation(
319            "Tool1",
320            TrackingInstrumentType::Tracker,
321            10.0,
322            20.0,
323            30.0,
324        );
325        assert_eq!(elem.matrix[0][3], 10.0);
326        assert_eq!(elem.matrix[1][3], 20.0);
327        assert_eq!(elem.matrix[2][3], 30.0);
328    }
329
330    #[test]
331    fn test_add_element() {
332        let mut msg = TDataMessage::empty();
333        msg.add_element(TrackingDataElement::identity(
334            "Tool1",
335            TrackingInstrumentType::Tracker,
336        ));
337        assert_eq!(msg.len(), 1);
338    }
339
340    #[test]
341    fn test_encode_single_element() {
342        let elem = TrackingDataElement::identity("Test", TrackingInstrumentType::Instrument6D);
343        let msg = TDataMessage::new(vec![elem]);
344        let encoded = msg.encode_content().unwrap();
345
346        assert_eq!(encoded.len(), 70);
347        // Check TYPE field
348        assert_eq!(encoded[20], 2); // Instrument6D
349                                    // Check Reserved field
350        assert_eq!(encoded[21], 0);
351    }
352
353    #[test]
354    fn test_roundtrip_single() {
355        let matrix = [
356            [1.0, 0.0, 0.0, 10.0],
357            [0.0, 1.0, 0.0, 20.0],
358            [0.0, 0.0, 1.0, 30.0],
359        ];
360
361        let original = TDataMessage::new(vec![TrackingDataElement::new(
362            "Tracker1",
363            TrackingInstrumentType::Tracker,
364            matrix,
365        )]);
366
367        let encoded = original.encode_content().unwrap();
368        let decoded = TDataMessage::decode_content(&encoded).unwrap();
369
370        assert_eq!(decoded.elements.len(), 1);
371        assert_eq!(decoded.elements[0].name, "Tracker1");
372        assert_eq!(
373            decoded.elements[0].instrument_type,
374            TrackingInstrumentType::Tracker
375        );
376        assert_eq!(decoded.elements[0].matrix, matrix);
377    }
378
379    #[test]
380    fn test_roundtrip_multiple() {
381        let original = TDataMessage::new(vec![
382            TrackingDataElement::identity("Tool1", TrackingInstrumentType::Instrument6D),
383            TrackingDataElement::with_translation(
384                "Tool2",
385                TrackingInstrumentType::Instrument3D,
386                5.0,
387                10.0,
388                15.0,
389            ),
390        ]);
391
392        let encoded = original.encode_content().unwrap();
393        let decoded = TDataMessage::decode_content(&encoded).unwrap();
394
395        assert_eq!(decoded.elements.len(), 2);
396        assert_eq!(decoded.elements[0].name, "Tool1");
397        assert_eq!(decoded.elements[1].name, "Tool2");
398    }
399
400    #[test]
401    fn test_name_truncation() {
402        let long_name = "ThisIsAVeryLongNameThatExceedsTwentyCharacters";
403        let elem = TrackingDataElement::identity(long_name, TrackingInstrumentType::Tracker);
404        let msg = TDataMessage::new(vec![elem]);
405
406        let encoded = msg.encode_content().unwrap();
407        let decoded = TDataMessage::decode_content(&encoded).unwrap();
408
409        assert!(decoded.elements[0].name.len() <= 19);
410    }
411
412    #[test]
413    fn test_empty_message() {
414        let msg = TDataMessage::empty();
415        let encoded = msg.encode_content().unwrap();
416        let decoded = TDataMessage::decode_content(&encoded).unwrap();
417
418        assert_eq!(decoded.elements.len(), 0);
419        assert_eq!(encoded.len(), 0);
420    }
421
422    #[test]
423    fn test_decode_invalid_size() {
424        let data = vec![0u8; 69]; // One byte short
425        let result = TDataMessage::decode_content(&data);
426        assert!(result.is_err());
427    }
428}