1use crate::EipClient;
2use crate::error::{EtherNetIpError, Result};
3use crate::udt::{UdtDefinition, UdtMember};
4use std::collections::HashMap;
5use std::sync::{LazyLock, RwLock};
6use std::time::{Duration, Instant};
7use tracing;
8
9static TAG_NAME_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
10 regex::Regex::new(r"^[a-zA-Z][a-zA-Z0-9]*(?:[._][a-zA-Z0-9]+)*$")
11 .expect("tag name regex pattern is a valid literal")
12});
13
14#[derive(Debug, Clone, PartialEq)]
16pub enum TagScope {
17 Controller,
19 Program(String),
21 Global,
22 Local,
23}
24
25#[derive(Debug, Clone)]
27pub struct ArrayInfo {
28 pub dimensions: Vec<u32>,
29 pub element_count: u32,
30}
31
32#[derive(Debug, Clone)]
34pub struct TagMetadata {
35 pub data_type: u16,
37 pub size: u32,
39 pub is_array: bool,
41 pub dimensions: Vec<u32>,
43 pub permissions: TagPermissions,
45 pub scope: TagScope,
47 pub last_access: Instant,
49 pub array_info: Option<ArrayInfo>,
50 pub last_updated: Instant,
51}
52
53#[derive(Debug, Clone, PartialEq)]
55pub struct TagPermissions {
56 pub readable: bool,
58 pub writable: bool,
60}
61
62impl TagMetadata {
63 pub fn is_structure(&self) -> bool {
65 (0x00A0..=0x00AF).contains(&self.data_type)
68 }
69}
70
71#[derive(Debug)]
73#[allow(dead_code)]
74pub struct TagCache {
75 tags: HashMap<String, (TagMetadata, Instant)>,
77 expiration: Duration,
79}
80
81impl TagCache {
82 #[allow(dead_code)]
84 pub fn new(expiration: Duration) -> Self {
85 Self {
86 tags: HashMap::new(),
87 expiration,
88 }
89 }
90
91 #[allow(dead_code)]
93 pub fn update_tag(&mut self, name: String, metadata: TagMetadata) {
94 self.tags.insert(name, (metadata, Instant::now()));
95 }
96
97 #[allow(dead_code)]
99 pub fn get_tag(&self, name: &str) -> Option<&TagMetadata> {
100 if let Some((metadata, timestamp)) = self.tags.get(name)
101 && timestamp.elapsed() < self.expiration
102 {
103 return Some(metadata);
104 }
105 None
106 }
107
108 #[allow(dead_code)]
110 pub fn cleanup(&mut self) {
111 self.tags
112 .retain(|_, (_, timestamp)| timestamp.elapsed() < self.expiration);
113 }
114}
115
116#[derive(Debug)]
118pub struct TagManager {
119 pub cache: RwLock<HashMap<String, TagMetadata>>,
120 cache_duration: Duration,
121 pub udt_definitions: RwLock<HashMap<String, UdtDefinition>>,
122}
123
124impl TagManager {
125 pub fn new() -> Self {
126 Self {
127 cache: RwLock::new(HashMap::new()),
128 cache_duration: Duration::from_secs(300), udt_definitions: RwLock::new(HashMap::new()),
130 }
131 }
132
133 pub async fn get_metadata(&self, tag_name: &str) -> Result<Option<TagMetadata>> {
134 let cache = self.cache.read()?;
135 Ok(cache.get(tag_name).and_then(|metadata| {
136 if metadata.last_updated.elapsed() < self.cache_duration {
137 Some(metadata.clone())
138 } else {
139 None
140 }
141 }))
142 }
143
144 pub async fn update_metadata(&self, tag_name: String, metadata: TagMetadata) -> Result<()> {
145 self.cache.write()?.insert(tag_name, metadata);
146 Ok(())
147 }
148
149 pub async fn validate_tag(
150 &self,
151 tag_name: &str,
152 required_permissions: &TagPermissions,
153 ) -> Result<()> {
154 if let Some(metadata) = self.get_metadata(tag_name).await? {
155 if !metadata.permissions.readable && required_permissions.readable {
156 return Err(EtherNetIpError::Permission(format!(
157 "Tag '{tag_name}' is not readable"
158 )));
159 }
160 if !metadata.permissions.writable && required_permissions.writable {
161 return Err(EtherNetIpError::Permission(format!(
162 "Tag '{tag_name}' is not writable"
163 )));
164 }
165 Ok(())
166 } else {
167 Err(EtherNetIpError::Tag(format!("Tag '{tag_name}' not found")))
168 }
169 }
170
171 pub async fn clear_cache(&self) -> Result<()> {
172 self.cache.write()?.clear();
173 Ok(())
174 }
175
176 pub async fn remove_stale_entries(&self) -> Result<()> {
177 self.cache
178 .write()?
179 .retain(|_, metadata| metadata.last_updated.elapsed() < self.cache_duration);
180 Ok(())
181 }
182
183 pub async fn discover_tags(&self, client: &mut EipClient) -> Result<()> {
184 let response = client
185 .send_cip_request(&client.build_list_tags_request())
186 .await?;
187 let tags = self.parse_tag_list(&response)?;
188
189 let mut all_tags = Vec::new();
191 for (name, metadata) in tags {
192 all_tags.push((name, metadata));
193 }
194
195 let hierarchical_tags = self.discover_hierarchical_tags(client, &all_tags).await?;
197
198 let mut cache = self.cache.write()?;
199 for (name, metadata) in hierarchical_tags {
200 cache.insert(name, metadata);
201 }
202 Ok(())
203 }
204
205 async fn discover_hierarchical_tags(
207 &self,
208 client: &mut EipClient,
209 base_tags: &[(String, TagMetadata)],
210 ) -> Result<Vec<(String, TagMetadata)>> {
211 let mut all_tags = Vec::new();
212 let mut tag_names = std::collections::HashSet::new();
213
214 for (name, metadata) in base_tags {
216 if self.validate_tag_name(name) {
217 all_tags.push((name.clone(), metadata.clone()));
218 tag_names.insert(name.clone());
219 }
220 }
221
222 for (name, metadata) in base_tags {
224 if metadata.is_structure() && !metadata.is_array {
225 if let Ok(members) = self.discover_udt_members(client, name).await {
227 for (member_name, member_metadata) in members {
228 let full_name = format!("{}.{}", name, member_name);
229 if self.validate_tag_name(&full_name) && !tag_names.contains(&full_name) {
230 all_tags.push((full_name.clone(), member_metadata.clone()));
231 tag_names.insert(full_name.clone());
232
233 if member_metadata.is_structure()
235 && !member_metadata.is_array
236 && let Ok(nested_members) =
237 self.discover_udt_members(client, &full_name).await
238 {
239 for (nested_name, nested_metadata) in nested_members {
240 let nested_full_name = format!("{}.{}", full_name, nested_name);
241 if self.validate_tag_name(&nested_full_name)
242 && !tag_names.contains(&nested_full_name)
243 {
244 all_tags.push((nested_full_name.clone(), nested_metadata));
245 tag_names.insert(nested_full_name);
246 }
247 }
248 }
249 }
250 }
251 }
252 }
253 }
254
255 tracing::debug!(
256 "Discovered {} total tags (including hierarchical)",
257 all_tags.len()
258 );
259 Ok(all_tags)
260 }
261
262 pub async fn discover_udt_members(
264 &self,
265 client: &mut EipClient,
266 udt_name: &str,
267 ) -> Result<Vec<(String, TagMetadata)>> {
268 tracing::debug!("Discovering UDT members for: {}", udt_name);
269
270 if let Ok(udt_definition) = self.get_udt_definition(client, udt_name).await {
272 let mut members = Vec::new();
273
274 for member in &udt_definition.members {
275 let member_name = member.name.clone();
276 let full_name = format!("{}.{}", udt_name, member_name);
277
278 let metadata = TagMetadata {
280 data_type: member.data_type,
281 scope: TagScope::Controller,
282 permissions: TagPermissions {
283 readable: true,
284 writable: true,
285 },
286 is_array: false, dimensions: Vec::new(),
288 last_access: Instant::now(),
289 size: member.size,
290 array_info: None,
291 last_updated: Instant::now(),
292 };
293
294 if self.validate_tag_name(&full_name) {
295 members.push((full_name.clone(), metadata));
296 tracing::trace!(
297 "Found UDT member: {} (Type: 0x{:04X})",
298 full_name,
299 member.data_type
300 );
301 }
302 }
303
304 Ok(members)
305 } else {
306 tracing::warn!("Could not get UDT definition for: {}", udt_name);
307 Ok(Vec::new())
308 }
309 }
310
311 async fn get_udt_definition(
313 &self,
314 client: &mut EipClient,
315 udt_name: &str,
316 ) -> Result<UdtDefinition> {
317 {
319 let definitions = self.udt_definitions.read().unwrap();
320 if let Some(definition) = definitions.get(udt_name) {
321 tracing::debug!("Using cached UDT definition for: {}", udt_name);
322 return Ok(definition.clone());
323 }
324 }
325
326 let cip_request = self.build_udt_definition_request(udt_name)?;
328
329 let response = client.send_cip_request(&cip_request).await?;
331
332 let definition = self.parse_udt_definition_response(&response, udt_name)?;
334
335 {
337 let mut definitions = self.udt_definitions.write().unwrap();
338 definitions.insert(udt_name.to_string(), definition.clone());
339 }
340
341 Ok(definition)
342 }
343
344 pub fn build_udt_definition_request(&self, udt_name: &str) -> Result<Vec<u8>> {
346 let mut request = Vec::new();
351
352 request.push(0x4C);
354
355 let path_size = 2 + udt_name.len().div_ceil(2); request.push(path_size as u8);
358
359 request.push(0x91); request.push(udt_name.len() as u8);
362 request.extend_from_slice(udt_name.as_bytes());
363
364 if !udt_name.len().is_multiple_of(2) {
366 request.push(0x00);
367 }
368
369 Ok(request)
370 }
371
372 pub fn parse_udt_definition_response(
374 &self,
375 response: &[u8],
376 udt_name: &str,
377 ) -> Result<UdtDefinition> {
378 tracing::trace!(
379 "Parsing UDT definition response for {} ({} bytes): {:02X?}",
380 udt_name,
381 response.len(),
382 response
383 );
384
385 let mut definition = UdtDefinition {
389 name: udt_name.to_string(),
390 members: Vec::new(),
391 };
392
393 if response.len() > 10 {
396 let mut offset = 0;
398 let mut member_offset = 0u32;
399
400 while offset < response.len().saturating_sub(4) {
401 if let Some((data_type, size)) =
403 self.extract_data_type_from_response(&response[offset..])
404 {
405 let member_name = format!("Member_{}", definition.members.len() + 1);
406
407 definition.members.push(UdtMember {
408 name: member_name,
409 data_type,
410 offset: member_offset,
411 size,
412 });
413
414 member_offset += size;
415 offset += 4; } else {
417 offset += 1;
418 }
419
420 if definition.members.len() > 50 {
422 break;
423 }
424 }
425 }
426
427 if definition.members.is_empty() {
429 definition.members.push(UdtMember {
430 name: "Value".to_string(),
431 data_type: 0x00C4, offset: 0,
433 size: 4,
434 });
435 }
436
437 tracing::debug!(
438 "Parsed UDT definition with {} members",
439 definition.members.len()
440 );
441 Ok(definition)
442 }
443
444 fn extract_data_type_from_response(&self, data: &[u8]) -> Option<(u16, u32)> {
446 if data.len() < 4 {
447 return None;
448 }
449
450 let data_type = u16::from_le_bytes([data[0], data[1]]);
452
453 match data_type {
454 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,
467 }
468 }
469
470 fn validate_tag_name(&self, tag_name: &str) -> bool {
472 if tag_name.is_empty() || tag_name.trim().is_empty() {
473 return false;
474 }
475
476 if !TAG_NAME_RE.is_match(tag_name) {
478 return false;
479 }
480
481 if tag_name.starts_with(char::is_numeric) {
483 return false;
484 }
485
486 if tag_name.contains("__") || tag_name.contains("..") {
487 return false;
488 }
489
490 true
491 }
492
493 pub fn get_udt_definition_cached(&self, udt_name: &str) -> Option<UdtDefinition> {
495 let definitions = self.udt_definitions.read().unwrap();
496 definitions.get(udt_name).cloned()
497 }
498
499 pub fn list_udt_definitions(&self) -> Vec<String> {
501 let definitions = self.udt_definitions.read().unwrap();
502 definitions.keys().cloned().collect()
503 }
504
505 pub fn clear_udt_cache(&self) {
507 let mut definitions = self.udt_definitions.write().unwrap();
508 definitions.clear();
509 }
510
511 pub fn parse_tag_list(&self, response: &[u8]) -> Result<Vec<(String, TagMetadata)>> {
512 tracing::trace!(
513 "Raw tag list response ({} bytes): {:02X?}",
514 response.len(),
515 response
516 );
517
518 if response.len() >= 3 {
520 let service_reply = response[0];
521 let general_status = response[2];
522
523 if general_status != 0x00 {
525 let error_msg = match general_status {
527 0x01 => "Connection failure - Tag discovery may not be supported on this PLC",
528 0x04 => "Path segment error",
529 0x05 => "Path destination unknown",
530 0x16 => "Object does not exist",
531 _ => "Unknown CIP error",
532 };
533 return Err(crate::error::EtherNetIpError::Protocol(format!(
534 "CIP Error 0x{:02X} during tag discovery: {}. Some PLCs do not support tag discovery. Try reading tags directly by name.",
535 general_status, error_msg
536 )));
537 }
538
539 if service_reply != 0xD5 && service_reply != 0x55 {
541 if general_status == 0x00 {
543 tracing::warn!(
544 "Unexpected service reply 0x{:02X}, but status is 0x00, attempting to parse",
545 service_reply
546 );
547 }
548 }
549 }
550
551 let mut tags = Vec::new();
552
553 if response.len() < 8 {
558 return Err(crate::error::EtherNetIpError::Protocol(
559 "Response too short for tag list".to_string(),
560 ));
561 }
562
563 let item_count = u32::from_le_bytes([response[4], response[5], response[6], response[7]]);
566 tracing::debug!("Detected item count: {}", item_count);
567
568 let mut offset = 8;
571 if response.len() > 4 {
572 let additional_status_size = response[3] as usize;
573 if additional_status_size > 0 {
574 offset += additional_status_size * 2; }
576 }
577
578 while offset < response.len() {
580 if offset + 4 > response.len() {
582 tracing::warn!("Not enough bytes for instance ID at offset {}", offset);
583 break;
584 }
585
586 let instance_id = u32::from_le_bytes([
587 response[offset],
588 response[offset + 1],
589 response[offset + 2],
590 response[offset + 3],
591 ]);
592 offset += 4;
593
594 if offset + 2 > response.len() {
596 tracing::warn!("Not enough bytes for name length at offset {}", offset);
597 break;
598 }
599
600 let name_length = u16::from_le_bytes([response[offset], response[offset + 1]]) as usize;
601 offset += 2;
602
603 if name_length > 1000 || name_length == 0 {
605 tracing::warn!(
606 "Invalid name length {} at offset {}, skipping entry",
607 name_length,
608 offset - 2
609 );
610 let mut found_next = false;
613 let search_start = offset;
614 for i in search_start..response.len().saturating_sub(4) {
615 if response[i] == 0x00
616 && response[i + 1] == 0x00
617 && response[i + 2] == 0x00
618 && response[i + 3] == 0x00
619 {
620 offset = i;
621 found_next = true;
622 break;
623 }
624 }
625 if !found_next {
626 break;
627 }
628 continue;
629 }
630
631 if offset
633 .checked_add(name_length)
634 .is_none_or(|end| end > response.len())
635 {
636 tracing::warn!(
637 "Not enough bytes for tag name at offset {} (need {}, have {})",
638 offset,
639 name_length,
640 response.len() - offset
641 );
642 break;
643 }
644
645 let name = String::from_utf8_lossy(&response[offset..offset + name_length]).to_string();
646 offset += name_length;
647
648 if offset + 2 > response.len() {
650 tracing::warn!("Not enough bytes for tag type at offset {}", offset);
651 break;
652 }
653
654 let tag_type = u16::from_le_bytes([response[offset], response[offset + 1]]);
655 offset += 2;
656
657 let (type_code, is_structure, array_dims, _reserved) = self.parse_tag_type(tag_type);
659
660 let is_array = array_dims > 0;
661 let dimensions = if is_array {
662 vec![0; array_dims as usize] } else {
664 Vec::new()
665 };
666
667 let array_info = if is_array && !dimensions.is_empty() {
668 Some(ArrayInfo {
669 element_count: dimensions.iter().product(),
670 dimensions: dimensions.clone(),
671 })
672 } else {
673 None
674 };
675
676 if !self.is_valid_tag_type(type_code) {
678 tracing::debug!(
679 "Skipping tag {} - unsupported type 0x{:04X}",
680 name,
681 type_code
682 );
683 continue;
684 }
685
686 let metadata = TagMetadata {
687 data_type: type_code,
688 scope: TagScope::Controller,
689 permissions: TagPermissions {
690 readable: true,
691 writable: true,
692 },
693 is_array,
694 dimensions,
695 last_access: Instant::now(),
696 size: 0,
697 array_info,
698 last_updated: Instant::now(),
699 };
700
701 tracing::trace!(
702 "Parsed tag: {} (ID: {}, Type: 0x{:04X}, Structure: {})",
703 name,
704 instance_id,
705 type_code,
706 is_structure
707 );
708
709 tags.push((name, metadata));
710 }
711
712 tracing::debug!("Parsed {} tags from response", tags.len());
713 Ok(tags)
714 }
715
716 fn parse_tag_type(&self, tag_type: u16) -> (u16, bool, u8, bool) {
718 let type_code = if (tag_type & 0x00ff) == 0xc1 {
719 0x00c1
720 } else {
721 tag_type & 0x0fff
722 };
723
724 let is_structure = (tag_type & 0x8000) != 0;
725 let array_dims = ((tag_type & 0x6000) >> 13) as u8;
726 let reserved = (tag_type & 0x1000) != 0;
727
728 (type_code, is_structure, array_dims, reserved)
729 }
730
731 fn is_valid_tag_type(&self, type_code: u16) -> bool {
733 match type_code {
734 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, }
748 }
749
750 pub async fn drill_down_tags(
752 &self,
753 base_tags: &[(String, TagMetadata)],
754 ) -> Result<Vec<(String, TagMetadata)>> {
755 let mut all_tags = Vec::new();
756 let mut tag_names = std::collections::HashSet::new();
757
758 for (tag_name, metadata) in base_tags {
760 self.drill_down_recursive(&mut all_tags, &mut tag_names, tag_name, metadata, "")?;
761 }
762
763 tracing::debug!(
764 "Drill down completed: {} total tags discovered",
765 all_tags.len()
766 );
767 Ok(all_tags)
768 }
769
770 fn drill_down_recursive(
772 &self,
773 all_tags: &mut Vec<(String, TagMetadata)>,
774 tag_names: &mut std::collections::HashSet<String>,
775 tag_name: &str,
776 metadata: &TagMetadata,
777 previous_name: &str,
778 ) -> Result<()> {
779 if metadata.is_array {
781 return Ok(());
782 }
783
784 let new_name = if previous_name.is_empty() {
785 tag_name.to_string()
786 } else {
787 format!("{}.{}", previous_name, tag_name)
788 };
789
790 if metadata.is_structure() && !metadata.is_array {
792 if self.validate_tag_name(&new_name) && !tag_names.contains(&new_name) {
795 all_tags.push((new_name.clone(), metadata.clone()));
796 tag_names.insert(new_name);
797 }
798 } else {
799 if self.is_valid_tag_type(metadata.data_type)
801 && self.validate_tag_name(&new_name)
802 && !tag_names.contains(&new_name)
803 {
804 all_tags.push((new_name.clone(), metadata.clone()));
805 tag_names.insert(new_name);
806 }
807 }
808
809 Ok(())
810 }
811}
812
813impl Default for TagManager {
814 fn default() -> Self {
815 Self::new()
816 }
817}
818
819#[cfg(test)]
820mod tests {
821 use super::*;
822 use crate::udt::UdtMember;
823
824 #[test]
825 fn test_tag_cache_expiration() {
826 let mut cache = TagCache::new(Duration::from_secs(1));
827 let metadata = TagMetadata {
828 data_type: 0x00C1,
829 size: 1,
830 is_array: false,
831 dimensions: vec![],
832 permissions: TagPermissions {
833 readable: true,
834 writable: true,
835 },
836 scope: TagScope::Controller,
837 last_access: Instant::now(),
838 array_info: None,
839 last_updated: Instant::now(),
840 };
841
842 cache.update_tag("TestTag".to_string(), metadata);
843 assert!(cache.get_tag("TestTag").is_some());
844
845 std::thread::sleep(Duration::from_secs(2));
847 assert!(cache.get_tag("TestTag").is_none());
848 }
849
850 #[test]
851 fn test_tag_metadata_is_structure() {
852 let bool_metadata = TagMetadata {
854 data_type: 0x00C1,
855 size: 1,
856 is_array: false,
857 dimensions: vec![],
858 permissions: TagPermissions {
859 readable: true,
860 writable: true,
861 },
862 scope: TagScope::Controller,
863 last_access: Instant::now(),
864 array_info: None,
865 last_updated: Instant::now(),
866 };
867 assert!(!bool_metadata.is_structure());
868
869 let dint_metadata = TagMetadata {
871 data_type: 0x00C4,
872 size: 4,
873 is_array: false,
874 dimensions: vec![],
875 permissions: TagPermissions {
876 readable: true,
877 writable: true,
878 },
879 scope: TagScope::Controller,
880 last_access: Instant::now(),
881 array_info: None,
882 last_updated: Instant::now(),
883 };
884 assert!(!dint_metadata.is_structure());
885
886 let udt_metadata = TagMetadata {
888 data_type: 0x00A0,
889 size: 20,
890 is_array: false,
891 dimensions: vec![],
892 permissions: TagPermissions {
893 readable: true,
894 writable: true,
895 },
896 scope: TagScope::Controller,
897 last_access: Instant::now(),
898 array_info: None,
899 last_updated: Instant::now(),
900 };
901 assert!(udt_metadata.is_structure());
902 }
903
904 #[test]
905 fn test_validate_tag_name() {
906 let tag_manager = TagManager::new();
907
908 assert!(tag_manager.validate_tag_name("ValidTag"));
910 assert!(tag_manager.validate_tag_name("Valid_Tag"));
911 assert!(tag_manager.validate_tag_name("Valid.Tag"));
912 assert!(tag_manager.validate_tag_name("Valid123"));
913 assert!(tag_manager.validate_tag_name("Valid_Tag123"));
914 assert!(tag_manager.validate_tag_name("Valid.Tag123"));
915
916 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")); }
926
927 #[test]
928 fn test_parse_tag_type() {
929 let tag_manager = TagManager::new();
930
931 let (type_code, is_structure, array_dims, reserved) = tag_manager.parse_tag_type(0x00C1);
933 assert_eq!(type_code, 0x00C1);
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(0x00C4);
940 assert_eq!(type_code, 0x00C4);
941 assert!(!is_structure);
942 assert_eq!(array_dims, 0);
943 assert!(!reserved);
944
945 let (type_code, is_structure, array_dims, reserved) = tag_manager.parse_tag_type(0x80A0);
947 assert_eq!(type_code, 0x00A0);
948 assert!(is_structure);
949 assert_eq!(array_dims, 0);
950 assert!(!reserved);
951
952 let (type_code, is_structure, array_dims, reserved) = tag_manager.parse_tag_type(0x20C4);
954 assert_eq!(type_code, 0x00C4);
955 assert!(!is_structure);
956 assert_eq!(array_dims, 1);
957 assert!(!reserved);
958
959 let (type_code, is_structure, array_dims, reserved) = tag_manager.parse_tag_type(0x40C4);
961 assert_eq!(type_code, 0x00C4);
962 assert!(!is_structure);
963 assert_eq!(array_dims, 2);
964 assert!(!reserved);
965 }
966
967 #[test]
968 fn test_extract_data_type_from_response() {
969 let tag_manager = TagManager::new();
970
971 let data = [0xC1, 0x00, 0x01, 0x00];
973 assert_eq!(
974 tag_manager.extract_data_type_from_response(&data),
975 Some((0x00C1, 1))
976 );
977
978 let data = [0xC4, 0x00, 0x04, 0x00];
980 assert_eq!(
981 tag_manager.extract_data_type_from_response(&data),
982 Some((0x00C4, 4))
983 );
984
985 let data = [0xCA, 0x00, 0x04, 0x00];
987 assert_eq!(
988 tag_manager.extract_data_type_from_response(&data),
989 Some((0x00CA, 4))
990 );
991
992 let data = [0xCE, 0x00, 0x56, 0x00];
994 assert_eq!(
995 tag_manager.extract_data_type_from_response(&data),
996 Some((0x00CE, 86))
997 );
998
999 let data = [0xFF, 0xFF, 0x00, 0x00];
1001 assert_eq!(tag_manager.extract_data_type_from_response(&data), None);
1002
1003 let data = [0xC1, 0x00];
1005 assert_eq!(tag_manager.extract_data_type_from_response(&data), None);
1006 }
1007
1008 #[test]
1009 fn test_parse_udt_definition_response() {
1010 let tag_manager = TagManager::new();
1011
1012 let empty_response = [];
1014 let definition = tag_manager
1015 .parse_udt_definition_response(&empty_response, "TestUDT")
1016 .unwrap();
1017 assert_eq!(definition.name, "TestUDT");
1018 assert_eq!(definition.members.len(), 1);
1019 assert_eq!(definition.members[0].name, "Value");
1020 assert_eq!(definition.members[0].data_type, 0x00C4);
1021
1022 let response_data = [
1024 0xC1, 0x00, 0x01, 0x00, 0xC4, 0x00, 0x04, 0x00, 0xCA, 0x00, 0x04, 0x00, ];
1028 let definition = tag_manager
1029 .parse_udt_definition_response(&response_data, "MotorData")
1030 .unwrap();
1031 assert_eq!(definition.name, "MotorData");
1032 assert_eq!(definition.members.len(), 2); assert_eq!(definition.members[0].name, "Member_1");
1034 assert_eq!(definition.members[0].data_type, 0x00C1);
1035 assert_eq!(definition.members[1].name, "Member_2");
1036 assert_eq!(definition.members[1].data_type, 0x00C4);
1037 }
1038
1039 #[test]
1040 fn test_build_udt_definition_request() {
1041 let tag_manager = TagManager::new();
1042
1043 let request = tag_manager
1045 .build_udt_definition_request("MotorData")
1046 .unwrap();
1047 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");
1052
1053 let request = tag_manager.build_udt_definition_request("Motor").unwrap();
1055 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");
1060 assert_eq!(request[9], 0x00); }
1062
1063 #[test]
1064 fn test_udt_definition_caching() {
1065 let tag_manager = TagManager::new();
1066
1067 assert!(tag_manager.list_udt_definitions().is_empty());
1069
1070 let udt_def = UdtDefinition {
1072 name: "TestUDT".to_string(),
1073 members: vec![
1074 UdtMember {
1075 name: "Value1".to_string(),
1076 data_type: 0x00C1,
1077 offset: 0,
1078 size: 1,
1079 },
1080 UdtMember {
1081 name: "Value2".to_string(),
1082 data_type: 0x00C4,
1083 offset: 4,
1084 size: 4,
1085 },
1086 ],
1087 };
1088
1089 {
1091 let mut definitions = tag_manager.udt_definitions.write().unwrap();
1092 definitions.insert("TestUDT".to_string(), udt_def);
1093 }
1094
1095 let retrieved = tag_manager.get_udt_definition_cached("TestUDT");
1097 assert!(retrieved.is_some());
1098 let retrieved = retrieved.unwrap();
1099 assert_eq!(retrieved.name, "TestUDT");
1100 assert_eq!(retrieved.members.len(), 2);
1101
1102 let udt_list = tag_manager.list_udt_definitions();
1104 assert_eq!(udt_list.len(), 1);
1105 assert_eq!(udt_list[0], "TestUDT");
1106
1107 tag_manager.clear_udt_cache();
1109 assert!(tag_manager.list_udt_definitions().is_empty());
1110 assert!(tag_manager.get_udt_definition_cached("TestUDT").is_none());
1111 }
1112
1113 #[test]
1114 fn test_parse_tag_list_with_invalid_data() {
1115 let tag_manager = TagManager::new();
1116
1117 let invalid_response = [
1119 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, ];
1123
1124 let result = tag_manager.parse_tag_list(&invalid_response);
1125 assert!(result.is_ok());
1126 let tags = result.unwrap();
1127 assert_eq!(tags.len(), 0); }
1129
1130 #[test]
1131 fn test_parse_tag_list_with_valid_data() {
1132 let tag_manager = TagManager::new();
1133
1134 let valid_response = [
1136 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, ];
1143
1144 let result = tag_manager.parse_tag_list(&valid_response);
1145 assert!(result.is_ok());
1146 let tags = result.unwrap();
1147 assert!(!tags.is_empty() || tags.is_empty()); }
1150
1151 #[test]
1152 fn test_tag_scope_enum() {
1153 let controller_scope = TagScope::Controller;
1155 assert_eq!(controller_scope, TagScope::Controller);
1156
1157 let program_scope = TagScope::Program("MainProgram".to_string());
1159 match program_scope {
1160 TagScope::Program(name) => assert_eq!(name, "MainProgram"),
1161 _ => panic!("Expected Program scope"),
1162 }
1163
1164 let global_scope = TagScope::Global;
1166 assert_eq!(global_scope, TagScope::Global);
1167
1168 let local_scope = TagScope::Local;
1170 assert_eq!(local_scope, TagScope::Local);
1171 }
1172
1173 #[test]
1174 fn test_array_info() {
1175 let array_info = ArrayInfo {
1176 dimensions: vec![10, 20],
1177 element_count: 200,
1178 };
1179
1180 assert_eq!(array_info.dimensions, vec![10, 20]);
1181 assert_eq!(array_info.element_count, 200);
1182 }
1183
1184 #[test]
1185 fn test_tag_permissions() {
1186 let permissions = TagPermissions {
1187 readable: true,
1188 writable: false,
1189 };
1190
1191 assert!(permissions.readable);
1192 assert!(!permissions.writable);
1193 }
1194}