1use crate::error::{EtherNetIpError, Result};
2use crate::udt::{UdtDefinition, UdtMember};
3use crate::EipClient;
4use std::collections::HashMap;
5use std::sync::RwLock;
6use std::time::{Duration, Instant};
7
8#[derive(Debug, Clone, PartialEq)]
10pub enum TagScope {
11 Controller,
13 Program(String),
15 Global,
16 Local,
17}
18
19#[derive(Debug, Clone)]
21pub struct ArrayInfo {
22 pub dimensions: Vec<u32>,
23 pub element_count: u32,
24}
25
26#[derive(Debug, Clone)]
28pub struct TagMetadata {
29 pub data_type: u16,
31 pub size: u32,
33 pub is_array: bool,
35 pub dimensions: Vec<u32>,
37 pub permissions: TagPermissions,
39 pub scope: TagScope,
41 pub last_access: Instant,
43 pub array_info: Option<ArrayInfo>,
44 pub last_updated: Instant,
45}
46
47#[derive(Debug, Clone, PartialEq)]
49pub struct TagPermissions {
50 pub readable: bool,
52 pub writable: bool,
54}
55
56impl TagMetadata {
57 pub fn is_structure(&self) -> bool {
59 (0x00A0..=0x00AF).contains(&self.data_type)
62 }
63}
64
65#[derive(Debug)]
67#[allow(dead_code)]
68pub struct TagCache {
69 tags: HashMap<String, (TagMetadata, Instant)>,
71 expiration: Duration,
73}
74
75impl TagCache {
76 #[allow(dead_code)]
78 pub fn new(expiration: Duration) -> Self {
79 Self {
80 tags: HashMap::new(),
81 expiration,
82 }
83 }
84
85 #[allow(dead_code)]
87 pub fn update_tag(&mut self, name: String, metadata: TagMetadata) {
88 self.tags.insert(name, (metadata, Instant::now()));
89 }
90
91 #[allow(dead_code)]
93 pub fn get_tag(&self, name: &str) -> Option<&TagMetadata> {
94 if let Some((metadata, timestamp)) = self.tags.get(name) {
95 if timestamp.elapsed() < self.expiration {
96 return Some(metadata);
97 }
98 }
99 None
100 }
101
102 #[allow(dead_code)]
104 pub fn cleanup(&mut self) {
105 self.tags
106 .retain(|_, (_, timestamp)| timestamp.elapsed() < self.expiration);
107 }
108}
109
110#[derive(Debug)]
112pub struct TagManager {
113 pub cache: RwLock<HashMap<String, TagMetadata>>,
114 cache_duration: Duration,
115 pub udt_definitions: RwLock<HashMap<String, UdtDefinition>>,
116}
117
118impl TagManager {
119 pub fn new() -> Self {
120 Self {
121 cache: RwLock::new(HashMap::new()),
122 cache_duration: Duration::from_secs(300), udt_definitions: RwLock::new(HashMap::new()),
124 }
125 }
126
127 pub async fn get_metadata(&self, tag_name: &str) -> Option<TagMetadata> {
128 let cache = self.cache.read().unwrap();
129 cache.get(tag_name).and_then(|metadata| {
130 if metadata.last_updated.elapsed() < self.cache_duration {
131 Some(metadata.clone())
132 } else {
133 None
134 }
135 })
136 }
137
138 pub async fn update_metadata(&self, tag_name: String, metadata: TagMetadata) {
139 self.cache.write().unwrap().insert(tag_name, metadata);
140 }
141
142 pub async fn validate_tag(
143 &self,
144 tag_name: &str,
145 required_permissions: &TagPermissions,
146 ) -> Result<()> {
147 if let Some(metadata) = self.get_metadata(tag_name).await {
148 if !metadata.permissions.readable && required_permissions.readable {
149 return Err(EtherNetIpError::Permission(format!(
150 "Tag '{tag_name}' is not readable"
151 )));
152 }
153 if !metadata.permissions.writable && required_permissions.writable {
154 return Err(EtherNetIpError::Permission(format!(
155 "Tag '{tag_name}' is not writable"
156 )));
157 }
158 Ok(())
159 } else {
160 Err(EtherNetIpError::Tag(format!("Tag '{tag_name}' not found")))
161 }
162 }
163
164 pub async fn clear_cache(&self) {
165 self.cache.write().unwrap().clear();
166 }
167
168 pub async fn remove_stale_entries(&self) {
169 self.cache
170 .write()
171 .unwrap()
172 .retain(|_, metadata| metadata.last_updated.elapsed() < self.cache_duration);
173 }
174
175 pub async fn discover_tags(&self, client: &mut EipClient) -> Result<()> {
176 let response = client
177 .send_cip_request(&client.build_list_tags_request())
178 .await?;
179 let tags = self.parse_tag_list(&response)?;
180
181 let mut all_tags = Vec::new();
183 for (name, metadata) in tags {
184 all_tags.push((name, metadata));
185 }
186
187 let hierarchical_tags = self.discover_hierarchical_tags(client, &all_tags).await?;
189
190 let mut cache = self.cache.write().unwrap();
191 for (name, metadata) in hierarchical_tags {
192 cache.insert(name, metadata);
193 }
194 Ok(())
195 }
196
197 async fn discover_hierarchical_tags(
199 &self,
200 client: &mut EipClient,
201 base_tags: &[(String, TagMetadata)],
202 ) -> Result<Vec<(String, TagMetadata)>> {
203 let mut all_tags = Vec::new();
204 let mut tag_names = std::collections::HashSet::new();
205
206 for (name, metadata) in base_tags {
208 if self.validate_tag_name(name) {
209 all_tags.push((name.clone(), metadata.clone()));
210 tag_names.insert(name.clone());
211 }
212 }
213
214 for (name, metadata) in base_tags {
216 if metadata.is_structure() && !metadata.is_array {
217 if let Ok(members) = self.discover_udt_members(client, name).await {
219 for (member_name, member_metadata) in members {
220 let full_name = format!("{}.{}", name, member_name);
221 if self.validate_tag_name(&full_name) && !tag_names.contains(&full_name) {
222 all_tags.push((full_name.clone(), member_metadata.clone()));
223 tag_names.insert(full_name.clone());
224
225 if member_metadata.is_structure() && !member_metadata.is_array {
227 if let Ok(nested_members) =
228 self.discover_udt_members(client, &full_name).await
229 {
230 for (nested_name, nested_metadata) in nested_members {
231 let nested_full_name =
232 format!("{}.{}", full_name, nested_name);
233 if self.validate_tag_name(&nested_full_name)
234 && !tag_names.contains(&nested_full_name)
235 {
236 all_tags
237 .push((nested_full_name.clone(), nested_metadata));
238 tag_names.insert(nested_full_name);
239 }
240 }
241 }
242 }
243 }
244 }
245 }
246 }
247 }
248
249 println!(
250 "[DEBUG] Discovered {} total tags (including hierarchical)",
251 all_tags.len()
252 );
253 Ok(all_tags)
254 }
255
256 pub async fn discover_udt_members(
258 &self,
259 client: &mut EipClient,
260 udt_name: &str,
261 ) -> Result<Vec<(String, TagMetadata)>> {
262 println!("[DEBUG] Discovering UDT members for: {}", udt_name);
263
264 if let Ok(udt_definition) = self.get_udt_definition(client, udt_name).await {
266 let mut members = Vec::new();
267
268 for member in &udt_definition.members {
269 let member_name = member.name.clone();
270 let full_name = format!("{}.{}", udt_name, member_name);
271
272 let metadata = TagMetadata {
274 data_type: member.data_type,
275 scope: TagScope::Controller,
276 permissions: TagPermissions {
277 readable: true,
278 writable: true,
279 },
280 is_array: false, dimensions: Vec::new(),
282 last_access: Instant::now(),
283 size: member.size,
284 array_info: None,
285 last_updated: Instant::now(),
286 };
287
288 if self.validate_tag_name(&full_name) {
289 members.push((full_name.clone(), metadata));
290 println!(
291 "[DEBUG] Found UDT member: {} (Type: 0x{:04X})",
292 full_name, member.data_type
293 );
294 }
295 }
296
297 Ok(members)
298 } else {
299 println!("[WARN] Could not get UDT definition for: {}", udt_name);
300 Ok(Vec::new())
301 }
302 }
303
304 async fn get_udt_definition(
306 &self,
307 client: &mut EipClient,
308 udt_name: &str,
309 ) -> Result<UdtDefinition> {
310 {
312 let definitions = self.udt_definitions.read().unwrap();
313 if let Some(definition) = definitions.get(udt_name) {
314 println!("[DEBUG] Using cached UDT definition for: {}", udt_name);
315 return Ok(definition.clone());
316 }
317 }
318
319 let cip_request = self.build_udt_definition_request(udt_name)?;
321
322 let response = client.send_cip_request(&cip_request).await?;
324
325 let definition = self.parse_udt_definition_response(&response, udt_name)?;
327
328 {
330 let mut definitions = self.udt_definitions.write().unwrap();
331 definitions.insert(udt_name.to_string(), definition.clone());
332 }
333
334 Ok(definition)
335 }
336
337 pub fn build_udt_definition_request(&self, udt_name: &str) -> Result<Vec<u8>> {
339 let mut request = Vec::new();
344
345 request.push(0x4C);
347
348 let path_size = 2 + (udt_name.len() + 1) / 2; request.push(path_size as u8);
351
352 request.push(0x91); request.push(udt_name.len() as u8);
355 request.extend_from_slice(udt_name.as_bytes());
356
357 if udt_name.len() % 2 != 0 {
359 request.push(0x00);
360 }
361
362 Ok(request)
363 }
364
365 pub fn parse_udt_definition_response(
367 &self,
368 response: &[u8],
369 udt_name: &str,
370 ) -> Result<UdtDefinition> {
371 println!(
372 "[DEBUG] Parsing UDT definition response for {} ({} bytes): {:02X?}",
373 udt_name,
374 response.len(),
375 response
376 );
377
378 let mut definition = UdtDefinition {
382 name: udt_name.to_string(),
383 members: Vec::new(),
384 };
385
386 if response.len() > 10 {
389 let mut offset = 0;
391 let mut member_offset = 0u32;
392
393 while offset < response.len().saturating_sub(4) {
394 if let Some((data_type, size)) =
396 self.extract_data_type_from_response(&response[offset..])
397 {
398 let member_name = format!("Member_{}", definition.members.len() + 1);
399
400 definition.members.push(UdtMember {
401 name: member_name,
402 data_type,
403 offset: member_offset,
404 size,
405 });
406
407 member_offset += size;
408 offset += 4; } else {
410 offset += 1;
411 }
412
413 if definition.members.len() > 50 {
415 break;
416 }
417 }
418 }
419
420 if definition.members.is_empty() {
422 definition.members.push(UdtMember {
423 name: "Value".to_string(),
424 data_type: 0x00C4, offset: 0,
426 size: 4,
427 });
428 }
429
430 println!(
431 "[DEBUG] Parsed UDT definition with {} members",
432 definition.members.len()
433 );
434 Ok(definition)
435 }
436
437 fn extract_data_type_from_response(&self, data: &[u8]) -> Option<(u16, u32)> {
439 if data.len() < 4 {
440 return None;
441 }
442
443 let data_type = u16::from_le_bytes([data[0], data[1]]);
445
446 match data_type {
447 0x00C1 => Some((0x00C1, 1)), 0x00C2 => Some((0x00C2, 1)), 0x00C3 => Some((0x00C3, 2)), 0x00C4 => Some((0x00C4, 4)), 0x00C5 => Some((0x00C5, 8)), 0x00C6 => Some((0x00C6, 1)), 0x00C7 => Some((0x00C7, 2)), 0x00C8 => Some((0x00C8, 4)), 0x00C9 => Some((0x00C9, 8)), 0x00CA => Some((0x00CA, 4)), 0x00CB => Some((0x00CB, 8)), 0x00CE => Some((0x00CE, 86)), _ => None,
460 }
461 }
462
463 fn validate_tag_name(&self, tag_name: &str) -> bool {
465 if tag_name.is_empty() || tag_name.trim().is_empty() {
466 return false;
467 }
468
469 let valid_tag_name_regex =
471 regex::Regex::new(r"^[a-zA-Z][a-zA-Z0-9]*(?:[._][a-zA-Z0-9]+)*$").unwrap();
472
473 if !valid_tag_name_regex.is_match(tag_name) {
474 return false;
475 }
476
477 if tag_name.starts_with(char::is_numeric) {
479 return false;
480 }
481
482 if tag_name.contains("__") || tag_name.contains("..") {
483 return false;
484 }
485
486 true
487 }
488
489 pub fn get_udt_definition_cached(&self, udt_name: &str) -> Option<UdtDefinition> {
491 let definitions = self.udt_definitions.read().unwrap();
492 definitions.get(udt_name).cloned()
493 }
494
495 pub fn list_udt_definitions(&self) -> Vec<String> {
497 let definitions = self.udt_definitions.read().unwrap();
498 definitions.keys().cloned().collect()
499 }
500
501 pub fn clear_udt_cache(&self) {
503 let mut definitions = self.udt_definitions.write().unwrap();
504 definitions.clear();
505 }
506
507 pub fn parse_tag_list(&self, response: &[u8]) -> Result<Vec<(String, TagMetadata)>> {
508 println!(
509 "[DEBUG] Raw tag list response ({} bytes): {:02X?}",
510 response.len(),
511 response
512 );
513
514 if response.len() >= 3 {
516 let service_reply = response[0];
517 let general_status = response[2];
518
519 if general_status != 0x00 {
521 let error_msg = match general_status {
523 0x01 => "Connection failure - Tag discovery may not be supported on this PLC",
524 0x04 => "Path segment error",
525 0x05 => "Path destination unknown",
526 0x16 => "Object does not exist",
527 _ => "Unknown CIP error",
528 };
529 return Err(crate::error::EtherNetIpError::Protocol(format!(
530 "CIP Error 0x{:02X} during tag discovery: {}. Some PLCs do not support tag discovery. Try reading tags directly by name.",
531 general_status, error_msg
532 )));
533 }
534
535 if service_reply != 0xD5 && service_reply != 0x55 {
537 if general_status == 0x00 {
539 println!("[WARN] Unexpected service reply 0x{:02X}, but status is 0x00, attempting to parse", service_reply);
540 }
541 }
542 }
543
544 let mut tags = Vec::new();
545
546 if response.len() < 8 {
551 return Err(crate::error::EtherNetIpError::Protocol(
552 "Response too short for tag list".to_string(),
553 ));
554 }
555
556 let item_count = u32::from_le_bytes([response[4], response[5], response[6], response[7]]);
559 println!("[DEBUG] Detected item count: {}", item_count);
560
561 let mut offset = 8;
564 if response.len() > 4 {
565 let additional_status_size = response[3] as usize;
566 if additional_status_size > 0 {
567 offset += additional_status_size * 2; }
569 }
570
571 while offset < response.len() {
573 if offset + 4 > response.len() {
575 println!("[WARN] Not enough bytes for instance ID at offset {offset}");
576 break;
577 }
578
579 let instance_id = u32::from_le_bytes([
580 response[offset],
581 response[offset + 1],
582 response[offset + 2],
583 response[offset + 3],
584 ]);
585 offset += 4;
586
587 if offset + 2 > response.len() {
589 println!("[WARN] Not enough bytes for name length at offset {offset}",);
590 break;
591 }
592
593 let name_length = u16::from_le_bytes([response[offset], response[offset + 1]]) as usize;
594 offset += 2;
595
596 if name_length > 1000 || name_length == 0 {
598 println!(
599 "[WARN] Invalid name length {} at offset {}, skipping entry",
600 name_length,
601 offset - 2
602 );
603 let mut found_next = false;
606 let search_start = offset;
607 for i in search_start..response.len().saturating_sub(4) {
608 if response[i] == 0x00
609 && response[i + 1] == 0x00
610 && response[i + 2] == 0x00
611 && response[i + 3] == 0x00
612 {
613 offset = i;
614 found_next = true;
615 break;
616 }
617 }
618 if !found_next {
619 break;
620 }
621 continue;
622 }
623
624 if offset + name_length > response.len() {
626 println!(
627 "[WARN] Not enough bytes for tag name at offset {} (need {}, have {})",
628 offset,
629 name_length,
630 response.len() - offset
631 );
632 break;
633 }
634
635 let name = String::from_utf8_lossy(&response[offset..offset + name_length]).to_string();
636 offset += name_length;
637
638 if offset + 2 > response.len() {
640 println!("[WARN] Not enough bytes for tag type at offset {offset}");
641 break;
642 }
643
644 let tag_type = u16::from_le_bytes([response[offset], response[offset + 1]]);
645 offset += 2;
646
647 let (type_code, is_structure, array_dims, _reserved) = self.parse_tag_type(tag_type);
649
650 let is_array = array_dims > 0;
651 let dimensions = if is_array {
652 vec![0; array_dims as usize] } else {
654 Vec::new()
655 };
656
657 let array_info = if is_array && !dimensions.is_empty() {
658 Some(ArrayInfo {
659 element_count: dimensions.iter().product(),
660 dimensions: dimensions.clone(),
661 })
662 } else {
663 None
664 };
665
666 if !self.is_valid_tag_type(type_code) {
668 println!(
669 "[DEBUG] Skipping tag {} - unsupported type 0x{:04X}",
670 name, type_code
671 );
672 continue;
673 }
674
675 let metadata = TagMetadata {
676 data_type: type_code,
677 scope: TagScope::Controller,
678 permissions: TagPermissions {
679 readable: true,
680 writable: true,
681 },
682 is_array,
683 dimensions,
684 last_access: Instant::now(),
685 size: 0,
686 array_info,
687 last_updated: Instant::now(),
688 };
689
690 println!(
691 "[DEBUG] Parsed tag: {} (ID: {}, Type: 0x{:04X}, Structure: {})",
692 name, instance_id, type_code, is_structure
693 );
694
695 tags.push((name, metadata));
696 }
697
698 println!("[DEBUG] Parsed {} tags from response", tags.len());
699 Ok(tags)
700 }
701
702 fn parse_tag_type(&self, tag_type: u16) -> (u16, bool, u8, bool) {
704 let type_code = if (tag_type & 0x00ff) == 0xc1 {
705 0x00c1
706 } else {
707 tag_type & 0x0fff
708 };
709
710 let is_structure = (tag_type & 0x8000) != 0;
711 let array_dims = ((tag_type & 0x6000) >> 13) as u8;
712 let reserved = (tag_type & 0x1000) != 0;
713
714 (type_code, is_structure, array_dims, reserved)
715 }
716
717 fn is_valid_tag_type(&self, type_code: u16) -> bool {
719 match type_code {
720 0x00C1 => true, 0x00C2 => true, 0x00C3 => true, 0x00C4 => true, 0x00C5 => true, 0x00C6 => true, 0x00C7 => true, 0x00C8 => true, 0x00C9 => true, 0x00CA => true, 0x00CB => true, 0x00CE => true, _ => false, }
734 }
735
736 pub async fn drill_down_tags(
738 &self,
739 base_tags: &[(String, TagMetadata)],
740 ) -> Result<Vec<(String, TagMetadata)>> {
741 let mut all_tags = Vec::new();
742 let mut tag_names = std::collections::HashSet::new();
743
744 for (tag_name, metadata) in base_tags {
746 self.drill_down_recursive(&mut all_tags, &mut tag_names, tag_name, metadata, "")?;
747 }
748
749 println!(
750 "[DEBUG] Drill down completed: {} total tags discovered",
751 all_tags.len()
752 );
753 Ok(all_tags)
754 }
755
756 fn drill_down_recursive(
758 &self,
759 all_tags: &mut Vec<(String, TagMetadata)>,
760 tag_names: &mut std::collections::HashSet<String>,
761 tag_name: &str,
762 metadata: &TagMetadata,
763 previous_name: &str,
764 ) -> Result<()> {
765 if metadata.is_array {
767 return Ok(());
768 }
769
770 let new_name = if previous_name.is_empty() {
771 tag_name.to_string()
772 } else {
773 format!("{}.{}", previous_name, tag_name)
774 };
775
776 if metadata.is_structure() && !metadata.is_array {
778 if self.validate_tag_name(&new_name) && !tag_names.contains(&new_name) {
781 all_tags.push((new_name.clone(), metadata.clone()));
782 tag_names.insert(new_name);
783 }
784 } else {
785 if self.is_valid_tag_type(metadata.data_type)
787 && self.validate_tag_name(&new_name)
788 && !tag_names.contains(&new_name)
789 {
790 all_tags.push((new_name.clone(), metadata.clone()));
791 tag_names.insert(new_name);
792 }
793 }
794
795 Ok(())
796 }
797}
798
799impl Default for TagManager {
800 fn default() -> Self {
801 Self::new()
802 }
803}
804
805#[cfg(test)]
806mod tests {
807 use super::*;
808 use crate::udt::UdtMember;
809
810 #[test]
811 fn test_tag_cache_expiration() {
812 let mut cache = TagCache::new(Duration::from_secs(1));
813 let metadata = TagMetadata {
814 data_type: 0x00C1,
815 size: 1,
816 is_array: false,
817 dimensions: vec![],
818 permissions: TagPermissions {
819 readable: true,
820 writable: true,
821 },
822 scope: TagScope::Controller,
823 last_access: Instant::now(),
824 array_info: None,
825 last_updated: Instant::now(),
826 };
827
828 cache.update_tag("TestTag".to_string(), metadata);
829 assert!(cache.get_tag("TestTag").is_some());
830
831 std::thread::sleep(Duration::from_secs(2));
833 assert!(cache.get_tag("TestTag").is_none());
834 }
835
836 #[test]
837 fn test_tag_metadata_is_structure() {
838 let bool_metadata = TagMetadata {
840 data_type: 0x00C1,
841 size: 1,
842 is_array: false,
843 dimensions: vec![],
844 permissions: TagPermissions {
845 readable: true,
846 writable: true,
847 },
848 scope: TagScope::Controller,
849 last_access: Instant::now(),
850 array_info: None,
851 last_updated: Instant::now(),
852 };
853 assert!(!bool_metadata.is_structure());
854
855 let dint_metadata = TagMetadata {
857 data_type: 0x00C4,
858 size: 4,
859 is_array: false,
860 dimensions: vec![],
861 permissions: TagPermissions {
862 readable: true,
863 writable: true,
864 },
865 scope: TagScope::Controller,
866 last_access: Instant::now(),
867 array_info: None,
868 last_updated: Instant::now(),
869 };
870 assert!(!dint_metadata.is_structure());
871
872 let udt_metadata = TagMetadata {
874 data_type: 0x00A0,
875 size: 20,
876 is_array: false,
877 dimensions: vec![],
878 permissions: TagPermissions {
879 readable: true,
880 writable: true,
881 },
882 scope: TagScope::Controller,
883 last_access: Instant::now(),
884 array_info: None,
885 last_updated: Instant::now(),
886 };
887 assert!(udt_metadata.is_structure());
888 }
889
890 #[test]
891 fn test_validate_tag_name() {
892 let tag_manager = TagManager::new();
893
894 assert!(tag_manager.validate_tag_name("ValidTag"));
896 assert!(tag_manager.validate_tag_name("Valid_Tag"));
897 assert!(tag_manager.validate_tag_name("Valid.Tag"));
898 assert!(tag_manager.validate_tag_name("Valid123"));
899 assert!(tag_manager.validate_tag_name("Valid_Tag123"));
900 assert!(tag_manager.validate_tag_name("Valid.Tag123"));
901
902 assert!(!tag_manager.validate_tag_name("")); assert!(!tag_manager.validate_tag_name(" ")); assert!(!tag_manager.validate_tag_name("123Invalid")); assert!(!tag_manager.validate_tag_name("Invalid__Tag")); assert!(!tag_manager.validate_tag_name("Invalid..Tag")); assert!(!tag_manager.validate_tag_name("Invalid-Tag")); assert!(!tag_manager.validate_tag_name("Invalid Tag")); assert!(!tag_manager.validate_tag_name("Invalid@Tag")); }
912
913 #[test]
914 fn test_parse_tag_type() {
915 let tag_manager = TagManager::new();
916
917 let (type_code, is_structure, array_dims, reserved) = tag_manager.parse_tag_type(0x00C1);
919 assert_eq!(type_code, 0x00C1);
920 assert!(!is_structure);
921 assert_eq!(array_dims, 0);
922 assert!(!reserved);
923
924 let (type_code, is_structure, array_dims, reserved) = tag_manager.parse_tag_type(0x00C4);
926 assert_eq!(type_code, 0x00C4);
927 assert!(!is_structure);
928 assert_eq!(array_dims, 0);
929 assert!(!reserved);
930
931 let (type_code, is_structure, array_dims, reserved) = tag_manager.parse_tag_type(0x80A0);
933 assert_eq!(type_code, 0x00A0);
934 assert!(is_structure);
935 assert_eq!(array_dims, 0);
936 assert!(!reserved);
937
938 let (type_code, is_structure, array_dims, reserved) = tag_manager.parse_tag_type(0x20C4);
940 assert_eq!(type_code, 0x00C4);
941 assert!(!is_structure);
942 assert_eq!(array_dims, 1);
943 assert!(!reserved);
944
945 let (type_code, is_structure, array_dims, reserved) = tag_manager.parse_tag_type(0x40C4);
947 assert_eq!(type_code, 0x00C4);
948 assert!(!is_structure);
949 assert_eq!(array_dims, 2);
950 assert!(!reserved);
951 }
952
953 #[test]
954 fn test_extract_data_type_from_response() {
955 let tag_manager = TagManager::new();
956
957 let data = [0xC1, 0x00, 0x01, 0x00];
959 assert_eq!(
960 tag_manager.extract_data_type_from_response(&data),
961 Some((0x00C1, 1))
962 );
963
964 let data = [0xC4, 0x00, 0x04, 0x00];
966 assert_eq!(
967 tag_manager.extract_data_type_from_response(&data),
968 Some((0x00C4, 4))
969 );
970
971 let data = [0xCA, 0x00, 0x04, 0x00];
973 assert_eq!(
974 tag_manager.extract_data_type_from_response(&data),
975 Some((0x00CA, 4))
976 );
977
978 let data = [0xCE, 0x00, 0x56, 0x00];
980 assert_eq!(
981 tag_manager.extract_data_type_from_response(&data),
982 Some((0x00CE, 86))
983 );
984
985 let data = [0xFF, 0xFF, 0x00, 0x00];
987 assert_eq!(tag_manager.extract_data_type_from_response(&data), None);
988
989 let data = [0xC1, 0x00];
991 assert_eq!(tag_manager.extract_data_type_from_response(&data), None);
992 }
993
994 #[test]
995 fn test_parse_udt_definition_response() {
996 let tag_manager = TagManager::new();
997
998 let empty_response = [];
1000 let definition = tag_manager
1001 .parse_udt_definition_response(&empty_response, "TestUDT")
1002 .unwrap();
1003 assert_eq!(definition.name, "TestUDT");
1004 assert_eq!(definition.members.len(), 1);
1005 assert_eq!(definition.members[0].name, "Value");
1006 assert_eq!(definition.members[0].data_type, 0x00C4);
1007
1008 let response_data = [
1010 0xC1, 0x00, 0x01, 0x00, 0xC4, 0x00, 0x04, 0x00, 0xCA, 0x00, 0x04, 0x00, ];
1014 let definition = tag_manager
1015 .parse_udt_definition_response(&response_data, "MotorData")
1016 .unwrap();
1017 assert_eq!(definition.name, "MotorData");
1018 assert_eq!(definition.members.len(), 2); assert_eq!(definition.members[0].name, "Member_1");
1020 assert_eq!(definition.members[0].data_type, 0x00C1);
1021 assert_eq!(definition.members[1].name, "Member_2");
1022 assert_eq!(definition.members[1].data_type, 0x00C4);
1023 }
1024
1025 #[test]
1026 fn test_build_udt_definition_request() {
1027 let tag_manager = TagManager::new();
1028
1029 let request = tag_manager
1031 .build_udt_definition_request("MotorData")
1032 .unwrap();
1033 assert_eq!(request[0], 0x4C); assert_eq!(request[1], 0x07); assert_eq!(request[2], 0x91); assert_eq!(request[3], 9); assert_eq!(&request[4..13], b"MotorData");
1038
1039 let request = tag_manager.build_udt_definition_request("Motor").unwrap();
1041 assert_eq!(request[0], 0x4C); assert_eq!(request[1], 0x05); assert_eq!(request[2], 0x91); assert_eq!(request[3], 5); assert_eq!(&request[4..9], b"Motor");
1046 assert_eq!(request[9], 0x00); }
1048
1049 #[test]
1050 fn test_udt_definition_caching() {
1051 let tag_manager = TagManager::new();
1052
1053 assert!(tag_manager.list_udt_definitions().is_empty());
1055
1056 let udt_def = UdtDefinition {
1058 name: "TestUDT".to_string(),
1059 members: vec![
1060 UdtMember {
1061 name: "Value1".to_string(),
1062 data_type: 0x00C1,
1063 offset: 0,
1064 size: 1,
1065 },
1066 UdtMember {
1067 name: "Value2".to_string(),
1068 data_type: 0x00C4,
1069 offset: 4,
1070 size: 4,
1071 },
1072 ],
1073 };
1074
1075 {
1077 let mut definitions = tag_manager.udt_definitions.write().unwrap();
1078 definitions.insert("TestUDT".to_string(), udt_def);
1079 }
1080
1081 let retrieved = tag_manager.get_udt_definition_cached("TestUDT");
1083 assert!(retrieved.is_some());
1084 let retrieved = retrieved.unwrap();
1085 assert_eq!(retrieved.name, "TestUDT");
1086 assert_eq!(retrieved.members.len(), 2);
1087
1088 let udt_list = tag_manager.list_udt_definitions();
1090 assert_eq!(udt_list.len(), 1);
1091 assert_eq!(udt_list[0], "TestUDT");
1092
1093 tag_manager.clear_udt_cache();
1095 assert!(tag_manager.list_udt_definitions().is_empty());
1096 assert!(tag_manager.get_udt_definition_cached("TestUDT").is_none());
1097 }
1098
1099 #[test]
1100 fn test_parse_tag_list_with_invalid_data() {
1101 let tag_manager = TagManager::new();
1102
1103 let invalid_response = [
1105 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, ];
1109
1110 let result = tag_manager.parse_tag_list(&invalid_response);
1111 assert!(result.is_ok());
1112 let tags = result.unwrap();
1113 assert_eq!(tags.len(), 0); }
1115
1116 #[test]
1117 fn test_parse_tag_list_with_valid_data() {
1118 let tag_manager = TagManager::new();
1119
1120 let valid_response = [
1122 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, b'M', b'o', b't', b'o', b'r', b'D', b'a', b't', 0xC4, 0x00, ];
1129
1130 let result = tag_manager.parse_tag_list(&valid_response);
1131 assert!(result.is_ok());
1132 let tags = result.unwrap();
1133 assert!(!tags.is_empty() || tags.is_empty()); }
1136
1137 #[test]
1138 fn test_tag_scope_enum() {
1139 let controller_scope = TagScope::Controller;
1141 assert_eq!(controller_scope, TagScope::Controller);
1142
1143 let program_scope = TagScope::Program("MainProgram".to_string());
1145 match program_scope {
1146 TagScope::Program(name) => assert_eq!(name, "MainProgram"),
1147 _ => panic!("Expected Program scope"),
1148 }
1149
1150 let global_scope = TagScope::Global;
1152 assert_eq!(global_scope, TagScope::Global);
1153
1154 let local_scope = TagScope::Local;
1156 assert_eq!(local_scope, TagScope::Local);
1157 }
1158
1159 #[test]
1160 fn test_array_info() {
1161 let array_info = ArrayInfo {
1162 dimensions: vec![10, 20],
1163 element_count: 200,
1164 };
1165
1166 assert_eq!(array_info.dimensions, vec![10, 20]);
1167 assert_eq!(array_info.element_count, 200);
1168 }
1169
1170 #[test]
1171 fn test_tag_permissions() {
1172 let permissions = TagPermissions {
1173 readable: true,
1174 writable: false,
1175 };
1176
1177 assert!(permissions.readable);
1178 assert!(!permissions.writable);
1179 }
1180}