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