rust_ethernet_ip/
tag_manager.rs

1use crate::error::{EtherNetIpError, Result};
2use crate::EipClient;
3use std::collections::HashMap;
4use std::sync::RwLock;
5use std::time::{Duration, Instant};
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
96            .retain(|_, (_, timestamp)| timestamp.elapsed() < self.expiration);
97    }
98}
99
100/// Manager for PLC tag discovery and caching
101#[derive(Debug)]
102pub struct TagManager {
103    pub cache: RwLock<HashMap<String, TagMetadata>>,
104    cache_duration: Duration,
105}
106
107impl TagManager {
108    pub fn new() -> Self {
109        Self {
110            cache: RwLock::new(HashMap::new()),
111            cache_duration: Duration::from_secs(300), // 5 minutes
112        }
113    }
114
115    pub async fn get_metadata(&self, tag_name: &str) -> Option<TagMetadata> {
116        let cache = self.cache.read().unwrap();
117        cache.get(tag_name).and_then(|metadata| {
118            if metadata.last_updated.elapsed() < self.cache_duration {
119                Some(metadata.clone())
120            } else {
121                None
122            }
123        })
124    }
125
126    pub async fn update_metadata(&self, tag_name: String, metadata: TagMetadata) {
127        self.cache.write().unwrap().insert(tag_name, metadata);
128    }
129
130    pub async fn validate_tag(
131        &self,
132        tag_name: &str,
133        required_permissions: &TagPermissions,
134    ) -> Result<()> {
135        if let Some(metadata) = self.get_metadata(tag_name).await {
136            if !metadata.permissions.readable && required_permissions.readable {
137                return Err(EtherNetIpError::Permission(format!(
138                    "Tag '{}' is not readable",
139                    tag_name
140                )));
141            }
142            if !metadata.permissions.writable && required_permissions.writable {
143                return Err(EtherNetIpError::Permission(format!(
144                    "Tag '{}' is not writable",
145                    tag_name
146                )));
147            }
148            Ok(())
149        } else {
150            Err(EtherNetIpError::Tag(format!(
151                "Tag '{}' not found",
152                tag_name
153            )))
154        }
155    }
156
157    pub async fn clear_cache(&self) {
158        self.cache.write().unwrap().clear();
159    }
160
161    pub async fn remove_stale_entries(&self) {
162        self.cache
163            .write()
164            .unwrap()
165            .retain(|_, metadata| metadata.last_updated.elapsed() < self.cache_duration);
166    }
167
168    pub async fn discover_tags(&self, client: &mut EipClient) -> Result<()> {
169        let response = client
170            .send_cip_request(&client.build_list_tags_request())
171            .await?;
172        let tags = self.parse_tag_list(&response)?;
173        let mut cache = self.cache.write().unwrap();
174        for (name, metadata) in tags {
175            cache.insert(name, metadata);
176        }
177        Ok(())
178    }
179
180    pub fn parse_tag_list(&self, response: &[u8]) -> Result<Vec<(String, TagMetadata)>> {
181        println!(
182            "[DEBUG] Raw tag list response ({} bytes): {:02X?}",
183            response.len(),
184            response
185        );
186
187        let mut tags = Vec::new();
188        let mut offset = 0;
189
190        // Parse the attribute list response format
191        // Each entry: [InstanceID(4)][NameLength(2)][Name][Type(2)]
192        while offset < response.len() {
193            // Check if we have enough bytes for instance ID
194            if offset + 4 > response.len() {
195                println!(
196                    "[WARN] Not enough bytes for instance ID at offset {}",
197                    offset
198                );
199                break;
200            }
201
202            let instance_id = u32::from_le_bytes([
203                response[offset],
204                response[offset + 1],
205                response[offset + 2],
206                response[offset + 3],
207            ]);
208            offset += 4;
209
210            // Check if we have enough bytes for name length
211            if offset + 2 > response.len() {
212                println!(
213                    "[WARN] Not enough bytes for name length at offset {}",
214                    offset
215                );
216                break;
217            }
218
219            let name_length = u16::from_le_bytes([response[offset], response[offset + 1]]) as usize;
220            offset += 2;
221
222            // Check if we have enough bytes for the tag name
223            if offset + name_length > response.len() {
224                println!(
225                    "[WARN] Not enough bytes for tag name at offset {} (need {}, have {})",
226                    offset,
227                    name_length,
228                    response.len() - offset
229                );
230                break;
231            }
232
233            let name = String::from_utf8_lossy(&response[offset..offset + name_length]).to_string();
234            offset += name_length;
235
236            // Check if we have enough bytes for tag type
237            if offset + 2 > response.len() {
238                println!("[WARN] Not enough bytes for tag type at offset {}", offset);
239                break;
240            }
241
242            let tag_type = u16::from_le_bytes([response[offset], response[offset + 1]]);
243            offset += 2;
244
245            // Parse tag type information (similar to Node.js implementation)
246            let (type_code, _is_structure, array_dims, _reserved) = self.parse_tag_type(tag_type);
247
248            let is_array = array_dims > 0;
249            let dimensions = if is_array {
250                vec![0; array_dims as usize] // Placeholder - actual dimensions would need more parsing
251            } else {
252                Vec::new()
253            };
254
255            let array_info = if is_array && !dimensions.is_empty() {
256                Some(ArrayInfo {
257                    element_count: dimensions.iter().product(),
258                    dimensions: dimensions.clone(),
259                })
260            } else {
261                None
262            };
263
264            let metadata = TagMetadata {
265                data_type: type_code,
266                scope: TagScope::Controller,
267                permissions: TagPermissions {
268                    readable: true,
269                    writable: true,
270                },
271                is_array,
272                dimensions,
273                last_access: Instant::now(),
274                size: 0,
275                array_info,
276                last_updated: Instant::now(),
277            };
278
279            println!(
280                "[DEBUG] Parsed tag: {} (ID: {}, Type: 0x{:04X})",
281                name, instance_id, type_code
282            );
283            tags.push((name, metadata));
284        }
285
286        println!("[DEBUG] Parsed {} tags from response", tags.len());
287        Ok(tags)
288    }
289
290    /// Parse tag type information from the raw type value
291    fn parse_tag_type(&self, tag_type: u16) -> (u16, bool, u8, bool) {
292        let type_code = if (tag_type & 0x00ff) == 0xc1 {
293            0x00c1
294        } else {
295            tag_type & 0x0fff
296        };
297
298        let is_structure = (tag_type & 0x8000) != 0;
299        let array_dims = ((tag_type & 0x6000) >> 13) as u8;
300        let reserved = (tag_type & 0x1000) != 0;
301
302        (type_code, is_structure, array_dims, reserved)
303    }
304}
305
306impl Default for TagManager {
307    fn default() -> Self {
308        Self::new()
309    }
310}
311
312#[cfg(test)]
313mod tests {
314    use super::*;
315
316    #[test]
317    fn test_tag_cache_expiration() {
318        let mut cache = TagCache::new(Duration::from_secs(1));
319        let metadata = TagMetadata {
320            data_type: 0x00C1,
321            size: 1,
322            is_array: false,
323            dimensions: vec![],
324            permissions: TagPermissions {
325                readable: true,
326                writable: true,
327            },
328            scope: TagScope::Controller,
329            last_access: Instant::now(),
330            array_info: None,
331            last_updated: Instant::now(),
332        };
333
334        cache.update_tag("TestTag".to_string(), metadata);
335        assert!(cache.get_tag("TestTag").is_some());
336
337        // Wait for expiration
338        std::thread::sleep(Duration::from_secs(2));
339        assert!(cache.get_tag("TestTag").is_none());
340    }
341}