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        // For now, return empty bytes
366        // Full UDT serialization can be implemented later
367        Ok(Vec::new())
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        // TODO: Implement CIP data parsing
425        Ok(Self {
426            name: String::new(),
427            members: Vec::new(),
428            size: 0,
429            member_offsets: HashMap::new(),
430        })
431    }
432
433    /// Converts a UDT instance to a `HashMap` of member values
434    pub fn to_hash_map(&self, data: &[u8]) -> crate::error::Result<HashMap<String, PlcValue>> {
435        if data.is_empty() {
436            return Err(crate::error::EtherNetIpError::Protocol(
437                "UDT data is empty".to_string(),
438            ));
439        }
440
441        let mut result = HashMap::new();
442
443        for member in &self.members {
444            let offset = member.offset as usize;
445            if offset + member.size as usize <= data.len() {
446                let member_data = &data[offset..offset + member.size as usize];
447                let value = self.parse_member_value(member, member_data)?;
448                result.insert(member.name.clone(), value);
449            }
450        }
451
452        Ok(result)
453    }
454
455    /// Converts a `HashMap` of member values to raw UDT bytes
456    pub fn from_hash_map(
457        &self,
458        values: &HashMap<String, PlcValue>,
459    ) -> crate::error::Result<Vec<u8>> {
460        let mut data = vec![0u8; self.size as usize];
461
462        for member in &self.members {
463            if let Some(value) = values.get(&member.name) {
464                let member_data = self.serialize_member_value(member, value)?;
465                let offset = member.offset as usize;
466                let end_offset = offset + member_data.len();
467
468                if end_offset <= data.len() {
469                    data[offset..end_offset].copy_from_slice(&member_data);
470                } else {
471                    return Err(crate::error::EtherNetIpError::Protocol(format!(
472                        "Member {} data exceeds UDT size",
473                        member.name
474                    )));
475                }
476            }
477        }
478
479        Ok(data)
480    }
481
482    /// Reads a specific UDT member by name
483    pub fn read_member(&self, data: &[u8], member_name: &str) -> crate::error::Result<PlcValue> {
484        if let Some(member) = self.members.iter().find(|m| m.name == member_name) {
485            let offset = member.offset as usize;
486            if offset + member.size as usize <= data.len() {
487                let member_data = &data[offset..offset + member.size as usize];
488                self.parse_member_value(member, member_data)
489            } else {
490                Err(crate::error::EtherNetIpError::Protocol(format!(
491                    "Member {} data incomplete",
492                    member_name
493                )))
494            }
495        } else {
496            Err(crate::error::EtherNetIpError::TagNotFound(format!(
497                "UDT member '{}' not found",
498                member_name
499            )))
500        }
501    }
502
503    /// Writes a specific UDT member by name
504    pub fn write_member(
505        &self,
506        data: &mut [u8],
507        member_name: &str,
508        value: &PlcValue,
509    ) -> crate::error::Result<()> {
510        if let Some(member) = self.members.iter().find(|m| m.name == member_name) {
511            let member_data = self.serialize_member_value(member, value)?;
512            let offset = member.offset as usize;
513            let end_offset = offset + member_data.len();
514
515            if end_offset <= data.len() {
516                data[offset..end_offset].copy_from_slice(&member_data);
517                Ok(())
518            } else {
519                Err(crate::error::EtherNetIpError::Protocol(format!(
520                    "Member {} data exceeds UDT size",
521                    member_name
522                )))
523            }
524        } else {
525            Err(crate::error::EtherNetIpError::TagNotFound(format!(
526                "UDT member '{}' not found",
527                member_name
528            )))
529        }
530    }
531
532    /// Gets the size of a specific member
533    pub fn get_member_size(&self, member_name: &str) -> Option<u32> {
534        self.members
535            .iter()
536            .find(|m| m.name == member_name)
537            .map(|m| m.size)
538    }
539
540    /// Gets the data type of a specific member
541    pub fn get_member_data_type(&self, member_name: &str) -> Option<u16> {
542        self.members
543            .iter()
544            .find(|m| m.name == member_name)
545            .map(|m| m.data_type)
546    }
547
548    /// Parses a member value from raw data
549    pub fn parse_member_value(
550        &self,
551        member: &UdtMember,
552        data: &[u8],
553    ) -> crate::error::Result<PlcValue> {
554        match member.data_type {
555            0x00C1 => {
556                if data.is_empty() {
557                    return Err(crate::error::EtherNetIpError::Protocol(
558                        "BOOL data too short".to_string(),
559                    ));
560                }
561                Ok(PlcValue::Bool(data[0] != 0))
562            }
563            0x00C2 => {
564                // SINT (8-bit signed integer)
565                if data.is_empty() {
566                    return Err(crate::error::EtherNetIpError::Protocol(
567                        "SINT data too short".to_string(),
568                    ));
569                }
570                Ok(PlcValue::Sint(data[0] as i8))
571            }
572            0x00C3 => {
573                // INT (16-bit signed integer)
574                if data.len() < 2 {
575                    return Err(crate::error::EtherNetIpError::Protocol(
576                        "INT data too short".to_string(),
577                    ));
578                }
579                let mut bytes = [0u8; 2];
580                bytes.copy_from_slice(&data[..2]);
581                Ok(PlcValue::Int(i16::from_le_bytes(bytes)))
582            }
583            0x00C4 => {
584                // DINT (32-bit signed integer)
585                if data.len() < 4 {
586                    return Err(crate::error::EtherNetIpError::Protocol(
587                        "DINT data too short".to_string(),
588                    ));
589                }
590                let mut bytes = [0u8; 4];
591                bytes.copy_from_slice(&data[..4]);
592                Ok(PlcValue::Dint(i32::from_le_bytes(bytes)))
593            }
594            0x00C5 => {
595                // LINT (64-bit signed integer)
596                if data.len() < 8 {
597                    return Err(crate::error::EtherNetIpError::Protocol(
598                        "LINT data too short".to_string(),
599                    ));
600                }
601                let mut bytes = [0u8; 8];
602                bytes.copy_from_slice(&data[..8]);
603                Ok(PlcValue::Lint(i64::from_le_bytes(bytes)))
604            }
605            0x00C6 => {
606                // USINT (8-bit unsigned integer)
607                if data.is_empty() {
608                    return Err(crate::error::EtherNetIpError::Protocol(
609                        "USINT data too short".to_string(),
610                    ));
611                }
612                Ok(PlcValue::Usint(data[0]))
613            }
614            0x00C7 => {
615                // UINT (16-bit unsigned integer)
616                if data.len() < 2 {
617                    return Err(crate::error::EtherNetIpError::Protocol(
618                        "UINT data too short".to_string(),
619                    ));
620                }
621                let mut bytes = [0u8; 2];
622                bytes.copy_from_slice(&data[..2]);
623                Ok(PlcValue::Uint(u16::from_le_bytes(bytes)))
624            }
625            0x00C8 => {
626                // UDINT (32-bit unsigned integer)
627                if data.len() < 4 {
628                    return Err(crate::error::EtherNetIpError::Protocol(
629                        "UDINT data too short".to_string(),
630                    ));
631                }
632                let mut bytes = [0u8; 4];
633                bytes.copy_from_slice(&data[..4]);
634                Ok(PlcValue::Udint(u32::from_le_bytes(bytes)))
635            }
636            0x00C9 => {
637                // ULINT (64-bit unsigned integer)
638                if data.len() < 8 {
639                    return Err(crate::error::EtherNetIpError::Protocol(
640                        "ULINT data too short".to_string(),
641                    ));
642                }
643                let mut bytes = [0u8; 8];
644                bytes.copy_from_slice(&data[..8]);
645                Ok(PlcValue::Ulint(u64::from_le_bytes(bytes)))
646            }
647            0x00CA => {
648                // REAL (32-bit float)
649                if data.len() < 4 {
650                    return Err(crate::error::EtherNetIpError::Protocol(
651                        "REAL data too short".to_string(),
652                    ));
653                }
654                let mut bytes = [0u8; 4];
655                bytes.copy_from_slice(&data[..4]);
656                Ok(PlcValue::Real(f32::from_le_bytes(bytes)))
657            }
658            0x00CB => {
659                // LREAL (64-bit float)
660                if data.len() < 8 {
661                    return Err(crate::error::EtherNetIpError::Protocol(
662                        "LREAL data too short".to_string(),
663                    ));
664                }
665                let mut bytes = [0u8; 8];
666                bytes.copy_from_slice(&data[..8]);
667                Ok(PlcValue::Lreal(f64::from_le_bytes(bytes)))
668            }
669            0x00CE => {
670                // STRING type - first 4 bytes are length (DINT), followed by data (up to 82 bytes)
671                if data.len() < 4 {
672                    return Err(crate::error::EtherNetIpError::Protocol(
673                        "STRING data too short".to_string(),
674                    ));
675                }
676                let length = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
677                if data.len() - 4 < length {
678                    return Err(crate::error::EtherNetIpError::Protocol(
679                        "STRING data incomplete".to_string(),
680                    ));
681                }
682                let string_data = &data[4..4 + length];
683                let string_value = String::from_utf8_lossy(string_data).to_string();
684                Ok(PlcValue::String(string_value))
685            }
686            _ => Err(crate::error::EtherNetIpError::Protocol(format!(
687                "Unsupported UDT data type: 0x{:04X}",
688                member.data_type
689            ))),
690        }
691    }
692
693    /// Serializes a member value to raw data
694    pub fn serialize_member_value(
695        &self,
696        member: &UdtMember,
697        value: &PlcValue,
698    ) -> crate::error::Result<Vec<u8>> {
699        match member.data_type {
700            0x00C1 => match value {
701                PlcValue::Bool(b) => Ok(vec![if *b { 0xFF } else { 0x00 }]),
702                _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
703                    expected: "BOOL".to_string(),
704                    actual: format!("{:?}", value),
705                }),
706            },
707            0x00C2 => match value {
708                PlcValue::Sint(s) => Ok(vec![*s as u8]),
709                _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
710                    expected: "SINT".to_string(),
711                    actual: format!("{:?}", value),
712                }),
713            },
714            0x00C3 => match value {
715                PlcValue::Int(i) => Ok(i.to_le_bytes().to_vec()),
716                _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
717                    expected: "INT".to_string(),
718                    actual: format!("{:?}", value),
719                }),
720            },
721            0x00C4 => match value {
722                PlcValue::Dint(d) => Ok(d.to_le_bytes().to_vec()),
723                _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
724                    expected: "DINT".to_string(),
725                    actual: format!("{:?}", value),
726                }),
727            },
728            0x00C5 => match value {
729                PlcValue::Lint(l) => Ok(l.to_le_bytes().to_vec()),
730                _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
731                    expected: "LINT".to_string(),
732                    actual: format!("{:?}", value),
733                }),
734            },
735            0x00C6 => match value {
736                PlcValue::Usint(u) => Ok(vec![*u]),
737                _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
738                    expected: "USINT".to_string(),
739                    actual: format!("{:?}", value),
740                }),
741            },
742            0x00C7 => match value {
743                PlcValue::Uint(u) => Ok(u.to_le_bytes().to_vec()),
744                _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
745                    expected: "UINT".to_string(),
746                    actual: format!("{:?}", value),
747                }),
748            },
749            0x00C8 => match value {
750                PlcValue::Udint(u) => Ok(u.to_le_bytes().to_vec()),
751                _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
752                    expected: "UDINT".to_string(),
753                    actual: format!("{:?}", value),
754                }),
755            },
756            0x00C9 => match value {
757                PlcValue::Ulint(u) => Ok(u.to_le_bytes().to_vec()),
758                _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
759                    expected: "ULINT".to_string(),
760                    actual: format!("{:?}", value),
761                }),
762            },
763            0x00CA => match value {
764                PlcValue::Real(r) => Ok(r.to_le_bytes().to_vec()),
765                _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
766                    expected: "REAL".to_string(),
767                    actual: format!("{:?}", value),
768                }),
769            },
770            0x00CB => match value {
771                PlcValue::Lreal(l) => Ok(l.to_le_bytes().to_vec()),
772                _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
773                    expected: "LREAL".to_string(),
774                    actual: format!("{:?}", value),
775                }),
776            },
777            0x00CE => {
778                match value {
779                    PlcValue::String(s) => {
780                        let mut result = Vec::new();
781                        let max_data_len = member.size.saturating_sub(4); // Subtract 4 for DINT length field
782                        let max_chars = (max_data_len as usize).min(82); // Max STRING length is 82
783                        let length = (s.len() as u32).min(max_chars as u32);
784                        // Length field is 4 bytes (DINT)
785                        result.extend_from_slice(&length.to_le_bytes());
786                        result.extend_from_slice(&s.as_bytes()[..length as usize]);
787                        // Pad to even byte boundary, but don't exceed member size
788                        while result.len() < member.size as usize && result.len() % 2 != 0 {
789                            result.push(0);
790                        }
791                        // Ensure we don't exceed member size
792                        if result.len() > member.size as usize {
793                            result.truncate(member.size as usize);
794                        }
795                        Ok(result)
796                    }
797                    _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
798                        expected: "STRING".to_string(),
799                        actual: format!("{:?}", value),
800                    }),
801                }
802            }
803            _ => Err(crate::error::EtherNetIpError::Protocol(format!(
804                "Unsupported UDT data type for serialization: 0x{:04X}",
805                member.data_type
806            ))),
807        }
808    }
809}
810
811#[cfg(test)]
812mod tests {
813    use super::*;
814
815    #[test]
816    fn test_udt_member_offsets() {
817        let mut udt = UserDefinedType::new("TestUDT".to_string());
818
819        udt.add_member(UdtMember {
820            name: "Bool1".to_string(),
821            data_type: 0x00C1,
822            offset: 0,
823            size: 1,
824        });
825
826        udt.add_member(UdtMember {
827            name: "Dint1".to_string(),
828            data_type: 0x00C4,
829            offset: 4,
830            size: 4,
831        });
832
833        assert_eq!(udt.get_member_offset("Bool1"), Some(0));
834        assert_eq!(udt.get_member_offset("Dint1"), Some(4));
835        assert_eq!(udt.size, 8);
836    }
837
838    #[test]
839    fn test_udt_parsing() {
840        let mut udt = UserDefinedType::new("TestUDT".to_string());
841
842        udt.add_member(UdtMember {
843            name: "Bool1".to_string(),
844            data_type: 0x00C1,
845            offset: 0,
846            size: 1,
847        });
848
849        udt.add_member(UdtMember {
850            name: "Dint1".to_string(),
851            data_type: 0x00C4,
852            offset: 4,
853            size: 4,
854        });
855
856        let data = vec![0xFF, 0x00, 0x00, 0x00, 0x2A, 0x00, 0x00, 0x00];
857        let result = udt.to_hash_map(&data).unwrap();
858
859        assert_eq!(result.get("Bool1"), Some(&PlcValue::Bool(true)));
860        assert_eq!(result.get("Dint1"), Some(&PlcValue::Dint(42)));
861    }
862}