1use crate::error::{EtherNetIpError, Result};
2use crate::EipClient;
3use std::collections::HashMap;
4use std::sync::RwLock;
5use std::time::{Duration, Instant};
6
7#[derive(Debug, Clone, PartialEq)]
9pub enum TagScope {
10 Controller,
12 Program(String),
14 Global,
15 Local,
16}
17
18#[derive(Debug, Clone)]
20pub struct ArrayInfo {
21 pub dimensions: Vec<u32>,
22 pub element_count: u32,
23}
24
25#[derive(Debug, Clone)]
27pub struct TagMetadata {
28 pub data_type: u16,
30 pub size: u32,
32 pub is_array: bool,
34 pub dimensions: Vec<u32>,
36 pub permissions: TagPermissions,
38 pub scope: TagScope,
40 pub last_access: Instant,
42 pub array_info: Option<ArrayInfo>,
43 pub last_updated: Instant,
44}
45
46#[derive(Debug, Clone, PartialEq)]
48pub struct TagPermissions {
49 pub readable: bool,
51 pub writable: bool,
53}
54
55#[derive(Debug)]
57#[allow(dead_code)]
58pub struct TagCache {
59 tags: HashMap<String, (TagMetadata, Instant)>,
61 expiration: Duration,
63}
64
65impl TagCache {
66 #[allow(dead_code)]
68 pub fn new(expiration: Duration) -> Self {
69 Self {
70 tags: HashMap::new(),
71 expiration,
72 }
73 }
74
75 #[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 #[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 #[allow(dead_code)]
94 pub fn cleanup(&mut self) {
95 self.tags
96 .retain(|_, (_, timestamp)| timestamp.elapsed() < self.expiration);
97 }
98}
99
100#[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), }
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 while offset < response.len() {
193 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 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 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 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 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] } 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 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 std::thread::sleep(Duration::from_secs(2));
339 assert!(cache.get_tag("TestTag").is_none());
340 }
341}