Skip to main content

rust_ethernet_ip_udt/
lib.rs

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