1use super::EipClient;
2use crate::batch::{BatchError, BatchOperation, BatchResult};
3use crate::error::{EtherNetIpError, Result};
4use crate::monitoring::DiagnosticsSnapshot;
5use crate::route::RoutePath;
6use crate::types::PlcValue;
7use std::time::Duration;
8use tokio::sync::{broadcast, mpsc, oneshot};
9
10type BatchReadResults = Vec<(String, std::result::Result<PlcValue, BatchError>)>;
11type BatchWriteResults = Vec<(String, std::result::Result<(), BatchError>)>;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum ConnectionEvent {
15 Connected,
16 Disconnected,
17 WorkerStopped,
18}
19
20#[derive(Debug, Clone)]
21pub struct Client {
22 tx: mpsc::Sender<ClientCommand>,
23 events: broadcast::Sender<ConnectionEvent>,
24}
25
26#[derive(Debug, Clone)]
27pub enum Backoff {
28 Constant(Duration),
29 Exponential {
30 initial: Duration,
31 max: Duration,
32 factor: u32,
33 },
34}
35
36#[derive(Debug, Clone)]
37pub struct RetryPolicy {
38 pub max_attempts: usize,
39 pub backoff: Backoff,
40 pub retry_writes: bool,
41}
42
43impl RetryPolicy {
44 pub fn constant(max_attempts: usize, delay: Duration) -> Self {
45 Self {
46 max_attempts: max_attempts.max(1),
47 backoff: Backoff::Constant(delay),
48 retry_writes: false,
49 }
50 }
51
52 pub fn exponential(max_attempts: usize, initial: Duration, max: Duration) -> Self {
53 Self {
54 max_attempts: max_attempts.max(1),
55 backoff: Backoff::Exponential {
56 initial,
57 max,
58 factor: 2,
59 },
60 retry_writes: false,
61 }
62 }
63
64 pub fn retry_writes(mut self, retry_writes: bool) -> Self {
65 self.retry_writes = retry_writes;
66 self
67 }
68
69 fn delay_for_attempt(&self, attempt_index: usize) -> Duration {
70 match self.backoff {
71 Backoff::Constant(delay) => delay,
72 Backoff::Exponential {
73 initial,
74 max,
75 factor,
76 } => {
77 let multiplier = factor.saturating_pow(attempt_index as u32);
78 initial.saturating_mul(multiplier).min(max)
79 }
80 }
81 }
82}
83
84#[derive(Clone)]
85pub struct RetryClient {
86 client: Client,
87 policy: RetryPolicy,
88}
89
90enum ClientCommand {
91 ReadTag {
92 tag_name: String,
93 reply: oneshot::Sender<Result<PlcValue>>,
94 },
95 WriteTag {
96 tag_name: String,
97 value: PlcValue,
98 reply: oneshot::Sender<Result<()>>,
99 },
100 WriteStringTag {
101 tag_name: String,
102 value: String,
103 reply: oneshot::Sender<Result<()>>,
104 },
105 WriteUdtMember {
106 tag_name: String,
107 member_name: String,
108 value: PlcValue,
109 reply: oneshot::Sender<Result<()>>,
110 },
111 ExecuteBatch {
112 operations: Vec<BatchOperation>,
113 reply: oneshot::Sender<Result<Vec<BatchResult>>>,
114 },
115 ReadTagsBatch {
116 tag_names: Vec<String>,
117 reply: oneshot::Sender<Result<BatchReadResults>>,
118 },
119 WriteTagsBatch {
120 tag_values: Vec<(String, PlcValue)>,
121 reply: oneshot::Sender<Result<BatchWriteResults>>,
122 },
123 CheckHealth {
124 reply: oneshot::Sender<bool>,
125 },
126 Diagnostics {
127 verified: bool,
128 reply: oneshot::Sender<Result<DiagnosticsSnapshot>>,
129 },
130}
131
132impl Client {
133 pub async fn connect(addr: &str) -> Result<Self> {
134 Self::from_eip_client(EipClient::connect(addr).await?)
135 }
136
137 pub async fn with_route_path(addr: &str, route: RoutePath) -> Result<Self> {
138 Self::from_eip_client(EipClient::with_route_path(addr, route).await?)
139 }
140
141 pub fn from_eip_client(client: EipClient) -> Result<Self> {
142 let (tx, rx) = mpsc::channel(128);
143 let (events, _) = broadcast::channel(128);
144 let actor = Self {
145 tx,
146 events: events.clone(),
147 };
148
149 tokio::spawn(run_client_actor(client, rx, events));
150 Ok(actor)
151 }
152
153 pub fn events(&self) -> broadcast::Receiver<ConnectionEvent> {
154 self.events.subscribe()
155 }
156
157 pub fn with_retry(&self, policy: RetryPolicy) -> RetryClient {
158 RetryClient {
159 client: self.clone(),
160 policy,
161 }
162 }
163
164 pub async fn read_tag(&self, tag_name: &str) -> Result<PlcValue> {
165 let (reply, rx) = oneshot::channel();
166 self.send(ClientCommand::ReadTag {
167 tag_name: tag_name.to_string(),
168 reply,
169 })
170 .await?;
171 rx.await.unwrap_or_else(|_| actor_stopped())
172 }
173
174 pub async fn write_tag(&self, tag_name: &str, value: PlcValue) -> Result<()> {
175 let (reply, rx) = oneshot::channel();
176 self.send(ClientCommand::WriteTag {
177 tag_name: tag_name.to_string(),
178 value,
179 reply,
180 })
181 .await?;
182 rx.await.unwrap_or_else(|_| actor_stopped())
183 }
184
185 pub async fn write_string_tag(&self, tag_name: &str, value: &str) -> Result<()> {
186 let (reply, rx) = oneshot::channel();
187 self.send(ClientCommand::WriteStringTag {
188 tag_name: tag_name.to_string(),
189 value: value.to_string(),
190 reply,
191 })
192 .await?;
193 rx.await.unwrap_or_else(|_| actor_stopped())
194 }
195
196 pub async fn write_udt_member(
197 &self,
198 udt_tag_name: &str,
199 member_name: &str,
200 value: PlcValue,
201 ) -> Result<()> {
202 let (reply, rx) = oneshot::channel();
203 self.send(ClientCommand::WriteUdtMember {
204 tag_name: udt_tag_name.to_string(),
205 member_name: member_name.to_string(),
206 value,
207 reply,
208 })
209 .await?;
210 rx.await.unwrap_or_else(|_| actor_stopped())
211 }
212
213 pub async fn write_udt_array_member(
214 &self,
215 udt_array_element_path: &str,
216 member_name: &str,
217 value: PlcValue,
218 ) -> Result<()> {
219 self.write_udt_member(udt_array_element_path, member_name, value)
220 .await
221 }
222
223 pub async fn execute_batch(&self, operations: &[BatchOperation]) -> Result<Vec<BatchResult>> {
224 let (reply, rx) = oneshot::channel();
225 self.send(ClientCommand::ExecuteBatch {
226 operations: operations.to_vec(),
227 reply,
228 })
229 .await?;
230 rx.await.unwrap_or_else(|_| actor_stopped())
231 }
232
233 pub async fn read_tags_batch(&self, tag_names: &[&str]) -> Result<BatchReadResults> {
234 let (reply, rx) = oneshot::channel();
235 self.send(ClientCommand::ReadTagsBatch {
236 tag_names: tag_names.iter().map(|name| (*name).to_string()).collect(),
237 reply,
238 })
239 .await?;
240 rx.await.unwrap_or_else(|_| actor_stopped())
241 }
242
243 pub async fn write_tags_batch(
244 &self,
245 tag_values: &[(&str, PlcValue)],
246 ) -> Result<BatchWriteResults> {
247 let (reply, rx) = oneshot::channel();
248 self.send(ClientCommand::WriteTagsBatch {
249 tag_values: tag_values
250 .iter()
251 .map(|(name, value)| ((*name).to_string(), value.clone()))
252 .collect(),
253 reply,
254 })
255 .await?;
256 rx.await.unwrap_or_else(|_| actor_stopped())
257 }
258
259 pub async fn check_health(&self) -> Result<bool> {
260 let (reply, rx) = oneshot::channel();
261 self.send(ClientCommand::CheckHealth { reply }).await?;
262 rx.await.map_err(|_| actor_stopped_error())
263 }
264
265 pub async fn get_diagnostics_snapshot(&self) -> Result<DiagnosticsSnapshot> {
266 self.diagnostics(false).await
267 }
268
269 pub async fn get_diagnostics_snapshot_detailed(&self) -> Result<DiagnosticsSnapshot> {
270 self.diagnostics(true).await
271 }
272
273 async fn diagnostics(&self, verified: bool) -> Result<DiagnosticsSnapshot> {
274 let (reply, rx) = oneshot::channel();
275 self.send(ClientCommand::Diagnostics { verified, reply })
276 .await?;
277 rx.await.unwrap_or_else(|_| actor_stopped())
278 }
279
280 async fn send(&self, command: ClientCommand) -> Result<()> {
281 self.tx
282 .send(command)
283 .await
284 .map_err(|_| actor_stopped_error())
285 }
286}
287
288impl RetryClient {
289 pub async fn read_tag(&self, tag_name: &str) -> Result<PlcValue> {
290 self.retry(|| async { self.client.read_tag(tag_name).await })
291 .await
292 }
293
294 pub async fn write_tag(&self, tag_name: &str, value: PlcValue) -> Result<()> {
295 if !self.policy.retry_writes {
296 return self.client.write_tag(tag_name, value).await;
297 }
298
299 self.retry(|| {
300 let value = value.clone();
301 async move { self.client.write_tag(tag_name, value).await }
302 })
303 .await
304 }
305
306 pub async fn write_string_tag(&self, tag_name: &str, value: &str) -> Result<()> {
307 if !self.policy.retry_writes {
308 return self.client.write_string_tag(tag_name, value).await;
309 }
310
311 self.retry(|| async { self.client.write_string_tag(tag_name, value).await })
312 .await
313 }
314
315 async fn retry<T, Fut, Op>(&self, mut op: Op) -> Result<T>
316 where
317 Fut: std::future::Future<Output = Result<T>>,
318 Op: FnMut() -> Fut,
319 {
320 let mut attempt = 0;
321 loop {
322 match op().await {
323 Ok(value) => return Ok(value),
324 Err(err) if err.is_retriable() && attempt + 1 < self.policy.max_attempts => {
325 let delay = self.policy.delay_for_attempt(attempt);
326 attempt += 1;
327 tokio::time::sleep(delay).await;
328 }
329 Err(err) => return Err(err),
330 }
331 }
332 }
333}
334
335async fn run_client_actor(
336 mut client: EipClient,
337 mut rx: mpsc::Receiver<ClientCommand>,
338 events: broadcast::Sender<ConnectionEvent>,
339) {
340 let _ = events.send(ConnectionEvent::Connected);
341
342 while let Some(command) = rx.recv().await {
343 match command {
344 ClientCommand::ReadTag { tag_name, reply } => {
345 let _ = reply.send(client.read_tag(&tag_name).await);
346 }
347 ClientCommand::WriteTag {
348 tag_name,
349 value,
350 reply,
351 } => {
352 let _ = reply.send(client.write_tag(&tag_name, value).await);
353 }
354 ClientCommand::WriteStringTag {
355 tag_name,
356 value,
357 reply,
358 } => {
359 let _ = reply.send(client.write_string_tag(&tag_name, &value).await);
360 }
361 ClientCommand::WriteUdtMember {
362 tag_name,
363 member_name,
364 value,
365 reply,
366 } => {
367 let _ = reply.send(
368 client
369 .write_udt_member(&tag_name, &member_name, value)
370 .await,
371 );
372 }
373 ClientCommand::ExecuteBatch { operations, reply } => {
374 let _ = reply.send(client.execute_batch(&operations).await);
375 }
376 ClientCommand::ReadTagsBatch { tag_names, reply } => {
377 let refs: Vec<&str> = tag_names.iter().map(String::as_str).collect();
378 let _ = reply.send(client.read_tags_batch(&refs).await);
379 }
380 ClientCommand::WriteTagsBatch { tag_values, reply } => {
381 let refs: Vec<(&str, PlcValue)> = tag_values
382 .iter()
383 .map(|(name, value)| (name.as_str(), value.clone()))
384 .collect();
385 let _ = reply.send(client.write_tags_batch(&refs).await);
386 }
387 ClientCommand::CheckHealth { reply } => {
388 let _ = reply.send(client.check_health().await);
389 }
390 ClientCommand::Diagnostics { verified, reply } => {
391 let result = if verified {
392 client.get_diagnostics_snapshot_detailed().await
393 } else {
394 Ok(client.get_diagnostics_snapshot().await)
395 };
396 let _ = reply.send(result);
397 }
398 }
399 }
400
401 let _ = client.unregister_session().await;
402 let _ = events.send(ConnectionEvent::Disconnected);
403 let _ = events.send(ConnectionEvent::WorkerStopped);
404}
405
406fn actor_stopped<T>() -> Result<T> {
407 Err(actor_stopped_error())
408}
409
410fn actor_stopped_error() -> EtherNetIpError {
411 EtherNetIpError::ConnectionLost("client actor stopped".to_string())
412}