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 => 2,  // INT
231            0x00C3 => 4,  // DINT
232            0x00C4 => 4,  // DINT
233            0x00C5 => 8,  // LINT
234            0x00C6 => 2,  // UINT
235            0x00C7 => 4,  // UDINT
236            0x00C8 => 8,  // ULINT
237            0x00CA => 4,  // REAL
238            0x00CB => 8,  // LREAL
239            0x00CE => 84, // STRING (max 82 chars + 2 length bytes)
240            0x00CF => 1,  // SINT
241            0x00D0 => 1,  // USINT
242            0x00D1 => 2,  // UINT
243            0x00D2 => 4,  // UDINT
244            0x00D3 => 8,  // ULINT
245            _ => 4,       // Default to 4 bytes for unknown types
246        }
247    }
248
249    /// Parses tag attributes from CIP response
250    pub fn parse_tag_attributes(&self, tag_name: &str, data: &[u8]) -> Result<TagAttributes> {
251        if data.len() < 8 {
252            return Err(EtherNetIpError::Protocol(
253                "Tag attributes data too short".to_string(),
254            ));
255        }
256
257        let mut offset = 0;
258
259        // Parse data type
260        let data_type = u16::from_le_bytes([data[offset], data[offset + 1]]);
261        offset += 2;
262
263        // Parse size
264        let size = u32::from_le_bytes([
265            data[offset],
266            data[offset + 1],
267            data[offset + 2],
268            data[offset + 3],
269        ]);
270        offset += 4;
271
272        // Parse dimensions (if present)
273        let mut dimensions = Vec::new();
274        if data.len() > offset {
275            let dimension_count = data[offset] as usize;
276            offset += 1;
277
278            for _ in 0..dimension_count {
279                if offset + 4 <= data.len() {
280                    let dim = u32::from_le_bytes([
281                        data[offset],
282                        data[offset + 1],
283                        data[offset + 2],
284                        data[offset + 3],
285                    ]);
286                    dimensions.push(dim);
287                    offset += 4;
288                }
289            }
290        }
291
292        // Parse permissions (simplified - would need more CIP data)
293        let permissions = TagPermissions::ReadWrite; // Default assumption
294
295        // Parse scope (simplified - would need more CIP data)
296        let scope = if tag_name.contains(':') {
297            let parts: Vec<&str> = tag_name.split(':').collect();
298            if parts.len() >= 2 {
299                TagScope::Program(parts[0].to_string())
300            } else {
301                TagScope::Controller
302            }
303        } else {
304            TagScope::Controller
305        };
306
307        // Get data type name
308        let data_type_name = self.get_data_type_name(data_type);
309
310        // Check if this is a UDT (has template instance ID)
311        let template_instance_id = if data_type == 0x00A0 {
312            // UDT type
313            Some(0) // Would need to extract from additional CIP data
314        } else {
315            None
316        };
317
318        Ok(TagAttributes {
319            name: tag_name.to_string(),
320            data_type,
321            data_type_name,
322            dimensions,
323            permissions,
324            scope,
325            template_instance_id,
326            size,
327        })
328    }
329
330    /// Gets the human-readable name of a data type
331    fn get_data_type_name(&self, data_type: u16) -> String {
332        match data_type {
333            0x00C1 => "BOOL".to_string(),
334            0x00C2 => "INT".to_string(),
335            0x00C3 => "DINT".to_string(),
336            0x00C4 => "DINT".to_string(),
337            0x00C5 => "LINT".to_string(),
338            0x00C6 => "UINT".to_string(),
339            0x00C7 => "UDINT".to_string(),
340            0x00C8 => "ULINT".to_string(),
341            0x00CA => "REAL".to_string(),
342            0x00CB => "LREAL".to_string(),
343            0x00CE => "STRING".to_string(),
344            0x00CF => "SINT".to_string(),
345            0x00D0 => "USINT".to_string(),
346            0x00D1 => "UINT".to_string(),
347            0x00D2 => "UDINT".to_string(),
348            0x00D3 => "ULINT".to_string(),
349            0x00A0 => "UDT".to_string(),
350            _ => format!("UNKNOWN(0x{:04X})", data_type),
351        }
352    }
353
354    /// Parse a UDT instance from raw bytes
355    ///
356    /// Returns raw UDT data in generic format. Note: symbol_id will be 0
357    /// since it's not available in this context. For proper UDT handling with
358    /// symbol_id, use read_tag() which gets tag attributes first.
359    pub fn parse_udt_instance(&self, _udt_name: &str, data: &[u8]) -> Result<PlcValue> {
360        // Return raw UDT data in generic format
361        // symbol_id is 0 since it's not available in this context
362        Ok(PlcValue::Udt(crate::UdtData {
363            symbol_id: 0, // Not available in this context
364            data: data.to_vec(),
365        }))
366    }
367
368    /// Serialize a UDT instance to bytes
369    pub fn serialize_udt_instance(
370        &self,
371        _udt_value: &HashMap<String, PlcValue>,
372    ) -> Result<Vec<u8>> {
373        // For now, return empty bytes
374        // Full UDT serialization can be implemented later
375        Ok(Vec::new())
376    }
377}
378
379impl Default for UdtManager {
380    fn default() -> Self {
381        Self::new()
382    }
383}
384
385// Note: Types are already defined above, no need to re-export
386
387/// Represents a User Defined Type (UDT)
388#[derive(Debug, Clone)]
389pub struct UserDefinedType {
390    /// Name of the UDT
391    pub name: String,
392    /// Total size of the UDT in bytes
393    pub size: u32,
394    /// Members of the UDT
395    pub members: Vec<UdtMember>,
396    /// Cache of member offsets for quick lookup
397    member_offsets: HashMap<String, u32>,
398}
399
400impl UserDefinedType {
401    /// Creates a new UDT
402    pub fn new(name: String) -> Self {
403        Self {
404            name,
405            size: 0,
406            members: Vec::new(),
407            member_offsets: HashMap::new(),
408        }
409    }
410
411    /// Adds a member to the UDT
412    pub fn add_member(&mut self, member: UdtMember) {
413        self.member_offsets
414            .insert(member.name.clone(), member.offset);
415        self.members.push(member);
416        // Calculate total size including padding
417        self.size = self
418            .members
419            .iter()
420            .map(|m| m.offset + m.size)
421            .max()
422            .unwrap_or(0);
423    }
424
425    /// Gets the offset of a member by name
426    pub fn get_member_offset(&self, name: &str) -> Option<u32> {
427        self.member_offsets.get(name).copied()
428    }
429
430    /// Parses a UDT from CIP data
431    pub fn from_cip_data(_data: &[u8]) -> crate::error::Result<Self> {
432        // TODO: Implement CIP data parsing
433        Ok(Self {
434            name: String::new(),
435            members: Vec::new(),
436            size: 0,
437            member_offsets: HashMap::new(),
438        })
439    }
440
441    /// Converts a UDT instance to a `HashMap` of member values
442    pub fn to_hash_map(&self, data: &[u8]) -> crate::error::Result<HashMap<String, PlcValue>> {
443        if data.is_empty() {
444            return Err(crate::error::EtherNetIpError::Protocol(
445                "UDT data is empty".to_string(),
446            ));
447        }
448
449        let mut result = HashMap::new();
450
451        for member in &self.members {
452            let offset = member.offset as usize;
453            if offset + member.size as usize <= data.len() {
454                let member_data = &data[offset..offset + member.size as usize];
455                let value = self.parse_member_value(member, member_data)?;
456                result.insert(member.name.clone(), value);
457            }
458        }
459
460        Ok(result)
461    }
462
463    /// Converts a `HashMap` of member values to raw UDT bytes
464    pub fn from_hash_map(
465        &self,
466        values: &HashMap<String, PlcValue>,
467    ) -> crate::error::Result<Vec<u8>> {
468        let mut data = vec![0u8; self.size as usize];
469
470        for member in &self.members {
471            if let Some(value) = values.get(&member.name) {
472                let member_data = self.serialize_member_value(member, value)?;
473                let offset = member.offset as usize;
474                let end_offset = offset + member_data.len();
475
476                if end_offset <= data.len() {
477                    data[offset..end_offset].copy_from_slice(&member_data);
478                } else {
479                    return Err(crate::error::EtherNetIpError::Protocol(format!(
480                        "Member {} data exceeds UDT size",
481                        member.name
482                    )));
483                }
484            }
485        }
486
487        Ok(data)
488    }
489
490    /// Reads a specific UDT member by name
491    pub fn read_member(&self, data: &[u8], member_name: &str) -> crate::error::Result<PlcValue> {
492        if let Some(member) = self.members.iter().find(|m| m.name == member_name) {
493            let offset = member.offset as usize;
494            if offset + member.size as usize <= data.len() {
495                let member_data = &data[offset..offset + member.size as usize];
496                self.parse_member_value(member, member_data)
497            } else {
498                Err(crate::error::EtherNetIpError::Protocol(format!(
499                    "Member {} data incomplete",
500                    member_name
501                )))
502            }
503        } else {
504            Err(crate::error::EtherNetIpError::TagNotFound(format!(
505                "UDT member '{}' not found",
506                member_name
507            )))
508        }
509    }
510
511    /// Writes a specific UDT member by name
512    pub fn write_member(
513        &self,
514        data: &mut [u8],
515        member_name: &str,
516        value: &PlcValue,
517    ) -> crate::error::Result<()> {
518        if let Some(member) = self.members.iter().find(|m| m.name == member_name) {
519            let member_data = self.serialize_member_value(member, value)?;
520            let offset = member.offset as usize;
521            let end_offset = offset + member_data.len();
522
523            if end_offset <= data.len() {
524                data[offset..end_offset].copy_from_slice(&member_data);
525                Ok(())
526            } else {
527                Err(crate::error::EtherNetIpError::Protocol(format!(
528                    "Member {} data exceeds UDT size",
529                    member_name
530                )))
531            }
532        } else {
533            Err(crate::error::EtherNetIpError::TagNotFound(format!(
534                "UDT member '{}' not found",
535                member_name
536            )))
537        }
538    }
539
540    /// Gets the size of a specific member
541    pub fn get_member_size(&self, member_name: &str) -> Option<u32> {
542        self.members
543            .iter()
544            .find(|m| m.name == member_name)
545            .map(|m| m.size)
546    }
547
548    /// Gets the data type of a specific member
549    pub fn get_member_data_type(&self, member_name: &str) -> Option<u16> {
550        self.members
551            .iter()
552            .find(|m| m.name == member_name)
553            .map(|m| m.data_type)
554    }
555
556    /// Parses a member value from raw data
557    pub fn parse_member_value(
558        &self,
559        member: &UdtMember,
560        data: &[u8],
561    ) -> crate::error::Result<PlcValue> {
562        match member.data_type {
563            0x00C1 => Ok(PlcValue::Bool(data[0] != 0)),
564            0x00C2 => {
565                if data.len() < 2 {
566                    return Err(crate::error::EtherNetIpError::Protocol(
567                        "INT data too short".to_string(),
568                    ));
569                }
570                let mut bytes = [0u8; 2];
571                bytes.copy_from_slice(&data[..2]);
572                Ok(PlcValue::Int(i16::from_le_bytes(bytes)))
573            }
574            0x00C3 => {
575                if data.len() < 4 {
576                    return Err(crate::error::EtherNetIpError::Protocol(
577                        "DINT data too short".to_string(),
578                    ));
579                }
580                let mut bytes = [0u8; 4];
581                bytes.copy_from_slice(&data[..4]);
582                Ok(PlcValue::Dint(i32::from_le_bytes(bytes)))
583            }
584            0x00C4 => {
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                if data.len() < 8 {
596                    return Err(crate::error::EtherNetIpError::Protocol(
597                        "LINT data too short".to_string(),
598                    ));
599                }
600                let mut bytes = [0u8; 8];
601                bytes.copy_from_slice(&data[..8]);
602                Ok(PlcValue::Lint(i64::from_le_bytes(bytes)))
603            }
604            0x00C6 => {
605                if data.len() < 2 {
606                    return Err(crate::error::EtherNetIpError::Protocol(
607                        "UINT data too short".to_string(),
608                    ));
609                }
610                let mut bytes = [0u8; 2];
611                bytes.copy_from_slice(&data[..2]);
612                Ok(PlcValue::Uint(u16::from_le_bytes(bytes)))
613            }
614            0x00C7 => {
615                if data.len() < 4 {
616                    return Err(crate::error::EtherNetIpError::Protocol(
617                        "UDINT data too short".to_string(),
618                    ));
619                }
620                let mut bytes = [0u8; 4];
621                bytes.copy_from_slice(&data[..4]);
622                Ok(PlcValue::Udint(u32::from_le_bytes(bytes)))
623            }
624            0x00C8 => {
625                if data.len() < 8 {
626                    return Err(crate::error::EtherNetIpError::Protocol(
627                        "ULINT data too short".to_string(),
628                    ));
629                }
630                let mut bytes = [0u8; 8];
631                bytes.copy_from_slice(&data[..8]);
632                Ok(PlcValue::Ulint(u64::from_le_bytes(bytes)))
633            }
634            0x00CA => {
635                if data.len() < 4 {
636                    return Err(crate::error::EtherNetIpError::Protocol(
637                        "REAL data too short".to_string(),
638                    ));
639                }
640                let mut bytes = [0u8; 4];
641                bytes.copy_from_slice(&data[..4]);
642                Ok(PlcValue::Real(f32::from_le_bytes(bytes)))
643            }
644            0x00CB => {
645                let mut bytes = [0u8; 8];
646                bytes.copy_from_slice(&data[..8]);
647                Ok(PlcValue::Lreal(f64::from_le_bytes(bytes)))
648            }
649            0x00CE => {
650                // STRING type - first 2 bytes are length, followed by data
651                if data.len() < 2 {
652                    return Err(crate::error::EtherNetIpError::Protocol(
653                        "STRING data too short".to_string(),
654                    ));
655                }
656                let length = u16::from_le_bytes([data[0], data[1]]) as usize;
657                if data.len() < 2 + length {
658                    return Err(crate::error::EtherNetIpError::Protocol(
659                        "STRING data incomplete".to_string(),
660                    ));
661                }
662                let string_data = &data[2..2 + length];
663                let string_value = String::from_utf8_lossy(string_data).to_string();
664                Ok(PlcValue::String(string_value))
665            }
666            0x00CF => {
667                // SINT (8-bit signed integer)
668                Ok(PlcValue::Sint(data[0] as i8))
669            }
670            0x00D0 => {
671                // USINT (8-bit unsigned integer)
672                Ok(PlcValue::Usint(data[0]))
673            }
674            0x00D1 => {
675                // UINT (16-bit unsigned integer)
676                let mut bytes = [0u8; 2];
677                bytes.copy_from_slice(&data[..2]);
678                Ok(PlcValue::Uint(u16::from_le_bytes(bytes)))
679            }
680            0x00D2 => {
681                // UDINT (32-bit unsigned integer)
682                let mut bytes = [0u8; 4];
683                bytes.copy_from_slice(&data[..4]);
684                Ok(PlcValue::Udint(u32::from_le_bytes(bytes)))
685            }
686            0x00D3 => {
687                // ULINT (64-bit unsigned integer)
688                let mut bytes = [0u8; 8];
689                bytes.copy_from_slice(&data[..8]);
690                Ok(PlcValue::Ulint(u64::from_le_bytes(bytes)))
691            }
692            _ => Err(crate::error::EtherNetIpError::Protocol(format!(
693                "Unsupported UDT data type: 0x{:04X}",
694                member.data_type
695            ))),
696        }
697    }
698
699    /// Serializes a member value to raw data
700    pub fn serialize_member_value(
701        &self,
702        member: &UdtMember,
703        value: &PlcValue,
704    ) -> crate::error::Result<Vec<u8>> {
705        match member.data_type {
706            0x00C1 => match value {
707                PlcValue::Bool(b) => Ok(vec![if *b { 0xFF } else { 0x00 }]),
708                _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
709                    expected: "BOOL".to_string(),
710                    actual: format!("{:?}", value),
711                }),
712            },
713            0x00C2 => match value {
714                PlcValue::Int(i) => Ok(i.to_le_bytes().to_vec()),
715                _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
716                    expected: "INT".to_string(),
717                    actual: format!("{:?}", value),
718                }),
719            },
720            0x00C3 | 0x00C4 => match value {
721                PlcValue::Dint(d) => Ok(d.to_le_bytes().to_vec()),
722                _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
723                    expected: "DINT".to_string(),
724                    actual: format!("{:?}", value),
725                }),
726            },
727            0x00C5 => match value {
728                PlcValue::Lint(l) => Ok(l.to_le_bytes().to_vec()),
729                _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
730                    expected: "LINT".to_string(),
731                    actual: format!("{:?}", value),
732                }),
733            },
734            0x00C6 => match value {
735                PlcValue::Uint(w) => Ok(w.to_le_bytes().to_vec()),
736                _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
737                    expected: "UINT".to_string(),
738                    actual: format!("{:?}", value),
739                }),
740            },
741            0x00C7 => match value {
742                PlcValue::Udint(d) => Ok(d.to_le_bytes().to_vec()),
743                _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
744                    expected: "UDINT".to_string(),
745                    actual: format!("{:?}", value),
746                }),
747            },
748            0x00C8 => match value {
749                PlcValue::Ulint(l) => Ok(l.to_le_bytes().to_vec()),
750                _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
751                    expected: "ULINT".to_string(),
752                    actual: format!("{:?}", value),
753                }),
754            },
755            0x00CA => match value {
756                PlcValue::Real(r) => Ok(r.to_le_bytes().to_vec()),
757                _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
758                    expected: "REAL".to_string(),
759                    actual: format!("{:?}", value),
760                }),
761            },
762            0x00CB => match value {
763                PlcValue::Lreal(l) => Ok(l.to_le_bytes().to_vec()),
764                _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
765                    expected: "LREAL".to_string(),
766                    actual: format!("{:?}", value),
767                }),
768            },
769            0x00CE => {
770                match value {
771                    PlcValue::String(s) => {
772                        let mut result = Vec::new();
773                        let max_data_len = member.size.saturating_sub(2); // Subtract 2 for length bytes
774                        let max_chars = (max_data_len as usize).min(82); // Max STRING length is 82
775                        let length = (s.len() as u16).min(max_chars as u16);
776                        result.extend_from_slice(&length.to_le_bytes());
777                        result.extend_from_slice(&s.as_bytes()[..length as usize]);
778                        // Pad to even byte boundary, but don't exceed member size
779                        while result.len() < member.size as usize && result.len() % 2 != 0 {
780                            result.push(0);
781                        }
782                        // Ensure we don't exceed member size
783                        if result.len() > member.size as usize {
784                            result.truncate(member.size as usize);
785                        }
786                        Ok(result)
787                    }
788                    _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
789                        expected: "STRING".to_string(),
790                        actual: format!("{:?}", value),
791                    }),
792                }
793            }
794            0x00CF => match value {
795                PlcValue::Sint(s) => Ok(vec![*s as u8]),
796                _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
797                    expected: "SINT".to_string(),
798                    actual: format!("{:?}", value),
799                }),
800            },
801            0x00D0 => match value {
802                PlcValue::Usint(u) => Ok(vec![*u]),
803                _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
804                    expected: "USINT".to_string(),
805                    actual: format!("{:?}", value),
806                }),
807            },
808            0x00D1 => match value {
809                PlcValue::Uint(u) => Ok(u.to_le_bytes().to_vec()),
810                _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
811                    expected: "UINT".to_string(),
812                    actual: format!("{:?}", value),
813                }),
814            },
815            0x00D2 => match value {
816                PlcValue::Udint(u) => Ok(u.to_le_bytes().to_vec()),
817                _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
818                    expected: "UDINT".to_string(),
819                    actual: format!("{:?}", value),
820                }),
821            },
822            0x00D3 => match value {
823                PlcValue::Ulint(u) => Ok(u.to_le_bytes().to_vec()),
824                _ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
825                    expected: "ULINT".to_string(),
826                    actual: format!("{:?}", value),
827                }),
828            },
829            _ => Err(crate::error::EtherNetIpError::Protocol(format!(
830                "Unsupported UDT data type for serialization: 0x{:04X}",
831                member.data_type
832            ))),
833        }
834    }
835}
836
837#[cfg(test)]
838mod tests {
839    use super::*;
840
841    #[test]
842    fn test_udt_member_offsets() {
843        let mut udt = UserDefinedType::new("TestUDT".to_string());
844
845        udt.add_member(UdtMember {
846            name: "Bool1".to_string(),
847            data_type: 0x00C1,
848            offset: 0,
849            size: 1,
850        });
851
852        udt.add_member(UdtMember {
853            name: "Dint1".to_string(),
854            data_type: 0x00C4,
855            offset: 4,
856            size: 4,
857        });
858
859        assert_eq!(udt.get_member_offset("Bool1"), Some(0));
860        assert_eq!(udt.get_member_offset("Dint1"), Some(4));
861        assert_eq!(udt.size, 8);
862    }
863
864    #[test]
865    fn test_udt_parsing() {
866        let mut udt = UserDefinedType::new("TestUDT".to_string());
867
868        udt.add_member(UdtMember {
869            name: "Bool1".to_string(),
870            data_type: 0x00C1,
871            offset: 0,
872            size: 1,
873        });
874
875        udt.add_member(UdtMember {
876            name: "Dint1".to_string(),
877            data_type: 0x00C4,
878            offset: 4,
879            size: 4,
880        });
881
882        let data = vec![0xFF, 0x00, 0x00, 0x00, 0x2A, 0x00, 0x00, 0x00];
883        let result = udt.to_hash_map(&data).unwrap();
884
885        assert_eq!(result.get("Bool1"), Some(&PlcValue::Bool(true)));
886        assert_eq!(result.get("Dint1"), Some(&PlcValue::Dint(42)));
887    }
888}