Skip to main content

rust_ethernet_ip/
udt.rs

1use crate::error::{EtherNetIpError, Result};
2use crate::PlcValue;
3use std::collections::HashMap;
4
5/// Definition of a User Defined Type
6#[derive(Debug, Clone)]
7pub struct UdtDefinition {
8    pub name: String,
9    pub members: Vec<UdtMember>,
10}
11
12/// Member of a UDT
13#[derive(Debug, Clone)]
14pub struct UdtMember {
15    pub name: String,
16    pub data_type: u16,
17    pub offset: u32,
18    pub size: u32,
19}
20
21/// UDT Template information from PLC
22#[derive(Debug, Clone)]
23pub struct UdtTemplate {
24    pub template_id: u32,
25    pub name: String,
26    pub size: u32,
27    pub member_count: u16,
28    pub members: Vec<UdtMember>,
29}
30
31/// Tag attributes from PLC
32#[derive(Debug, Clone)]
33pub struct TagAttributes {
34    pub name: String,
35    pub data_type: u16,
36    pub data_type_name: String,
37    pub dimensions: Vec<u32>,
38    pub permissions: TagPermissions,
39    pub scope: TagScope,
40    pub template_instance_id: Option<u32>,
41    pub size: u32,
42}
43
44/// Tag permissions
45#[derive(Debug, Clone, PartialEq)]
46pub enum TagPermissions {
47    ReadOnly,
48    ReadWrite,
49    WriteOnly,
50    Unknown,
51}
52
53/// Tag scope
54#[derive(Debug, Clone, PartialEq)]
55pub enum TagScope {
56    Controller,
57    Program(String),
58    Unknown,
59}
60
61/// Manager for UDT operations
62#[derive(Debug)]
63pub struct UdtManager {
64    definitions: HashMap<String, UdtDefinition>,
65    templates: HashMap<u32, UdtTemplate>,
66    tag_attributes: HashMap<String, TagAttributes>,
67}
68
69impl UdtManager {
70    pub fn new() -> Self {
71        Self {
72            definitions: HashMap::new(),
73            templates: HashMap::new(),
74            tag_attributes: HashMap::new(),
75        }
76    }
77
78    /// Adds a UDT definition to the cache
79    pub fn add_definition(&mut self, definition: UdtDefinition) {
80        self.definitions.insert(definition.name.clone(), definition);
81    }
82
83    /// Gets a cached UDT definition
84    pub fn get_definition(&self, name: &str) -> Option<&UdtDefinition> {
85        self.definitions.get(name)
86    }
87
88    /// Adds a UDT template to the cache
89    pub fn add_template(&mut self, template: UdtTemplate) {
90        self.templates.insert(template.template_id, template);
91    }
92
93    /// Gets a cached UDT template
94    pub fn get_template(&self, template_id: u32) -> Option<&UdtTemplate> {
95        self.templates.get(&template_id)
96    }
97
98    /// Adds tag attributes to the cache
99    pub fn add_tag_attributes(&mut self, attributes: TagAttributes) {
100        self.tag_attributes
101            .insert(attributes.name.clone(), attributes);
102    }
103
104    /// Gets cached tag attributes
105    pub fn get_tag_attributes(&self, name: &str) -> Option<&TagAttributes> {
106        self.tag_attributes.get(name)
107    }
108
109    /// Lists all cached UDT definitions
110    pub fn list_definitions(&self) -> Vec<String> {
111        self.definitions.keys().cloned().collect()
112    }
113
114    /// Lists all cached templates
115    pub fn list_templates(&self) -> Vec<u32> {
116        self.templates.keys().cloned().collect()
117    }
118
119    /// Lists all cached tag attributes
120    pub fn list_tag_attributes(&self) -> Vec<String> {
121        self.tag_attributes.keys().cloned().collect()
122    }
123
124    /// Clears all caches
125    pub fn clear_cache(&mut self) {
126        self.definitions.clear();
127        self.templates.clear();
128        self.tag_attributes.clear();
129    }
130
131    /// Parses UDT template data from CIP response
132    pub fn parse_udt_template(&self, template_id: u32, data: &[u8]) -> Result<UdtTemplate> {
133        if data.len() < 8 {
134            return Err(EtherNetIpError::Protocol(
135                "UDT template data too short".to_string(),
136            ));
137        }
138
139        let mut offset = 0;
140
141        // Parse template header
142        let structure_size = u32::from_le_bytes([
143            data[offset],
144            data[offset + 1],
145            data[offset + 2],
146            data[offset + 3],
147        ]);
148        offset += 4;
149
150        let member_count = u16::from_le_bytes([data[offset], data[offset + 1]]);
151        offset += 2;
152
153        // Skip reserved bytes
154        offset += 2;
155
156        let mut members = Vec::new();
157        let mut current_offset = 0u32;
158
159        // Parse each member
160        for i in 0..member_count {
161            if offset + 8 > data.len() {
162                return Err(EtherNetIpError::Protocol(format!(
163                    "UDT template member {} data incomplete",
164                    i
165                )));
166            }
167
168            // Parse member info
169            let member_info = u32::from_le_bytes([
170                data[offset],
171                data[offset + 1],
172                data[offset + 2],
173                data[offset + 3],
174            ]);
175            offset += 4;
176
177            let member_name_length = u16::from_le_bytes([data[offset], data[offset + 1]]);
178            offset += 2;
179
180            // Skip reserved bytes
181            offset += 2;
182
183            // Extract member properties from member_info
184            let data_type = (member_info & 0xFFFF) as u16;
185            let _dimensions = ((member_info >> 16) & 0xFF) as u8;
186
187            // Read member name
188            if offset + member_name_length as usize > data.len() {
189                return Err(EtherNetIpError::Protocol(format!(
190                    "UDT template member {} name data incomplete",
191                    i
192                )));
193            }
194
195            let name_bytes = &data[offset..offset + member_name_length as usize];
196            let member_name = String::from_utf8_lossy(name_bytes).to_string();
197            offset += member_name_length as usize;
198
199            // Align to 4-byte boundary
200            offset = (offset + 3) & !3;
201
202            // Calculate member size based on data type
203            let member_size = self.get_data_type_size(data_type);
204
205            // Create member
206            let member = UdtMember {
207                name: member_name,
208                data_type,
209                offset: current_offset,
210                size: member_size,
211            };
212
213            members.push(member);
214            current_offset += member_size;
215        }
216
217        Ok(UdtTemplate {
218            template_id,
219            name: format!("Template_{}", template_id),
220            size: structure_size,
221            member_count,
222            members,
223        })
224    }
225
226    /// Gets the size of a data type in bytes
227    fn get_data_type_size(&self, data_type: u16) -> u32 {
228        match data_type {
229            0x00C1 => 1,  // BOOL
230            0x00C2 => 1,  // SINT (8-bit signed)
231            0x00C3 => 2,  // INT (16-bit signed)
232            0x00C4 => 4,  // DINT (32-bit signed)
233            0x00C5 => 8,  // LINT (64-bit signed)
234            0x00C6 => 1,  // USINT (8-bit unsigned)
235            0x00C7 => 2,  // UINT (16-bit unsigned)
236            0x00C8 => 4,  // UDINT (32-bit unsigned)
237            0x00C9 => 8,  // ULINT (64-bit unsigned)
238            0x00CA => 4,  // REAL (32-bit float)
239            0x00CB => 8,  // LREAL (64-bit float)
240            0x00CE => 88, // STRING (4-byte DINT length + 82 chars + 2 padding)
241            _ => 4,       // Default to 4 bytes for unknown types
242        }
243    }
244
245    /// Parses tag attributes from CIP response
246    pub fn parse_tag_attributes(&self, tag_name: &str, data: &[u8]) -> Result<TagAttributes> {
247        if data.len() < 8 {
248            return Err(EtherNetIpError::Protocol(
249                "Tag attributes data too short".to_string(),
250            ));
251        }
252
253        let mut offset = 0;
254
255        // Parse data type
256        let data_type = u16::from_le_bytes([data[offset], data[offset + 1]]);
257        offset += 2;
258
259        // Parse size
260        let size = u32::from_le_bytes([
261            data[offset],
262            data[offset + 1],
263            data[offset + 2],
264            data[offset + 3],
265        ]);
266        offset += 4;
267
268        // Parse dimensions (if present)
269        let mut dimensions = Vec::new();
270        if data.len() > offset {
271            let dimension_count = data[offset] as usize;
272            offset += 1;
273
274            for _ in 0..dimension_count {
275                if offset + 4 <= data.len() {
276                    let dim = u32::from_le_bytes([
277                        data[offset],
278                        data[offset + 1],
279                        data[offset + 2],
280                        data[offset + 3],
281                    ]);
282                    dimensions.push(dim);
283                    offset += 4;
284                }
285            }
286        }
287
288        // Parse permissions (simplified - would need more CIP data)
289        let permissions = TagPermissions::ReadWrite; // Default assumption
290
291        // Parse scope (simplified - would need more CIP data)
292        let scope = if tag_name.contains(':') {
293            let parts: Vec<&str> = tag_name.split(':').collect();
294            if parts.len() >= 2 {
295                TagScope::Program(parts[0].to_string())
296            } else {
297                TagScope::Controller
298            }
299        } else {
300            TagScope::Controller
301        };
302
303        // Get data type name
304        let data_type_name = self.get_data_type_name(data_type);
305
306        // Check if this is a UDT (has template instance ID)
307        let template_instance_id = if data_type == 0x00A0 {
308            // UDT type
309            Some(0) // Would need to extract from additional CIP data
310        } else {
311            None
312        };
313
314        Ok(TagAttributes {
315            name: tag_name.to_string(),
316            data_type,
317            data_type_name,
318            dimensions,
319            permissions,
320            scope,
321            template_instance_id,
322            size,
323        })
324    }
325
326    /// Gets the human-readable name of a data type
327    fn get_data_type_name(&self, data_type: u16) -> String {
328        match data_type {
329            0x00C1 => "BOOL".to_string(),
330            0x00C2 => "SINT".to_string(),
331            0x00C3 => "INT".to_string(),
332            0x00C4 => "DINT".to_string(),
333            0x00C5 => "LINT".to_string(),
334            0x00C6 => "USINT".to_string(),
335            0x00C7 => "UINT".to_string(),
336            0x00C8 => "UDINT".to_string(),
337            0x00C9 => "ULINT".to_string(),
338            0x00CA => "REAL".to_string(),
339            0x00CB => "LREAL".to_string(),
340            0x00CE => "STRING".to_string(),
341            0x00A0 => "UDT".to_string(),
342            _ => format!("UNKNOWN(0x{:04X})", data_type),
343        }
344    }
345
346    /// Parse a UDT instance from raw bytes
347    ///
348    /// Returns raw UDT data in generic format. Note: symbol_id will be 0
349    /// since it's not available in this context. For proper UDT handling with
350    /// symbol_id, use read_tag() which gets tag attributes first.
351    pub fn parse_udt_instance(&self, _udt_name: &str, data: &[u8]) -> Result<PlcValue> {
352        // Return raw UDT data in generic format
353        // symbol_id is 0 since it's not available in this context
354        Ok(PlcValue::Udt(crate::UdtData {
355            symbol_id: 0, // Not available in this context
356            data: data.to_vec(),
357        }))
358    }
359
360    /// Serialize a UDT instance to bytes
361    pub fn serialize_udt_instance(
362        &self,
363        _udt_value: &HashMap<String, PlcValue>,
364    ) -> Result<Vec<u8>> {
365        Err(crate::error::EtherNetIpError::Protocol(
366            "UDT instance serialization is not implemented yet".to_string(),
367        ))
368    }
369}
370
371impl Default for UdtManager {
372    fn default() -> Self {
373        Self::new()
374    }
375}
376
377// Note: Types are already defined above, no need to re-export
378
379/// Represents a User Defined Type (UDT)
380#[derive(Debug, Clone)]
381pub struct UserDefinedType {
382    /// Name of the UDT
383    pub name: String,
384    /// Total size of the UDT in bytes
385    pub size: u32,
386    /// Members of the UDT
387    pub members: Vec<UdtMember>,
388    /// Cache of member offsets for quick lookup
389    member_offsets: HashMap<String, u32>,
390}
391
392impl UserDefinedType {
393    /// Creates a new UDT
394    pub fn new(name: String) -> Self {
395        Self {
396            name,
397            size: 0,
398            members: Vec::new(),
399            member_offsets: HashMap::new(),
400        }
401    }
402
403    /// Adds a member to the UDT
404    pub fn add_member(&mut self, member: UdtMember) {
405        self.member_offsets
406            .insert(member.name.clone(), member.offset);
407        self.members.push(member);
408        // Calculate total size including padding
409        self.size = self
410            .members
411            .iter()
412            .map(|m| m.offset + m.size)
413            .max()
414            .unwrap_or(0);
415    }
416
417    /// Gets the offset of a member by name
418    pub fn get_member_offset(&self, name: &str) -> Option<u32> {
419        self.member_offsets.get(name).copied()
420    }
421
422    /// Parses a UDT from CIP data
423    pub fn from_cip_data(_data: &[u8]) -> crate::error::Result<Self> {
424        Err(crate::error::EtherNetIpError::Protocol(
425            "UDT CIP definition parsing is not implemented yet".to_string(),
426        ))
427    }
428
429    /// Converts a UDT instance to a `HashMap` of member values
430    pub fn to_hash_map(&self, data: &[u8]) -> crate::error::Result<HashMap<String, PlcValue>> {
431        if data.is_empty() {
432            return Err(crate::error::EtherNetIpError::Protocol(
433                "UDT data is empty".to_string(),
434            ));
435        }
436
437        let mut result = HashMap::new();
438
439        for member in &self.members {
440            let offset = member.offset as usize;
441            if offset + member.size as usize <= data.len() {
442                let member_data = &data[offset..offset + member.size as usize];
443                let value = self.parse_member_value(member, member_data)?;
444                result.insert(member.name.clone(), value);
445            }
446        }
447
448        Ok(result)
449    }
450
451    /// Converts a `HashMap` of member values to raw UDT bytes
452    pub fn from_hash_map(
453        &self,
454        values: &HashMap<String, PlcValue>,
455    ) -> crate::error::Result<Vec<u8>> {
456        let mut data = vec![0u8; self.size as usize];
457
458        for member in &self.members {
459            if let Some(value) = values.get(&member.name) {
460                let member_data = self.serialize_member_value(member, value)?;
461                let offset = member.offset as usize;
462                let end_offset = offset + member_data.len();
463
464                if end_offset <= data.len() {
465                    data[offset..end_offset].copy_from_slice(&member_data);
466                } else {
467                    return Err(crate::error::EtherNetIpError::Protocol(format!(
468                        "Member {} data exceeds UDT size",
469                        member.name
470                    )));
471                }
472            }
473        }
474
475        Ok(data)
476    }
477
478    /// Reads a specific UDT member by name
479    pub fn read_member(&self, data: &[u8], member_name: &str) -> crate::error::Result<PlcValue> {
480        if let Some(member) = self.members.iter().find(|m| m.name == member_name) {
481            let offset = member.offset as usize;
482            if offset + member.size as usize <= data.len() {
483                let member_data = &data[offset..offset + member.size as usize];
484                self.parse_member_value(member, member_data)
485            } else {
486                Err(crate::error::EtherNetIpError::Protocol(format!(
487                    "Member {} data incomplete",
488                    member_name
489                )))
490            }
491        } else {
492            Err(crate::error::EtherNetIpError::TagNotFound(format!(
493                "UDT member '{}' not found",
494                member_name
495            )))
496        }
497    }
498
499    /// Writes a specific UDT member by name
500    pub fn write_member(
501        &self,
502        data: &mut [u8],
503        member_name: &str,
504        value: &PlcValue,
505    ) -> crate::error::Result<()> {
506        if let Some(member) = self.members.iter().find(|m| m.name == member_name) {
507            let member_data = self.serialize_member_value(member, value)?;
508            let offset = member.offset as usize;
509            let end_offset = offset + member_data.len();
510
511            if end_offset <= data.len() {
512                data[offset..end_offset].copy_from_slice(&member_data);
513                Ok(())
514            } else {
515                Err(crate::error::EtherNetIpError::Protocol(format!(
516                    "Member {} data exceeds UDT size",
517                    member_name
518                )))
519            }
520        } else {
521            Err(crate::error::EtherNetIpError::TagNotFound(format!(
522                "UDT member '{}' not found",
523                member_name
524            )))
525        }
526    }
527
528    /// Gets the size of a specific member
529    pub fn get_member_size(&self, member_name: &str) -> Option<u32> {
530        self.members
531            .iter()
532            .find(|m| m.name == member_name)
533            .map(|m| m.size)
534    }
535
536    /// Gets the data type of a specific member
537    pub fn get_member_data_type(&self, member_name: &str) -> Option<u16> {
538        self.members
539            .iter()
540            .find(|m| m.name == member_name)
541            .map(|m| m.data_type)
542    }
543
544    /// Parses a member value from raw data
545    pub fn parse_member_value(
546        &self,
547        member: &UdtMember,
548        data: &[u8],
549    ) -> crate::error::Result<PlcValue> {
550        match member.data_type {
551            0x00C1 => {
552                if data.is_empty() {
553                    return Err(crate::error::EtherNetIpError::Protocol(
554                        "BOOL data too short".to_string(),
555                    ));
556                }
557                Ok(PlcValue::Bool(data[0] != 0))
558            }
559            0x00C2 => {
560                // SINT (8-bit signed integer)
561                if data.is_empty() {
562                    return Err(crate::error::EtherNetIpError::Protocol(
563                        "SINT data too short".to_string(),
564                    ));
565                }
566                Ok(PlcValue::Sint(data[0] as i8))
567            }
568            0x00C3 => {
569                // INT (16-bit signed integer)
570                if data.len() < 2 {
571                    return Err(crate::error::EtherNetIpError::Protocol(
572                        "INT data too short".to_string(),
573                    ));
574                }
575                let mut bytes = [0u8; 2];
576                bytes.copy_from_slice(&data[..2]);
577                Ok(PlcValue::Int(i16::from_le_bytes(bytes)))
578            }
579            0x00C4 => {
580                // DINT (32-bit signed integer)
581                if data.len() < 4 {
582                    return Err(crate::error::EtherNetIpError::Protocol(
583                        "DINT data too short".to_string(),
584                    ));
585                }
586                let mut bytes = [0u8; 4];
587                bytes.copy_from_slice(&data[..4]);
588                Ok(PlcValue::Dint(i32::from_le_bytes(bytes)))
589            }
590            0x00C5 => {
591                // LINT (64-bit signed integer)
592                if data.len() < 8 {
593                    return Err(crate::error::EtherNetIpError::Protocol(
594                        "LINT data too short".to_string(),
595                    ));
596                }
597                let mut bytes = [0u8; 8];
598                bytes.copy_from_slice(&data[..8]);
599                Ok(PlcValue::Lint(i64::from_le_bytes(bytes)))
600            }
601            0x00C6 => {
602                // USINT (8-bit unsigned integer)
603                if data.is_empty() {
604                    return Err(crate::error::EtherNetIpError::Protocol(
605                        "USINT data too short".to_string(),
606                    ));
607                }
608                Ok(PlcValue::Usint(data[0]))
609            }
610            0x00C7 => {
611                // UINT (16-bit unsigned integer)
612                if data.len() < 2 {
613                    return Err(crate::error::EtherNetIpError::Protocol(
614                        "UINT data too short".to_string(),
615                    ));
616                }
617                let mut bytes = [0u8; 2];
618                bytes.copy_from_slice(&data[..2]);
619                Ok(PlcValue::Uint(u16::from_le_bytes(bytes)))
620            }
621            0x00C8 => {
622                // UDINT (32-bit unsigned integer)
623                if data.len() < 4 {
624                    return Err(crate::error::EtherNetIpError::Protocol(
625                        "UDINT data too short".to_string(),
626                    ));
627                }
628                let mut bytes = [0u8; 4];
629                bytes.copy_from_slice(&data[..4]);
630                Ok(PlcValue::Udint(u32::from_le_bytes(bytes)))
631            }
632            0x00C9 => {
633                // ULINT (64-bit unsigned integer)
634                if data.len() < 8 {
635                    return Err(crate::error::EtherNetIpError::Protocol(
636                        "ULINT data too short".to_string(),
637                    ));
638                }
639                let mut bytes = [0u8; 8];
640                bytes.copy_from_slice(&data[..8]);
641                Ok(PlcValue::Ulint(u64::from_le_bytes(bytes)))
642            }
643            0x00CA => {
644                // REAL (32-bit float)
645                if data.len() < 4 {
646                    return Err(crate::error::EtherNetIpError::Protocol(
647                        "REAL data too short".to_string(),
648                    ));
649                }
650                let mut bytes = [0u8; 4];
651                bytes.copy_from_slice(&data[..4]);
652                Ok(PlcValue::Real(f32::from_le_bytes(bytes)))
653            }
654            0x00CB => {
655                // LREAL (64-bit float)
656                if data.len() < 8 {
657                    return Err(crate::error::EtherNetIpError::Protocol(
658                        "LREAL data too short".to_string(),
659                    ));
660                }
661                let mut bytes = [0u8; 8];
662                bytes.copy_from_slice(&data[..8]);
663                Ok(PlcValue::Lreal(f64::from_le_bytes(bytes)))
664            }
665            0x00CE => {
666                // STRING type - first 4 bytes are length (DINT), followed by data (up to 82 bytes)
667                if data.len() < 4 {
668                    return Err(crate::error::EtherNetIpError::Protocol(
669                        "STRING data too short".to_string(),
670                    ));
671                }
672                let length = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
673                if data.len() - 4 < length {
674                    return Err(crate::error::EtherNetIpError::Protocol(
675                        "STRING data incomplete".to_string(),
676                    ));
677                }
678                let string_data = &data[4..4 + length];
679                let string_value = String::from_utf8_lossy(string_data).to_string();
680                Ok(PlcValue::String(string_value))
681            }
682            _ => Err(crate::error::EtherNetIpError::Protocol(format!(
683                "Unsupported UDT data type: 0x{:04X}",
684                member.data_type
685            ))),
686        }
687    }
688
689    /// Serializes a member value to raw data
690    pub fn serialize_member_value(
691        &self,
692        member: &UdtMember,
693        value: &PlcValue,
694    ) -> crate::error::Result<Vec<u8>> {
695        match member.data_type {
696            0x00C1 => match value {
697                PlcValue::Bool(b) => Ok(vec![if *b { 0xFF } else { 0x00 }]),
698                _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
699                    expected: "BOOL".to_string(),
700                    actual: format!("{:?}", value),
701                }),
702            },
703            0x00C2 => match value {
704                PlcValue::Sint(s) => Ok(vec![*s as u8]),
705                _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
706                    expected: "SINT".to_string(),
707                    actual: format!("{:?}", value),
708                }),
709            },
710            0x00C3 => match value {
711                PlcValue::Int(i) => Ok(i.to_le_bytes().to_vec()),
712                _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
713                    expected: "INT".to_string(),
714                    actual: format!("{:?}", value),
715                }),
716            },
717            0x00C4 => match value {
718                PlcValue::Dint(d) => Ok(d.to_le_bytes().to_vec()),
719                _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
720                    expected: "DINT".to_string(),
721                    actual: format!("{:?}", value),
722                }),
723            },
724            0x00C5 => match value {
725                PlcValue::Lint(l) => Ok(l.to_le_bytes().to_vec()),
726                _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
727                    expected: "LINT".to_string(),
728                    actual: format!("{:?}", value),
729                }),
730            },
731            0x00C6 => match value {
732                PlcValue::Usint(u) => Ok(vec![*u]),
733                _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
734                    expected: "USINT".to_string(),
735                    actual: format!("{:?}", value),
736                }),
737            },
738            0x00C7 => match value {
739                PlcValue::Uint(u) => Ok(u.to_le_bytes().to_vec()),
740                _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
741                    expected: "UINT".to_string(),
742                    actual: format!("{:?}", value),
743                }),
744            },
745            0x00C8 => match value {
746                PlcValue::Udint(u) => Ok(u.to_le_bytes().to_vec()),
747                _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
748                    expected: "UDINT".to_string(),
749                    actual: format!("{:?}", value),
750                }),
751            },
752            0x00C9 => match value {
753                PlcValue::Ulint(u) => Ok(u.to_le_bytes().to_vec()),
754                _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
755                    expected: "ULINT".to_string(),
756                    actual: format!("{:?}", value),
757                }),
758            },
759            0x00CA => match value {
760                PlcValue::Real(r) => Ok(r.to_le_bytes().to_vec()),
761                _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
762                    expected: "REAL".to_string(),
763                    actual: format!("{:?}", value),
764                }),
765            },
766            0x00CB => match value {
767                PlcValue::Lreal(l) => Ok(l.to_le_bytes().to_vec()),
768                _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
769                    expected: "LREAL".to_string(),
770                    actual: format!("{:?}", value),
771                }),
772            },
773            0x00CE => {
774                match value {
775                    PlcValue::String(s) => {
776                        let mut result = Vec::new();
777                        let max_data_len = member.size.saturating_sub(4); // Subtract 4 for DINT length field
778                        let max_chars = (max_data_len as usize).min(82); // Max STRING length is 82
779                        let length = (s.len() as u32).min(max_chars as u32);
780                        // Length field is 4 bytes (DINT)
781                        result.extend_from_slice(&length.to_le_bytes());
782                        result.extend_from_slice(&s.as_bytes()[..length as usize]);
783                        // Pad to even byte boundary, but don't exceed member size
784                        while result.len() < member.size as usize && result.len() % 2 != 0 {
785                            result.push(0);
786                        }
787                        // Ensure we don't exceed member size
788                        if result.len() > member.size as usize {
789                            result.truncate(member.size as usize);
790                        }
791                        Ok(result)
792                    }
793                    _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
794                        expected: "STRING".to_string(),
795                        actual: format!("{:?}", value),
796                    }),
797                }
798            }
799            _ => Err(crate::error::EtherNetIpError::Protocol(format!(
800                "Unsupported UDT data type for serialization: 0x{:04X}",
801                member.data_type
802            ))),
803        }
804    }
805}
806
807#[cfg(test)]
808mod tests {
809    use super::*;
810
811    #[test]
812    fn test_udt_member_offsets() {
813        let mut udt = UserDefinedType::new("TestUDT".to_string());
814
815        udt.add_member(UdtMember {
816            name: "Bool1".to_string(),
817            data_type: 0x00C1,
818            offset: 0,
819            size: 1,
820        });
821
822        udt.add_member(UdtMember {
823            name: "Dint1".to_string(),
824            data_type: 0x00C4,
825            offset: 4,
826            size: 4,
827        });
828
829        assert_eq!(udt.get_member_offset("Bool1"), Some(0));
830        assert_eq!(udt.get_member_offset("Dint1"), Some(4));
831        assert_eq!(udt.size, 8);
832    }
833
834    #[test]
835    fn test_udt_parsing() {
836        let mut udt = UserDefinedType::new("TestUDT".to_string());
837
838        udt.add_member(UdtMember {
839            name: "Bool1".to_string(),
840            data_type: 0x00C1,
841            offset: 0,
842            size: 1,
843        });
844
845        udt.add_member(UdtMember {
846            name: "Dint1".to_string(),
847            data_type: 0x00C4,
848            offset: 4,
849            size: 4,
850        });
851
852        let data = vec![0xFF, 0x00, 0x00, 0x00, 0x2A, 0x00, 0x00, 0x00];
853        let result = udt.to_hash_map(&data).unwrap();
854
855        assert_eq!(result.get("Bool1"), Some(&PlcValue::Bool(true)));
856        assert_eq!(result.get("Dint1"), Some(&PlcValue::Dint(42)));
857    }
858
859    #[test]
860    fn test_from_cip_data_returns_explicit_error_until_implemented() {
861        let result = UserDefinedType::from_cip_data(&[0x01, 0x02, 0x03]);
862        assert!(result.is_err());
863        let error_text = result.err().unwrap().to_string();
864        assert!(error_text.contains("not implemented"));
865    }
866
867    #[test]
868    fn test_serialize_udt_instance_returns_explicit_error_until_implemented() {
869        let manager = UdtManager::new();
870        let values = HashMap::new();
871        let result = manager.serialize_udt_instance(&values);
872        assert!(result.is_err());
873        let error_text = result.err().unwrap().to_string();
874        assert!(error_text.contains("not implemented"));
875    }
876}