1use super::EipClient;
2use crate::error::{EtherNetIpError, Result};
3use crate::subscription::{SubscriptionOptions, TagSubscription};
4use crate::tag_group::{
5 TagGroupConfig, TagGroupEvent, TagGroupEventKind, TagGroupFailureDiagnostic, TagGroupSnapshot,
6 TagGroupSubscription, TagGroupValueResult,
7};
8use crate::types::PlcValue;
9
10const CONNECTION_LOST_FAILURE_LIMIT: usize = 3;
11const RETRIABLE_ERROR_BACKOFF_MS: u64 = 250;
12
13impl EipClient {
14 pub async fn subscribe_to_tag(
26 &self,
27 tag_path: &str,
28 options: SubscriptionOptions,
29 ) -> Result<TagSubscription> {
30 let subscription = TagSubscription::new(tag_path.to_string(), options.clone());
31 let mut validation_client = self.clone();
32 let initial_value = validation_client.read_tag(tag_path).await?;
33 subscription.update_value(&initial_value).await?;
34
35 let mut subscriptions = self.subscriptions.lock().await;
36 let update_rate_ms = options.update_rate;
37 subscriptions.push(subscription.clone());
38 drop(subscriptions);
39
40 let tag_path = tag_path.to_string();
41 let mut client = self.clone();
42 let subscription_task = subscription.clone();
43 tokio::spawn(async move {
44 let interval = std::time::Duration::from_millis(update_rate_ms as u64);
45 let retry_backoff = std::time::Duration::from_millis(RETRIABLE_ERROR_BACKOFF_MS);
46 let mut consecutive_connection_lost = 0usize;
47 while subscription_task.is_active() {
48 match client.read_tag(&tag_path).await {
49 Ok(value) => {
50 consecutive_connection_lost = 0;
51 if let Err(e) = client.update_subscription(&tag_path, &value).await {
52 let _ = subscription_task.publish_error(&e, true).await;
53 tracing::error!("Error updating subscription: {}", e);
54 break;
55 }
56 }
57 Err(e) => {
58 tracing::error!("Error reading tag {}: {}", tag_path, e);
59 let is_connection_lost = matches!(e, EtherNetIpError::ConnectionLost(_));
60 if is_connection_lost {
61 consecutive_connection_lost += 1;
62 } else {
63 consecutive_connection_lost = 0;
64 }
65
66 let terminal = !e.is_retriable()
67 || consecutive_connection_lost >= CONNECTION_LOST_FAILURE_LIMIT;
68 let _ = subscription_task.publish_error(&e, terminal).await;
69 if terminal {
70 break;
71 }
72
73 tokio::time::sleep(interval.max(retry_backoff)).await;
74 continue;
75 }
76 }
77 if subscription_task.is_active() {
78 tokio::time::sleep(interval).await;
79 }
80 }
81 });
82 Ok(subscription)
83 }
84
85 pub async fn unsubscribe(&self, tag_path: &str) -> bool {
90 let mut subscriptions = self.subscriptions.lock().await;
91 let mut found = false;
92 for subscription in subscriptions.iter() {
93 if subscription.tag_path == tag_path {
94 subscription.stop();
95 found = true;
96 }
97 }
98 subscriptions.retain(TagSubscription::is_active);
99 found
100 }
101
102 #[doc(hidden)]
103 pub async fn subscription_count(&self) -> usize {
104 let mut subscriptions = self.subscriptions.lock().await;
105 subscriptions.retain(TagSubscription::is_active);
106 subscriptions.len()
107 }
108
109 pub async fn subscribe_to_tags(
111 &self,
112 tags: &[(&str, SubscriptionOptions)],
113 ) -> Result<Vec<TagSubscription>> {
114 let mut subs = Vec::with_capacity(tags.len());
115 for (tag_name, options) in tags {
116 subs.push(self.subscribe_to_tag(tag_name, options.clone()).await?);
117 }
118 Ok(subs)
119 }
120
121 pub async fn upsert_tag_group(
126 &self,
127 group_name: &str,
128 tags: &[&str],
129 update_rate_ms: u32,
130 ) -> Result<()> {
131 if group_name.trim().is_empty() {
132 return Err(EtherNetIpError::Protocol(
133 "Tag group name cannot be empty".to_string(),
134 ));
135 }
136 if tags.is_empty() {
137 return Err(EtherNetIpError::Protocol(
138 "Tag group must contain at least one tag".to_string(),
139 ));
140 }
141 if update_rate_ms == 0 {
142 return Err(EtherNetIpError::Protocol(
143 "Tag group update rate must be greater than 0 ms".to_string(),
144 ));
145 }
146
147 let config = TagGroupConfig {
148 name: group_name.to_string(),
149 tags: tags.iter().map(|s| (*s).to_string()).collect(),
150 update_rate_ms,
151 };
152
153 let mut groups = self.tag_groups.lock().await;
154 groups.insert(group_name.to_string(), config);
155 Ok(())
156 }
157
158 pub async fn remove_tag_group(&self, group_name: &str) -> bool {
160 let mut groups = self.tag_groups.lock().await;
161 groups.remove(group_name).is_some()
162 }
163
164 pub async fn list_tag_groups(&self) -> Vec<TagGroupConfig> {
166 let groups = self.tag_groups.lock().await;
167 groups.values().cloned().collect()
168 }
169
170 pub async fn read_tag_group_once(&self, group_name: &str) -> Result<TagGroupSnapshot> {
172 let config = {
173 let groups = self.tag_groups.lock().await;
174 groups.get(group_name).cloned().ok_or_else(|| {
175 EtherNetIpError::Protocol(format!("Tag group '{}' is not registered", group_name))
176 })?
177 };
178
179 let mut client = self.clone();
180 let tag_refs: Vec<&str> = config.tags.iter().map(String::as_str).collect();
181 let values = client.read_tags_batch(&tag_refs).await?;
182
183 let mapped = values
184 .into_iter()
185 .map(|(tag_name, result)| match result {
186 Ok(value) => TagGroupValueResult {
187 tag_name,
188 value: Some(value),
189 error: None,
190 },
191 Err(e) => TagGroupValueResult {
192 tag_name,
193 value: None,
194 error: Some(e.to_string()),
195 },
196 })
197 .collect();
198
199 Ok(TagGroupSnapshot {
200 group_name: config.name,
201 sampled_at: std::time::SystemTime::now(),
202 values: mapped,
203 })
204 }
205
206 pub async fn subscribe_tag_group(&self, group_name: &str) -> Result<TagGroupSubscription> {
210 let config = {
211 let groups = self.tag_groups.lock().await;
212 groups.get(group_name).cloned().ok_or_else(|| {
213 EtherNetIpError::Protocol(format!("Tag group '{}' is not registered", group_name))
214 })?
215 };
216
217 let subscription = TagGroupSubscription::new(config.name.clone(), config.update_rate_ms);
218 let subscription_task = subscription.clone();
219 let mut client = self.clone();
220 let tags = config.tags.clone();
221 let interval = std::time::Duration::from_millis(config.update_rate_ms as u64);
222 let group_name_owned = config.name.clone();
223
224 tokio::spawn(async move {
225 while subscription_task.is_active() {
226 let tag_refs: Vec<&str> = tags.iter().map(String::as_str).collect();
227 match client.read_tags_batch(&tag_refs).await {
228 Ok(values) => {
229 let has_errors = values.iter().any(|(_, result)| result.is_err());
230 let snapshot = TagGroupSnapshot {
231 group_name: group_name_owned.clone(),
232 sampled_at: std::time::SystemTime::now(),
233 values: values
234 .into_iter()
235 .map(|(tag_name, result)| match result {
236 Ok(value) => TagGroupValueResult {
237 tag_name,
238 value: Some(value),
239 error: None,
240 },
241 Err(e) => TagGroupValueResult {
242 tag_name,
243 value: None,
244 error: Some(e.to_string()),
245 },
246 })
247 .collect(),
248 };
249
250 let event = TagGroupEvent {
251 kind: if has_errors {
252 TagGroupEventKind::PartialError
253 } else {
254 TagGroupEventKind::Data
255 },
256 snapshot,
257 error: None,
258 failure: None,
259 };
260
261 if let Err(e) = subscription_task.publish_event(event).await {
262 tracing::error!(
263 "Tag group '{}' publish failed: {}",
264 group_name_owned,
265 e
266 );
267 break;
268 }
269 }
270 Err(e) => {
271 tracing::error!(
272 "Tag group '{}' polling read failed: {}",
273 group_name_owned,
274 e
275 );
276 let failure_event = TagGroupEvent {
277 kind: TagGroupEventKind::ReadFailure,
278 snapshot: TagGroupSnapshot {
279 group_name: group_name_owned.clone(),
280 sampled_at: std::time::SystemTime::now(),
281 values: Vec::new(),
282 },
283 error: Some(e.to_string()),
284 failure: Some(TagGroupFailureDiagnostic::from_error(&e)),
285 };
286 if let Err(publish_error) =
287 subscription_task.publish_event(failure_event).await
288 {
289 tracing::error!(
290 "Tag group '{}' failure-event publish failed: {}",
291 group_name_owned,
292 publish_error
293 );
294 break;
295 }
296 }
297 }
298 tokio::time::sleep(interval).await;
299 }
300 });
301
302 Ok(subscription)
303 }
304
305 async fn update_subscription(&self, tag_name: &str, value: &PlcValue) -> Result<()> {
306 let subscriptions = {
307 let mut subscriptions = self.subscriptions.lock().await;
308 subscriptions.retain(TagSubscription::is_active);
309 subscriptions
310 .iter()
311 .filter(|subscription| subscription.tag_path == tag_name)
312 .cloned()
313 .collect::<Vec<_>>()
314 };
315 for subscription in &subscriptions {
316 if subscription.is_active() {
317 subscription.update_value(value).await?;
318 }
319 }
320 Ok(())
321 }
322}