1use crate::error::{EtherNetIpError, Result};
2use crate::PlcValue;
3use std::collections::HashMap;
4
5#[derive(Debug, Clone)]
7pub struct UdtDefinition {
8 pub name: String,
9 pub members: Vec<UdtMember>,
10}
11
12#[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#[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#[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#[derive(Debug, Clone, PartialEq)]
46pub enum TagPermissions {
47 ReadOnly,
48 ReadWrite,
49 WriteOnly,
50 Unknown,
51}
52
53#[derive(Debug, Clone, PartialEq)]
55pub enum TagScope {
56 Controller,
57 Program(String),
58 Unknown,
59}
60
61#[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 pub fn add_definition(&mut self, definition: UdtDefinition) {
80 self.definitions.insert(definition.name.clone(), definition);
81 }
82
83 pub fn get_definition(&self, name: &str) -> Option<&UdtDefinition> {
85 self.definitions.get(name)
86 }
87
88 pub fn add_template(&mut self, template: UdtTemplate) {
90 self.templates.insert(template.template_id, template);
91 }
92
93 pub fn get_template(&self, template_id: u32) -> Option<&UdtTemplate> {
95 self.templates.get(&template_id)
96 }
97
98 pub fn add_tag_attributes(&mut self, attributes: TagAttributes) {
100 self.tag_attributes
101 .insert(attributes.name.clone(), attributes);
102 }
103
104 pub fn get_tag_attributes(&self, name: &str) -> Option<&TagAttributes> {
106 self.tag_attributes.get(name)
107 }
108
109 pub fn list_definitions(&self) -> Vec<String> {
111 self.definitions.keys().cloned().collect()
112 }
113
114 pub fn list_templates(&self) -> Vec<u32> {
116 self.templates.keys().cloned().collect()
117 }
118
119 pub fn list_tag_attributes(&self) -> Vec<String> {
121 self.tag_attributes.keys().cloned().collect()
122 }
123
124 pub fn clear_cache(&mut self) {
126 self.definitions.clear();
127 self.templates.clear();
128 self.tag_attributes.clear();
129 }
130
131 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 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 offset += 2;
155
156 let mut members = Vec::new();
157 let mut current_offset = 0u32;
158
159 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 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 offset += 2;
182
183 let data_type = (member_info & 0xFFFF) as u16;
185 let _dimensions = ((member_info >> 16) & 0xFF) as u8;
186
187 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 offset = (offset + 3) & !3;
201
202 let member_size = self.get_data_type_size(data_type);
204
205 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 fn get_data_type_size(&self, data_type: u16) -> u32 {
228 match data_type {
229 0x00C1 => 1, 0x00C2 => 2, 0x00C3 => 4, 0x00C4 => 4, 0x00C5 => 8, 0x00C6 => 2, 0x00C7 => 4, 0x00C8 => 8, 0x00CA => 4, 0x00CB => 8, 0x00CE => 84, 0x00CF => 1, 0x00D0 => 1, 0x00D1 => 2, 0x00D2 => 4, 0x00D3 => 8, _ => 4, }
247 }
248
249 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 let data_type = u16::from_le_bytes([data[offset], data[offset + 1]]);
261 offset += 2;
262
263 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 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 let permissions = TagPermissions::ReadWrite; 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 let data_type_name = self.get_data_type_name(data_type);
309
310 let template_instance_id = if data_type == 0x00A0 {
312 Some(0) } 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 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 pub fn parse_udt_instance(&self, _udt_name: &str, data: &[u8]) -> Result<PlcValue> {
360 Ok(PlcValue::Udt(crate::UdtData {
363 symbol_id: 0, data: data.to_vec(),
365 }))
366 }
367
368 pub fn serialize_udt_instance(
370 &self,
371 _udt_value: &HashMap<String, PlcValue>,
372 ) -> Result<Vec<u8>> {
373 Ok(Vec::new())
376 }
377}
378
379impl Default for UdtManager {
380 fn default() -> Self {
381 Self::new()
382 }
383}
384
385#[derive(Debug, Clone)]
389pub struct UserDefinedType {
390 pub name: String,
392 pub size: u32,
394 pub members: Vec<UdtMember>,
396 member_offsets: HashMap<String, u32>,
398}
399
400impl UserDefinedType {
401 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 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 self.size = self
418 .members
419 .iter()
420 .map(|m| m.offset + m.size)
421 .max()
422 .unwrap_or(0);
423 }
424
425 pub fn get_member_offset(&self, name: &str) -> Option<u32> {
427 self.member_offsets.get(name).copied()
428 }
429
430 pub fn from_cip_data(_data: &[u8]) -> crate::error::Result<Self> {
432 Ok(Self {
434 name: String::new(),
435 members: Vec::new(),
436 size: 0,
437 member_offsets: HashMap::new(),
438 })
439 }
440
441 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 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 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 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 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 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 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 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 Ok(PlcValue::Sint(data[0] as i8))
669 }
670 0x00D0 => {
671 Ok(PlcValue::Usint(data[0]))
673 }
674 0x00D1 => {
675 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 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 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 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); let max_chars = (max_data_len as usize).min(82); 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 while result.len() < member.size as usize && result.len() % 2 != 0 {
780 result.push(0);
781 }
782 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}