1use std::collections::HashMap;
2use std::sync::RwLock;
3use std::time::{Duration, Instant};
4use crate::error::{EtherNetIpError, Result};
5use crate::EipClient;
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.retain(|_, (_, timestamp)| timestamp.elapsed() < self.expiration);
96 }
97}
98
99#[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), }
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 std::thread::sleep(Duration::from_secs(2));
284 assert!(cache.get_tag("TestTag").is_none());
285 }
286}