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};
7use tracing;
8
9#[derive(Debug, Clone, PartialEq)]
11pub enum TagScope {
12 Controller,
14 Program(String),
16 Global,
17 Local,
18}
19
20#[derive(Debug, Clone)]
22pub struct ArrayInfo {
23 pub dimensions: Vec<u32>,
24 pub element_count: u32,
25}
26
27#[derive(Debug, Clone)]
29pub struct TagMetadata {
30 pub data_type: u16,
32 pub size: u32,
34 pub is_array: bool,
36 pub dimensions: Vec<u32>,
38 pub permissions: TagPermissions,
40 pub scope: TagScope,
42 pub last_access: Instant,
44 pub array_info: Option<ArrayInfo>,
45 pub last_updated: Instant,
46}
47
48#[derive(Debug, Clone, PartialEq)]
50pub struct TagPermissions {
51 pub readable: bool,
53 pub writable: bool,
55}
56
57impl TagMetadata {
58 pub fn is_structure(&self) -> bool {
60 (0x00A0..=0x00AF).contains(&self.data_type)
63 }
64}
65
66#[derive(Debug)]
68#[allow(dead_code)]
69pub struct TagCache {
70 tags: HashMap<String, (TagMetadata, Instant)>,
72 expiration: Duration,
74}
75
76impl TagCache {
77 #[allow(dead_code)]
79 pub fn new(expiration: Duration) -> Self {
80 Self {
81 tags: HashMap::new(),
82 expiration,
83 }
84 }
85
86 #[allow(dead_code)]
88 pub fn update_tag(&mut self, name: String, metadata: TagMetadata) {
89 self.tags.insert(name, (metadata, Instant::now()));
90 }
91
92 #[allow(dead_code)]
94 pub fn get_tag(&self, name: &str) -> Option<&TagMetadata> {
95 if let Some((metadata, timestamp)) = self.tags.get(name) {
96 if timestamp.elapsed() < self.expiration {
97 return Some(metadata);
98 }
99 }
100 None
101 }
102
103 #[allow(dead_code)]
105 pub fn cleanup(&mut self) {
106 self.tags
107 .retain(|_, (_, timestamp)| timestamp.elapsed() < self.expiration);
108 }
109}
110
111#[derive(Debug)]
113pub struct TagManager {
114 pub cache: RwLock<HashMap<String, TagMetadata>>,
115 cache_duration: Duration,
116 pub udt_definitions: RwLock<HashMap<String, UdtDefinition>>,
117}
118
119impl TagManager {
120 pub fn new() -> Self {
121 Self {
122 cache: RwLock::new(HashMap::new()),
123 cache_duration: Duration::from_secs(300), udt_definitions: RwLock::new(HashMap::new()),
125 }
126 }
127
128 pub async fn get_metadata(&self, tag_name: &str) -> Option<TagMetadata> {
129 let cache = self.cache.read().unwrap();
130 cache.get(tag_name).and_then(|metadata| {
131 if metadata.last_updated.elapsed() < self.cache_duration {
132 Some(metadata.clone())
133 } else {
134 None
135 }
136 })
137 }
138
139 pub async fn update_metadata(&self, tag_name: String, metadata: TagMetadata) {
140 self.cache.write().unwrap().insert(tag_name, metadata);
141 }
142
143 pub async fn validate_tag(
144 &self,
145 tag_name: &str,
146 required_permissions: &TagPermissions,
147 ) -> Result<()> {
148 if let Some(metadata) = self.get_metadata(tag_name).await {
149 if !metadata.permissions.readable && required_permissions.readable {
150 return Err(EtherNetIpError::Permission(format!(
151 "Tag '{tag_name}' is not readable"
152 )));
153 }
154 if !metadata.permissions.writable && required_permissions.writable {
155 return Err(EtherNetIpError::Permission(format!(
156 "Tag '{tag_name}' is not writable"
157 )));
158 }
159 Ok(())
160 } else {
161 Err(EtherNetIpError::Tag(format!("Tag '{tag_name}' not found")))
162 }
163 }
164
165 pub async fn clear_cache(&self) {
166 self.cache.write().unwrap().clear();
167 }
168
169 pub async fn remove_stale_entries(&self) {
170 self.cache
171 .write()
172 .unwrap()
173 .retain(|_, metadata| metadata.last_updated.elapsed() < self.cache_duration);
174 }
175
176 pub async fn discover_tags(&self, client: &mut EipClient) -> Result<()> {
177 let response = client
178 .send_cip_request(&client.build_list_tags_request())
179 .await?;
180 let tags = self.parse_tag_list(&response)?;
181
182 let mut all_tags = Vec::new();
184 for (name, metadata) in tags {
185 all_tags.push((name, metadata));
186 }
187
188 let hierarchical_tags = self.discover_hierarchical_tags(client, &all_tags).await?;
190
191 let mut cache = self.cache.write().unwrap();
192 for (name, metadata) in hierarchical_tags {
193 cache.insert(name, metadata);
194 }
195 Ok(())
196 }
197
198 async fn discover_hierarchical_tags(
200 &self,
201 client: &mut EipClient,
202 base_tags: &[(String, TagMetadata)],
203 ) -> Result<Vec<(String, TagMetadata)>> {
204 let mut all_tags = Vec::new();
205 let mut tag_names = std::collections::HashSet::new();
206
207 for (name, metadata) in base_tags {
209 if self.validate_tag_name(name) {
210 all_tags.push((name.clone(), metadata.clone()));
211 tag_names.insert(name.clone());
212 }
213 }
214
215 for (name, metadata) in base_tags {
217 if metadata.is_structure() && !metadata.is_array {
218 if let Ok(members) = self.discover_udt_members(client, name).await {
220 for (member_name, member_metadata) in members {
221 let full_name = format!("{}.{}", name, member_name);
222 if self.validate_tag_name(&full_name) && !tag_names.contains(&full_name) {
223 all_tags.push((full_name.clone(), member_metadata.clone()));
224 tag_names.insert(full_name.clone());
225
226 if member_metadata.is_structure() && !member_metadata.is_array {
228 if let Ok(nested_members) =
229 self.discover_udt_members(client, &full_name).await
230 {
231 for (nested_name, nested_metadata) in nested_members {
232 let nested_full_name =
233 format!("{}.{}", full_name, nested_name);
234 if self.validate_tag_name(&nested_full_name)
235 && !tag_names.contains(&nested_full_name)
236 {
237 all_tags
238 .push((nested_full_name.clone(), nested_metadata));
239 tag_names.insert(nested_full_name);
240 }
241 }
242 }
243 }
244 }
245 }
246 }
247 }
248 }
249
250 tracing::debug!(
251 "Discovered {} total tags (including hierarchical)",
252 all_tags.len()
253 );
254 Ok(all_tags)
255 }
256
257 pub async fn discover_udt_members(
259 &self,
260 client: &mut EipClient,
261 udt_name: &str,
262 ) -> Result<Vec<(String, TagMetadata)>> {
263 tracing::debug!("Discovering UDT members for: {}", udt_name);
264
265 if let Ok(udt_definition) = self.get_udt_definition(client, udt_name).await {
267 let mut members = Vec::new();
268
269 for member in &udt_definition.members {
270 let member_name = member.name.clone();
271 let full_name = format!("{}.{}", udt_name, member_name);
272
273 let metadata = TagMetadata {
275 data_type: member.data_type,
276 scope: TagScope::Controller,
277 permissions: TagPermissions {
278 readable: true,
279 writable: true,
280 },
281 is_array: false, dimensions: Vec::new(),
283 last_access: Instant::now(),
284 size: member.size,
285 array_info: None,
286 last_updated: Instant::now(),
287 };
288
289 if self.validate_tag_name(&full_name) {
290 members.push((full_name.clone(), metadata));
291 tracing::trace!(
292 "Found UDT member: {} (Type: 0x{:04X})",
293 full_name,
294 member.data_type
295 );
296 }
297 }
298
299 Ok(members)
300 } else {
301 tracing::warn!("Could not get UDT definition for: {}", udt_name);
302 Ok(Vec::new())
303 }
304 }
305
306 async fn get_udt_definition(
308 &self,
309 client: &mut EipClient,
310 udt_name: &str,
311 ) -> Result<UdtDefinition> {
312 {
314 let definitions = self.udt_definitions.read().unwrap();
315 if let Some(definition) = definitions.get(udt_name) {
316 tracing::debug!("Using cached UDT definition for: {}", udt_name);
317 return Ok(definition.clone());
318 }
319 }
320
321 let cip_request = self.build_udt_definition_request(udt_name)?;
323
324 let response = client.send_cip_request(&cip_request).await?;
326
327 let definition = self.parse_udt_definition_response(&response, udt_name)?;
329
330 {
332 let mut definitions = self.udt_definitions.write().unwrap();
333 definitions.insert(udt_name.to_string(), definition.clone());
334 }
335
336 Ok(definition)
337 }
338
339 pub fn build_udt_definition_request(&self, udt_name: &str) -> Result<Vec<u8>> {
341 let mut request = Vec::new();
346
347 request.push(0x4C);
349
350 let path_size = 2 + (udt_name.len() + 1) / 2; request.push(path_size as u8);
353
354 request.push(0x91); request.push(udt_name.len() as u8);
357 request.extend_from_slice(udt_name.as_bytes());
358
359 if udt_name.len() % 2 != 0 {
361 request.push(0x00);
362 }
363
364 Ok(request)
365 }
366
367 pub fn parse_udt_definition_response(
369 &self,
370 response: &[u8],
371 udt_name: &str,
372 ) -> Result<UdtDefinition> {
373 tracing::trace!(
374 "Parsing UDT definition response for {} ({} bytes): {:02X?}",
375 udt_name,
376 response.len(),
377 response
378 );
379
380 let mut definition = UdtDefinition {
384 name: udt_name.to_string(),
385 members: Vec::new(),
386 };
387
388 if response.len() > 10 {
391 let mut offset = 0;
393 let mut member_offset = 0u32;
394
395 while offset < response.len().saturating_sub(4) {
396 if let Some((data_type, size)) =
398 self.extract_data_type_from_response(&response[offset..])
399 {
400 let member_name = format!("Member_{}", definition.members.len() + 1);
401
402 definition.members.push(UdtMember {
403 name: member_name,
404 data_type,
405 offset: member_offset,
406 size,
407 });
408
409 member_offset += size;
410 offset += 4; } else {
412 offset += 1;
413 }
414
415 if definition.members.len() > 50 {
417 break;
418 }
419 }
420 }
421
422 if definition.members.is_empty() {
424 definition.members.push(UdtMember {
425 name: "Value".to_string(),
426 data_type: 0x00C4, offset: 0,
428 size: 4,
429 });
430 }
431
432 tracing::debug!(
433 "Parsed UDT definition with {} members",
434 definition.members.len()
435 );
436 Ok(definition)
437 }
438
439 fn extract_data_type_from_response(&self, data: &[u8]) -> Option<(u16, u32)> {
441 if data.len() < 4 {
442 return None;
443 }
444
445 let data_type = u16::from_le_bytes([data[0], data[1]]);
447
448 match data_type {
449 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,
462 }
463 }
464
465 fn validate_tag_name(&self, tag_name: &str) -> bool {
467 if tag_name.is_empty() || tag_name.trim().is_empty() {
468 return false;
469 }
470
471 let valid_tag_name_regex =
473 regex::Regex::new(r"^[a-zA-Z][a-zA-Z0-9]*(?:[._][a-zA-Z0-9]+)*$").unwrap();
474
475 if !valid_tag_name_regex.is_match(tag_name) {
476 return false;
477 }
478
479 if tag_name.starts_with(char::is_numeric) {
481 return false;
482 }
483
484 if tag_name.contains("__") || tag_name.contains("..") {
485 return false;
486 }
487
488 true
489 }
490
491 pub fn get_udt_definition_cached(&self, udt_name: &str) -> Option<UdtDefinition> {
493 let definitions = self.udt_definitions.read().unwrap();
494 definitions.get(udt_name).cloned()
495 }
496
497 pub fn list_udt_definitions(&self) -> Vec<String> {
499 let definitions = self.udt_definitions.read().unwrap();
500 definitions.keys().cloned().collect()
501 }
502
503 pub fn clear_udt_cache(&self) {
505 let mut definitions = self.udt_definitions.write().unwrap();
506 definitions.clear();
507 }
508
509 pub fn parse_tag_list(&self, response: &[u8]) -> Result<Vec<(String, TagMetadata)>> {
510 tracing::trace!(
511 "Raw tag list response ({} bytes): {:02X?}",
512 response.len(),
513 response
514 );
515
516 if response.len() >= 3 {
518 let service_reply = response[0];
519 let general_status = response[2];
520
521 if general_status != 0x00 {
523 let error_msg = match general_status {
525 0x01 => "Connection failure - Tag discovery may not be supported on this PLC",
526 0x04 => "Path segment error",
527 0x05 => "Path destination unknown",
528 0x16 => "Object does not exist",
529 _ => "Unknown CIP error",
530 };
531 return Err(crate::error::EtherNetIpError::Protocol(format!(
532 "CIP Error 0x{:02X} during tag discovery: {}. Some PLCs do not support tag discovery. Try reading tags directly by name.",
533 general_status, error_msg
534 )));
535 }
536
537 if service_reply != 0xD5 && service_reply != 0x55 {
539 if general_status == 0x00 {
541 tracing::warn!("Unexpected service reply 0x{:02X}, but status is 0x00, attempting to parse", service_reply);
542 }
543 }
544 }
545
546 let mut tags = Vec::new();
547
548 if response.len() < 8 {
553 return Err(crate::error::EtherNetIpError::Protocol(
554 "Response too short for tag list".to_string(),
555 ));
556 }
557
558 let item_count = u32::from_le_bytes([response[4], response[5], response[6], response[7]]);
561 tracing::debug!("Detected item count: {}", item_count);
562
563 let mut offset = 8;
566 if response.len() > 4 {
567 let additional_status_size = response[3] as usize;
568 if additional_status_size > 0 {
569 offset += additional_status_size * 2; }
571 }
572
573 while offset < response.len() {
575 if offset + 4 > response.len() {
577 tracing::warn!("Not enough bytes for instance ID at offset {}", offset);
578 break;
579 }
580
581 let instance_id = u32::from_le_bytes([
582 response[offset],
583 response[offset + 1],
584 response[offset + 2],
585 response[offset + 3],
586 ]);
587 offset += 4;
588
589 if offset + 2 > response.len() {
591 tracing::warn!("Not enough bytes for name length at offset {}", offset);
592 break;
593 }
594
595 let name_length = u16::from_le_bytes([response[offset], response[offset + 1]]) as usize;
596 offset += 2;
597
598 if name_length > 1000 || name_length == 0 {
600 tracing::warn!(
601 "Invalid name length {} at offset {}, skipping entry",
602 name_length,
603 offset - 2
604 );
605 let mut found_next = false;
608 let search_start = offset;
609 for i in search_start..response.len().saturating_sub(4) {
610 if response[i] == 0x00
611 && response[i + 1] == 0x00
612 && response[i + 2] == 0x00
613 && response[i + 3] == 0x00
614 {
615 offset = i;
616 found_next = true;
617 break;
618 }
619 }
620 if !found_next {
621 break;
622 }
623 continue;
624 }
625
626 if offset
628 .checked_add(name_length)
629 .map_or(true, |end| end > response.len())
630 {
631 tracing::warn!(
632 "Not enough bytes for tag name at offset {} (need {}, have {})",
633 offset,
634 name_length,
635 response.len() - offset
636 );
637 break;
638 }
639
640 let name = String::from_utf8_lossy(&response[offset..offset + name_length]).to_string();
641 offset += name_length;
642
643 if offset + 2 > response.len() {
645 tracing::warn!("Not enough bytes for tag type at offset {}", offset);
646 break;
647 }
648
649 let tag_type = u16::from_le_bytes([response[offset], response[offset + 1]]);
650 offset += 2;
651
652 let (type_code, is_structure, array_dims, _reserved) = self.parse_tag_type(tag_type);
654
655 let is_array = array_dims > 0;
656 let dimensions = if is_array {
657 vec![0; array_dims as usize] } else {
659 Vec::new()
660 };
661
662 let array_info = if is_array && !dimensions.is_empty() {
663 Some(ArrayInfo {
664 element_count: dimensions.iter().product(),
665 dimensions: dimensions.clone(),
666 })
667 } else {
668 None
669 };
670
671 if !self.is_valid_tag_type(type_code) {
673 tracing::debug!(
674 "Skipping tag {} - unsupported type 0x{:04X}",
675 name,
676 type_code
677 );
678 continue;
679 }
680
681 let metadata = TagMetadata {
682 data_type: type_code,
683 scope: TagScope::Controller,
684 permissions: TagPermissions {
685 readable: true,
686 writable: true,
687 },
688 is_array,
689 dimensions,
690 last_access: Instant::now(),
691 size: 0,
692 array_info,
693 last_updated: Instant::now(),
694 };
695
696 tracing::trace!(
697 "Parsed tag: {} (ID: {}, Type: 0x{:04X}, Structure: {})",
698 name,
699 instance_id,
700 type_code,
701 is_structure
702 );
703
704 tags.push((name, metadata));
705 }
706
707 tracing::debug!("Parsed {} tags from response", tags.len());
708 Ok(tags)
709 }
710
711 fn parse_tag_type(&self, tag_type: u16) -> (u16, bool, u8, bool) {
713 let type_code = if (tag_type & 0x00ff) == 0xc1 {
714 0x00c1
715 } else {
716 tag_type & 0x0fff
717 };
718
719 let is_structure = (tag_type & 0x8000) != 0;
720 let array_dims = ((tag_type & 0x6000) >> 13) as u8;
721 let reserved = (tag_type & 0x1000) != 0;
722
723 (type_code, is_structure, array_dims, reserved)
724 }
725
726 fn is_valid_tag_type(&self, type_code: u16) -> bool {
728 match type_code {
729 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, }
743 }
744
745 pub async fn drill_down_tags(
747 &self,
748 base_tags: &[(String, TagMetadata)],
749 ) -> Result<Vec<(String, TagMetadata)>> {
750 let mut all_tags = Vec::new();
751 let mut tag_names = std::collections::HashSet::new();
752
753 for (tag_name, metadata) in base_tags {
755 self.drill_down_recursive(&mut all_tags, &mut tag_names, tag_name, metadata, "")?;
756 }
757
758 tracing::debug!(
759 "Drill down completed: {} total tags discovered",
760 all_tags.len()
761 );
762 Ok(all_tags)
763 }
764
765 fn drill_down_recursive(
767 &self,
768 all_tags: &mut Vec<(String, TagMetadata)>,
769 tag_names: &mut std::collections::HashSet<String>,
770 tag_name: &str,
771 metadata: &TagMetadata,
772 previous_name: &str,
773 ) -> Result<()> {
774 if metadata.is_array {
776 return Ok(());
777 }
778
779 let new_name = if previous_name.is_empty() {
780 tag_name.to_string()
781 } else {
782 format!("{}.{}", previous_name, tag_name)
783 };
784
785 if metadata.is_structure() && !metadata.is_array {
787 if self.validate_tag_name(&new_name) && !tag_names.contains(&new_name) {
790 all_tags.push((new_name.clone(), metadata.clone()));
791 tag_names.insert(new_name);
792 }
793 } else {
794 if self.is_valid_tag_type(metadata.data_type)
796 && self.validate_tag_name(&new_name)
797 && !tag_names.contains(&new_name)
798 {
799 all_tags.push((new_name.clone(), metadata.clone()));
800 tag_names.insert(new_name);
801 }
802 }
803
804 Ok(())
805 }
806}
807
808impl Default for TagManager {
809 fn default() -> Self {
810 Self::new()
811 }
812}
813
814#[cfg(test)]
815mod tests {
816 use super::*;
817 use crate::udt::UdtMember;
818
819 #[test]
820 fn test_tag_cache_expiration() {
821 let mut cache = TagCache::new(Duration::from_secs(1));
822 let metadata = TagMetadata {
823 data_type: 0x00C1,
824 size: 1,
825 is_array: false,
826 dimensions: vec![],
827 permissions: TagPermissions {
828 readable: true,
829 writable: true,
830 },
831 scope: TagScope::Controller,
832 last_access: Instant::now(),
833 array_info: None,
834 last_updated: Instant::now(),
835 };
836
837 cache.update_tag("TestTag".to_string(), metadata);
838 assert!(cache.get_tag("TestTag").is_some());
839
840 std::thread::sleep(Duration::from_secs(2));
842 assert!(cache.get_tag("TestTag").is_none());
843 }
844
845 #[test]
846 fn test_tag_metadata_is_structure() {
847 let bool_metadata = TagMetadata {
849 data_type: 0x00C1,
850 size: 1,
851 is_array: false,
852 dimensions: vec![],
853 permissions: TagPermissions {
854 readable: true,
855 writable: true,
856 },
857 scope: TagScope::Controller,
858 last_access: Instant::now(),
859 array_info: None,
860 last_updated: Instant::now(),
861 };
862 assert!(!bool_metadata.is_structure());
863
864 let dint_metadata = TagMetadata {
866 data_type: 0x00C4,
867 size: 4,
868 is_array: false,
869 dimensions: vec![],
870 permissions: TagPermissions {
871 readable: true,
872 writable: true,
873 },
874 scope: TagScope::Controller,
875 last_access: Instant::now(),
876 array_info: None,
877 last_updated: Instant::now(),
878 };
879 assert!(!dint_metadata.is_structure());
880
881 let udt_metadata = TagMetadata {
883 data_type: 0x00A0,
884 size: 20,
885 is_array: false,
886 dimensions: vec![],
887 permissions: TagPermissions {
888 readable: true,
889 writable: true,
890 },
891 scope: TagScope::Controller,
892 last_access: Instant::now(),
893 array_info: None,
894 last_updated: Instant::now(),
895 };
896 assert!(udt_metadata.is_structure());
897 }
898
899 #[test]
900 fn test_validate_tag_name() {
901 let tag_manager = TagManager::new();
902
903 assert!(tag_manager.validate_tag_name("ValidTag"));
905 assert!(tag_manager.validate_tag_name("Valid_Tag"));
906 assert!(tag_manager.validate_tag_name("Valid.Tag"));
907 assert!(tag_manager.validate_tag_name("Valid123"));
908 assert!(tag_manager.validate_tag_name("Valid_Tag123"));
909 assert!(tag_manager.validate_tag_name("Valid.Tag123"));
910
911 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")); }
921
922 #[test]
923 fn test_parse_tag_type() {
924 let tag_manager = TagManager::new();
925
926 let (type_code, is_structure, array_dims, reserved) = tag_manager.parse_tag_type(0x00C1);
928 assert_eq!(type_code, 0x00C1);
929 assert!(!is_structure);
930 assert_eq!(array_dims, 0);
931 assert!(!reserved);
932
933 let (type_code, is_structure, array_dims, reserved) = tag_manager.parse_tag_type(0x00C4);
935 assert_eq!(type_code, 0x00C4);
936 assert!(!is_structure);
937 assert_eq!(array_dims, 0);
938 assert!(!reserved);
939
940 let (type_code, is_structure, array_dims, reserved) = tag_manager.parse_tag_type(0x80A0);
942 assert_eq!(type_code, 0x00A0);
943 assert!(is_structure);
944 assert_eq!(array_dims, 0);
945 assert!(!reserved);
946
947 let (type_code, is_structure, array_dims, reserved) = tag_manager.parse_tag_type(0x20C4);
949 assert_eq!(type_code, 0x00C4);
950 assert!(!is_structure);
951 assert_eq!(array_dims, 1);
952 assert!(!reserved);
953
954 let (type_code, is_structure, array_dims, reserved) = tag_manager.parse_tag_type(0x40C4);
956 assert_eq!(type_code, 0x00C4);
957 assert!(!is_structure);
958 assert_eq!(array_dims, 2);
959 assert!(!reserved);
960 }
961
962 #[test]
963 fn test_extract_data_type_from_response() {
964 let tag_manager = TagManager::new();
965
966 let data = [0xC1, 0x00, 0x01, 0x00];
968 assert_eq!(
969 tag_manager.extract_data_type_from_response(&data),
970 Some((0x00C1, 1))
971 );
972
973 let data = [0xC4, 0x00, 0x04, 0x00];
975 assert_eq!(
976 tag_manager.extract_data_type_from_response(&data),
977 Some((0x00C4, 4))
978 );
979
980 let data = [0xCA, 0x00, 0x04, 0x00];
982 assert_eq!(
983 tag_manager.extract_data_type_from_response(&data),
984 Some((0x00CA, 4))
985 );
986
987 let data = [0xCE, 0x00, 0x56, 0x00];
989 assert_eq!(
990 tag_manager.extract_data_type_from_response(&data),
991 Some((0x00CE, 86))
992 );
993
994 let data = [0xFF, 0xFF, 0x00, 0x00];
996 assert_eq!(tag_manager.extract_data_type_from_response(&data), None);
997
998 let data = [0xC1, 0x00];
1000 assert_eq!(tag_manager.extract_data_type_from_response(&data), None);
1001 }
1002
1003 #[test]
1004 fn test_parse_udt_definition_response() {
1005 let tag_manager = TagManager::new();
1006
1007 let empty_response = [];
1009 let definition = tag_manager
1010 .parse_udt_definition_response(&empty_response, "TestUDT")
1011 .unwrap();
1012 assert_eq!(definition.name, "TestUDT");
1013 assert_eq!(definition.members.len(), 1);
1014 assert_eq!(definition.members[0].name, "Value");
1015 assert_eq!(definition.members[0].data_type, 0x00C4);
1016
1017 let response_data = [
1019 0xC1, 0x00, 0x01, 0x00, 0xC4, 0x00, 0x04, 0x00, 0xCA, 0x00, 0x04, 0x00, ];
1023 let definition = tag_manager
1024 .parse_udt_definition_response(&response_data, "MotorData")
1025 .unwrap();
1026 assert_eq!(definition.name, "MotorData");
1027 assert_eq!(definition.members.len(), 2); assert_eq!(definition.members[0].name, "Member_1");
1029 assert_eq!(definition.members[0].data_type, 0x00C1);
1030 assert_eq!(definition.members[1].name, "Member_2");
1031 assert_eq!(definition.members[1].data_type, 0x00C4);
1032 }
1033
1034 #[test]
1035 fn test_build_udt_definition_request() {
1036 let tag_manager = TagManager::new();
1037
1038 let request = tag_manager
1040 .build_udt_definition_request("MotorData")
1041 .unwrap();
1042 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");
1047
1048 let request = tag_manager.build_udt_definition_request("Motor").unwrap();
1050 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");
1055 assert_eq!(request[9], 0x00); }
1057
1058 #[test]
1059 fn test_udt_definition_caching() {
1060 let tag_manager = TagManager::new();
1061
1062 assert!(tag_manager.list_udt_definitions().is_empty());
1064
1065 let udt_def = UdtDefinition {
1067 name: "TestUDT".to_string(),
1068 members: vec![
1069 UdtMember {
1070 name: "Value1".to_string(),
1071 data_type: 0x00C1,
1072 offset: 0,
1073 size: 1,
1074 },
1075 UdtMember {
1076 name: "Value2".to_string(),
1077 data_type: 0x00C4,
1078 offset: 4,
1079 size: 4,
1080 },
1081 ],
1082 };
1083
1084 {
1086 let mut definitions = tag_manager.udt_definitions.write().unwrap();
1087 definitions.insert("TestUDT".to_string(), udt_def);
1088 }
1089
1090 let retrieved = tag_manager.get_udt_definition_cached("TestUDT");
1092 assert!(retrieved.is_some());
1093 let retrieved = retrieved.unwrap();
1094 assert_eq!(retrieved.name, "TestUDT");
1095 assert_eq!(retrieved.members.len(), 2);
1096
1097 let udt_list = tag_manager.list_udt_definitions();
1099 assert_eq!(udt_list.len(), 1);
1100 assert_eq!(udt_list[0], "TestUDT");
1101
1102 tag_manager.clear_udt_cache();
1104 assert!(tag_manager.list_udt_definitions().is_empty());
1105 assert!(tag_manager.get_udt_definition_cached("TestUDT").is_none());
1106 }
1107
1108 #[test]
1109 fn test_parse_tag_list_with_invalid_data() {
1110 let tag_manager = TagManager::new();
1111
1112 let invalid_response = [
1114 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, ];
1118
1119 let result = tag_manager.parse_tag_list(&invalid_response);
1120 assert!(result.is_ok());
1121 let tags = result.unwrap();
1122 assert_eq!(tags.len(), 0); }
1124
1125 #[test]
1126 fn test_parse_tag_list_with_valid_data() {
1127 let tag_manager = TagManager::new();
1128
1129 let valid_response = [
1131 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, ];
1138
1139 let result = tag_manager.parse_tag_list(&valid_response);
1140 assert!(result.is_ok());
1141 let tags = result.unwrap();
1142 assert!(!tags.is_empty() || tags.is_empty()); }
1145
1146 #[test]
1147 fn test_tag_scope_enum() {
1148 let controller_scope = TagScope::Controller;
1150 assert_eq!(controller_scope, TagScope::Controller);
1151
1152 let program_scope = TagScope::Program("MainProgram".to_string());
1154 match program_scope {
1155 TagScope::Program(name) => assert_eq!(name, "MainProgram"),
1156 _ => panic!("Expected Program scope"),
1157 }
1158
1159 let global_scope = TagScope::Global;
1161 assert_eq!(global_scope, TagScope::Global);
1162
1163 let local_scope = TagScope::Local;
1165 assert_eq!(local_scope, TagScope::Local);
1166 }
1167
1168 #[test]
1169 fn test_array_info() {
1170 let array_info = ArrayInfo {
1171 dimensions: vec![10, 20],
1172 element_count: 200,
1173 };
1174
1175 assert_eq!(array_info.dimensions, vec![10, 20]);
1176 assert_eq!(array_info.element_count, 200);
1177 }
1178
1179 #[test]
1180 fn test_tag_permissions() {
1181 let permissions = TagPermissions {
1182 readable: true,
1183 writable: false,
1184 };
1185
1186 assert!(permissions.readable);
1187 assert!(!permissions.writable);
1188 }
1189}