rust_ethernet_ip/
tag_manager.rs

1use std::collections::HashMap;
2use std::sync::RwLock;
3use std::time::{Duration, Instant};
4use crate::error::{EtherNetIpError, Result};
5use crate::EipClient;
6
7/// Represents the scope of a tag in the PLC
8#[derive(Debug, Clone, PartialEq)]
9pub enum TagScope {
10    /// Tag in the controller scope
11    Controller,
12    /// Tag in a program scope
13    Program(String),
14    Global,
15    Local,
16}
17
18/// Array information for tags
19#[derive(Debug, Clone)]
20pub struct ArrayInfo {
21    pub dimensions: Vec<u32>,
22    pub element_count: u32,
23}
24
25/// Metadata for a PLC tag
26#[derive(Debug, Clone)]
27pub struct TagMetadata {
28    /// The data type of the tag
29    pub data_type: u16,
30    /// Size of the tag in bytes
31    pub size: u32,
32    /// Whether the tag is an array
33    pub is_array: bool,
34    /// Array dimensions if applicable
35    pub dimensions: Vec<u32>,
36    /// Access permissions for the tag
37    pub permissions: TagPermissions,
38    /// Scope of the tag
39    pub scope: TagScope,
40    /// Last time this tag was accessed
41    pub last_access: Instant,
42    pub array_info: Option<ArrayInfo>,
43    pub last_updated: Instant,
44}
45
46/// Access permissions for a tag
47#[derive(Debug, Clone, PartialEq)]
48pub struct TagPermissions {
49    /// Whether the tag can be read
50    pub readable: bool,
51    /// Whether the tag can be written
52    pub writable: bool,
53}
54
55/// Cache for PLC tags with automatic expiration
56#[derive(Debug)]
57#[allow(dead_code)]
58pub struct TagCache {
59    /// Map of tag names to their metadata
60    tags: HashMap<String, (TagMetadata, Instant)>,
61    /// Cache expiration time
62    expiration: Duration,
63}
64
65impl TagCache {
66    /// Creates a new tag cache with the specified expiration time
67    #[allow(dead_code)]
68    pub fn new(expiration: Duration) -> Self {
69        Self {
70            tags: HashMap::new(),
71            expiration,
72        }
73    }
74
75    /// Updates or adds a tag to the cache
76    #[allow(dead_code)]
77    pub fn update_tag(&mut self, name: String, metadata: TagMetadata) {
78        self.tags.insert(name, (metadata, Instant::now()));
79    }
80
81    /// Gets a tag from the cache if it exists and hasn't expired
82    #[allow(dead_code)]
83    pub fn get_tag(&self, name: &str) -> Option<&TagMetadata> {
84        if let Some((metadata, timestamp)) = self.tags.get(name) {
85            if timestamp.elapsed() < self.expiration {
86                return Some(metadata);
87            }
88        }
89        None
90    }
91
92    /// Removes expired tags from the cache
93    #[allow(dead_code)]
94    pub fn cleanup(&mut self) {
95        self.tags.retain(|_, (_, timestamp)| timestamp.elapsed() < self.expiration);
96    }
97}
98
99/// Manager for PLC tag discovery and caching
100#[derive(Debug)]
101pub struct TagManager {
102    pub cache: RwLock<HashMap<String, TagMetadata>>,
103    cache_duration: Duration,
104}
105
106impl TagManager {
107    pub fn new() -> Self {
108        Self {
109            cache: RwLock::new(HashMap::new()),
110            cache_duration: Duration::from_secs(300), // 5 minutes
111        }
112    }
113
114    pub async fn get_metadata(&self, tag_name: &str) -> Option<TagMetadata> {
115        let cache = self.cache.read().unwrap();
116        cache.get(tag_name).and_then(|metadata| {
117            if metadata.last_updated.elapsed() < self.cache_duration {
118                Some(metadata.clone())
119            } else {
120                None
121            }
122        })
123    }
124
125    pub async fn update_metadata(&self, tag_name: String, metadata: TagMetadata) {
126        self.cache.write().unwrap().insert(tag_name, metadata);
127    }
128
129    pub async fn validate_tag(&self, tag_name: &str, required_permissions: &TagPermissions) -> Result<()> {
130        if let Some(metadata) = self.get_metadata(tag_name).await {
131            if !metadata.permissions.readable && required_permissions.readable {
132                return Err(EtherNetIpError::Permission(format!(
133                    "Tag '{}' is not readable",
134                    tag_name
135                )));
136            }
137            if !metadata.permissions.writable && required_permissions.writable {
138                return Err(EtherNetIpError::Permission(format!(
139                    "Tag '{}' is not writable",
140                    tag_name
141                )));
142            }
143            Ok(())
144        } else {
145            Err(EtherNetIpError::Tag(format!("Tag '{}' not found", tag_name)))
146        }
147    }
148
149    pub async fn clear_cache(&self) {
150        self.cache.write().unwrap().clear();
151    }
152
153    pub async fn remove_stale_entries(&self) {
154        self.cache.write().unwrap().retain(|_, metadata| {
155            metadata.last_updated.elapsed() < self.cache_duration
156        });
157    }
158
159    pub async fn discover_tags(&self, client: &mut EipClient) -> Result<()> {
160        let response = client.send_cip_request(&client.build_list_tags_request()).await?;
161        let tags = self.parse_tag_list(&response)?;
162        let mut cache = self.cache.write().unwrap();
163        for (name, metadata) in tags {
164            cache.insert(name, metadata);
165        }
166        Ok(())
167    }
168
169    pub fn parse_tag_list(&self, response: &[u8]) -> Result<Vec<(String, TagMetadata)>> {
170        println!("[DEBUG] Raw tag list response ({} bytes): {:02X?}", response.len(), response);
171        let mut tags = Vec::new();
172        let mut offset = 0;
173        while offset < response.len() {
174            if offset + 1 > response.len() {
175                println!("[WARN] Not enough bytes for name_len at offset {}", offset);
176                break;
177            }
178            let name_len = response[offset] as usize;
179            offset += 1;
180            if offset + name_len > response.len() {
181                println!("[WARN] Not enough bytes for tag name at offset {}", offset);
182                break;
183            }
184            let name = String::from_utf8_lossy(&response[offset..offset + name_len]).to_string();
185            offset += name_len;
186            if offset + 2 > response.len() {
187                println!("[WARN] Not enough bytes for data_type at offset {}", offset);
188                break;
189            }
190            let data_type = u16::from_le_bytes([
191                response[offset],
192                response[offset + 1],
193            ]);
194            offset += 2;
195            if offset + 1 > response.len() {
196                println!("[WARN] Not enough bytes for is_array at offset {}", offset);
197                break;
198            }
199            let is_array = response[offset] != 0;
200            offset += 1;
201            let mut dimensions = Vec::new();
202            if is_array {
203                if offset + 1 > response.len() {
204                    println!("[WARN] Not enough bytes for dim_count at offset {}", offset);
205                    break;
206                }
207                let dim_count = response[offset] as usize;
208                offset += 1;
209                for _ in 0..dim_count {
210                    if offset + 4 > response.len() {
211                        println!("[WARN] Not enough bytes for dimension at offset {}", offset);
212                        break;
213                    }
214                    let dim = u32::from_le_bytes([
215                        response[offset],
216                        response[offset + 1],
217                        response[offset + 2],
218                        response[offset + 3],
219                    ]);
220                    dimensions.push(dim);
221                    offset += 4;
222                }
223            }
224            
225            let array_info = if is_array && !dimensions.is_empty() {
226                Some(ArrayInfo {
227                    element_count: dimensions.iter().product(),
228                    dimensions: dimensions.clone(),
229                })
230            } else {
231                None
232            };
233            
234            let metadata = TagMetadata {
235                data_type,
236                scope: TagScope::Controller,
237                permissions: TagPermissions { readable: true, writable: true },
238                is_array,
239                dimensions,
240                last_access: Instant::now(),
241                size: 0,
242                array_info,
243                last_updated: Instant::now(),
244            };
245            tags.push((name, metadata));
246        }
247        Ok(tags)
248    }
249}
250
251impl Default for TagManager {
252    fn default() -> Self {
253        Self::new()
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260
261    #[test]
262    fn test_tag_cache_expiration() {
263        let mut cache = TagCache::new(Duration::from_secs(1));
264        let metadata = TagMetadata {
265            data_type: 0x00C1,
266            size: 1,
267            is_array: false,
268            dimensions: vec![],
269            permissions: TagPermissions {
270                readable: true,
271                writable: true,
272            },
273            scope: TagScope::Controller,
274            last_access: Instant::now(),
275            array_info: None,
276            last_updated: Instant::now(),
277        };
278
279        cache.update_tag("TestTag".to_string(), metadata);
280        assert!(cache.get_tag("TestTag").is_some());
281
282        // Wait for expiration
283        std::thread::sleep(Duration::from_secs(2));
284        assert!(cache.get_tag("TestTag").is_none());
285    }
286}