rust_ethernet_ip/
plc_manager.rs1use crate::EipClient;
2use crate::error::{EtherNetIpError, Result};
3use std::collections::HashMap;
4use std::net::SocketAddr;
5use std::time::{Duration, Instant};
6
7#[derive(Debug, Clone)]
9pub struct PlcConfig {
10 pub address: SocketAddr,
12 pub max_connections: u32,
14 pub connection_timeout: Duration,
16 pub health_check_interval: Duration,
18 pub max_packet_size: usize,
20}
21
22impl Default for PlcConfig {
23 fn default() -> Self {
24 Self {
25 address: SocketAddr::from(([127, 0, 0, 1], 44818)),
26 max_connections: 5,
27 connection_timeout: Duration::from_secs(5),
28 health_check_interval: Duration::from_secs(30),
29 max_packet_size: 4000,
30 }
31 }
32}
33
34#[derive(Debug, Clone)]
36pub struct ConnectionHealth {
37 pub is_active: bool,
39 pub last_success: Instant,
41 pub failed_attempts: u32,
43 pub latency: Duration,
45}
46
47#[derive(Debug)]
49pub struct PlcConnection {
50 client: EipClient,
52 health: ConnectionHealth,
54 last_used: Instant,
56}
57
58impl PlcConnection {
59 pub fn new(client: EipClient) -> Self {
61 Self {
62 client,
63 health: ConnectionHealth {
64 is_active: true,
65 last_success: Instant::now(),
66 failed_attempts: 0,
67 latency: Duration::from_millis(0),
68 },
69 last_used: Instant::now(),
70 }
71 }
72
73 pub fn update_health(&mut self, is_active: bool, latency: Duration) {
75 self.health.is_active = is_active;
76 if is_active {
77 self.health.last_success = Instant::now();
78 self.health.failed_attempts = 0;
79 self.health.latency = latency;
80 } else {
81 self.health.failed_attempts += 1;
82 }
83 }
84}
85
86#[derive(Debug)]
88#[deprecated(
89 since = "1.2.0",
90 note = "PlcManager predates Fleet and cannot provide concurrent pooled use through its &mut self API; use Fleet for multi-PLC management. The type will be removed in 2.0."
91)]
92pub struct PlcManager {
93 configs: HashMap<SocketAddr, PlcConfig>,
95 connections: HashMap<SocketAddr, Vec<PlcConnection>>,
97}
98
99#[expect(
100 deprecated,
101 reason = "CODEX-AQ keeps PlcManager compatibility until 2.0 removal"
102)]
103impl Default for PlcManager {
104 fn default() -> Self {
105 Self::new()
106 }
107}
108
109#[expect(
110 deprecated,
111 reason = "CODEX-AQ keeps PlcManager compatibility until 2.0 removal"
112)]
113impl PlcManager {
114 pub fn new() -> Self {
116 Self {
117 configs: HashMap::new(),
118 connections: HashMap::new(),
119 }
120 }
121
122 pub fn add_plc(&mut self, config: PlcConfig) {
124 self.configs.insert(config.address, config);
125 }
126
127 pub async fn get_connection(&mut self, address: SocketAddr) -> Result<&mut EipClient> {
129 let config = self
130 .configs
131 .get(&address)
132 .ok_or_else(|| EtherNetIpError::Connection("PLC not configured".to_string()))?;
133 let max_connections = config.max_connections as usize;
134 let max_packet_size = config.max_packet_size as u32;
135
136 if let std::collections::hash_map::Entry::Vacant(e) = self.connections.entry(address) {
138 let mut client = EipClient::new(&address.to_string()).await?;
140 client.set_max_packet_size(max_packet_size);
141 let mut new_conn = PlcConnection::new(client);
142 new_conn.last_used = Instant::now();
143 e.insert(vec![new_conn]);
144 let connections = self.connections.get_mut(&address).ok_or_else(|| {
145 EtherNetIpError::Connection("connection pool missing after insert".to_string())
146 })?;
147 let connection = connections.first_mut().ok_or_else(|| {
148 EtherNetIpError::Connection("connection pool was empty after insert".to_string())
149 })?;
150 return Ok(&mut connection.client);
151 }
152
153 let connections = self
155 .connections
156 .get_mut(&address)
157 .ok_or_else(|| EtherNetIpError::Connection("connection pool missing".to_string()))?;
158
159 if let Some(i) = connections
161 .iter()
162 .position(|connection| !connection.health.is_active)
163 {
164 let mut client = EipClient::new(&address.to_string()).await?;
165 client.set_max_packet_size(max_packet_size);
166 let connection = &mut connections[i];
167 connection.client = client;
168 connection.health.is_active = true;
169 connection.health.last_success = Instant::now();
170 connection.health.failed_attempts = 0;
171 connection.health.latency = Duration::from_millis(0);
172 connection.last_used = Instant::now();
173 return Ok(&mut connection.client);
174 }
175
176 if connections.len() < max_connections {
178 let mut client = EipClient::new(&address.to_string()).await?;
179 client.set_max_packet_size(max_packet_size);
180 let mut new_conn = PlcConnection::new(client);
181 new_conn.last_used = Instant::now();
182 connections.push(new_conn);
183 let connection = connections.last_mut().ok_or_else(|| {
184 EtherNetIpError::Connection("connection pool was empty after push".to_string())
185 })?;
186 return Ok(&mut connection.client);
187 }
188
189 let lru_index = connections
192 .iter()
193 .enumerate()
194 .min_by_key(|(_, conn)| conn.last_used)
195 .map(|(i, _)| i)
196 .ok_or_else(|| EtherNetIpError::Connection("connection pool is empty".to_string()))?;
197
198 connections[lru_index].last_used = Instant::now();
200 Ok(&mut connections[lru_index].client)
201 }
202
203 pub async fn check_health(&mut self) {
205 for (address, connections) in &mut self.connections {
206 for conn in connections.iter_mut() {
207 if !conn.health.is_active {
208 let new_client = EipClient::new(&address.to_string()).await;
209 if let Ok(new_client) = new_client {
210 conn.client = new_client;
211 conn.health.is_active = true;
212 conn.health.last_success = Instant::now();
213 conn.health.failed_attempts = 0;
214 conn.health.latency = Duration::from_millis(0);
215 conn.last_used = Instant::now();
216 }
217 }
218 }
219 }
220 }
221
222 pub fn cleanup_connections(&mut self) {
224 for connections in self.connections.values_mut() {
225 connections.retain(|conn| conn.health.is_active);
226 }
227 }
228
229 pub async fn get_client(&mut self, address: &str) -> Result<&mut EipClient> {
230 let addr = address
231 .parse::<SocketAddr>()
232 .map_err(|_| EtherNetIpError::Connection("Invalid address format".to_string()))?;
233 self.get_connection(addr).await
234 }
235}
236
237#[cfg(test)]
238#[expect(
239 deprecated,
240 reason = "CODEX-AQ keeps PlcManager unit coverage until 2.0 removal"
241)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn test_plc_config_default() {
247 let config = PlcConfig::default();
248 assert_eq!(config.max_connections, 5);
249 assert_eq!(config.max_packet_size, 4000);
250 }
251
252 #[tokio::test]
253 async fn test_plc_manager_connection_pool() {
254 let mut manager = PlcManager::new();
255 let config = PlcConfig {
256 address: "127.0.0.1:44818".parse().unwrap(),
257 max_connections: 2,
258 ..Default::default()
259 };
260 manager.add_plc(config.clone());
261
262 let result = manager.get_connection(config.address).await;
265 assert!(result.is_err());
266 }
267}