rust_ethernet_ip/
plc_manager.rs

1use crate::error::{EtherNetIpError, Result};
2use crate::EipClient;
3use std::collections::HashMap;
4use std::net::SocketAddr;
5use std::time::{Duration, Instant};
6
7/// Configuration for a PLC connection
8#[derive(Debug, Clone)]
9pub struct PlcConfig {
10    /// IP address and port of the PLC
11    pub address: SocketAddr,
12    /// Maximum number of connections to maintain
13    pub max_connections: u32,
14    /// Connection timeout in milliseconds
15    pub connection_timeout: Duration,
16    /// Health check interval in milliseconds
17    pub health_check_interval: Duration,
18    /// Maximum packet size in bytes
19    pub max_packet_size: usize,
20}
21
22impl Default for PlcConfig {
23    fn default() -> Self {
24        Self {
25            address: "127.0.0.1:44818".parse().unwrap(),
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/// Represents the health status of a PLC connection
35#[derive(Debug, Clone)]
36pub struct ConnectionHealth {
37    /// Whether the connection is currently active
38    pub is_active: bool,
39    /// Last successful communication timestamp
40    pub last_success: Instant,
41    /// Number of failed attempts since last success
42    pub failed_attempts: u32,
43    /// Current connection latency in milliseconds
44    pub latency: Duration,
45}
46
47/// Represents a connection to a PLC
48#[derive(Debug)]
49pub struct PlcConnection {
50    /// The EIP client instance
51    client: EipClient,
52    /// Health status of the connection
53    health: ConnectionHealth,
54    /// Last time this connection was used
55    last_used: Instant,
56}
57
58impl PlcConnection {
59    /// Creates a new PLC connection
60    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    /// Updates the health status of the connection
74    #[allow(dead_code)]
75    pub fn update_health(&mut self, is_active: bool, latency: Duration) {
76        self.health.is_active = is_active;
77        if is_active {
78            self.health.last_success = Instant::now();
79            self.health.failed_attempts = 0;
80            self.health.latency = latency;
81        } else {
82            self.health.failed_attempts += 1;
83        }
84    }
85}
86
87/// Manager for multiple PLC connections
88#[derive(Debug)]
89pub struct PlcManager {
90    /// Configuration for each PLC
91    configs: HashMap<SocketAddr, PlcConfig>,
92    /// Active connections for each PLC
93    connections: HashMap<SocketAddr, Vec<PlcConnection>>,
94    /// Health check interval
95    #[allow(dead_code)]
96    health_check_interval: Duration,
97}
98
99impl Default for PlcManager {
100    fn default() -> Self {
101        Self::new()
102    }
103}
104
105impl PlcManager {
106    /// Creates a new PLC manager
107    pub fn new() -> Self {
108        Self {
109            configs: HashMap::new(),
110            connections: HashMap::new(),
111            health_check_interval: Duration::from_secs(30),
112        }
113    }
114
115    /// Adds a PLC configuration
116    pub fn add_plc(&mut self, config: PlcConfig) {
117        self.configs.insert(config.address, config);
118    }
119
120    /// Gets a connection to a PLC
121    pub async fn get_connection(&mut self, address: SocketAddr) -> Result<&mut EipClient> {
122        let config = self
123            .configs
124            .get(&address)
125            .ok_or_else(|| EtherNetIpError::Connection("PLC not configured".to_string()))?;
126
127        // First check if we have any connections for this address
128        if !self.connections.contains_key(&address) {
129            // No connections exist, create a new one
130            let mut client = EipClient::new(&address.to_string()).await?;
131            client.set_max_packet_size(config.max_packet_size as u32);
132            let mut new_conn = PlcConnection::new(client);
133            new_conn.last_used = Instant::now();
134            self.connections.insert(address, vec![new_conn]);
135            return Ok(&mut self.connections.get_mut(&address).unwrap()[0].client);
136        }
137
138        // Get mutable access to the connections
139        let connections = self.connections.get_mut(&address).unwrap();
140
141        // First try to find an inactive connection
142        for i in 0..connections.len() {
143            if !connections[i].health.is_active {
144                let mut client = EipClient::new(&address.to_string()).await?;
145                client.set_max_packet_size(config.max_packet_size as u32);
146                connections[i].client = client;
147                connections[i].health.is_active = true;
148                connections[i].health.last_success = Instant::now();
149                connections[i].health.failed_attempts = 0;
150                connections[i].health.latency = Duration::from_millis(0);
151                connections[i].last_used = Instant::now();
152                return Ok(&mut connections[i].client);
153            }
154        }
155
156        // If we have room for more connections, create a new one
157        if connections.len() < config.max_connections as usize {
158            let mut client = EipClient::new(&address.to_string()).await?;
159            client.set_max_packet_size(config.max_packet_size as u32);
160            let mut new_conn = PlcConnection::new(client);
161            new_conn.last_used = Instant::now();
162            connections.push(new_conn);
163            return Ok(&mut connections.last_mut().unwrap().client);
164        }
165
166        // Find the least recently used connection
167        let lru_index = connections
168            .iter()
169            .enumerate()
170            .min_by_key(|(_, conn)| conn.last_used)
171            .map(|(i, _)| i)
172            .unwrap();
173
174        // Update the LRU connection
175        let mut client = EipClient::new(&address.to_string()).await?;
176        client.set_max_packet_size(config.max_packet_size as u32);
177        connections[lru_index].client = client;
178        connections[lru_index].health.is_active = true;
179        connections[lru_index].health.last_success = Instant::now();
180        connections[lru_index].health.failed_attempts = 0;
181        connections[lru_index].health.latency = Duration::from_millis(0);
182        connections[lru_index].last_used = Instant::now();
183        Ok(&mut connections[lru_index].client)
184    }
185
186    /// Performs health checks on all connections
187    pub async fn check_health(&mut self) {
188        for (address, connections) in &mut self.connections {
189            let _config = self.configs.get(address).unwrap();
190
191            for conn in connections.iter_mut() {
192                if !conn.health.is_active {
193                    if let Ok(new_client) = EipClient::new(&address.to_string()).await {
194                        conn.client = new_client;
195                        conn.health.is_active = true;
196                        conn.health.last_success = Instant::now();
197                        conn.health.failed_attempts = 0;
198                        conn.health.latency = Duration::from_millis(0);
199                        conn.last_used = Instant::now();
200                    }
201                }
202            }
203        }
204    }
205
206    /// Removes inactive connections
207    pub fn cleanup_connections(&mut self) {
208        for connections in self.connections.values_mut() {
209            connections.retain(|conn| conn.health.is_active);
210        }
211    }
212
213    pub async fn get_client(&mut self, address: &str) -> Result<&mut EipClient> {
214        let addr = address
215            .parse::<SocketAddr>()
216            .map_err(|_| EtherNetIpError::Connection("Invalid address format".to_string()))?;
217        self.get_connection(addr).await
218    }
219
220    async fn _get_or_create_connection(&mut self, address: &SocketAddr) -> Result<&mut EipClient> {
221        let config = self.configs.get(address).cloned().unwrap();
222        let connections = self.connections.entry(*address).or_default();
223
224        // Try to find an existing inactive connection
225        for i in 0..connections.len() {
226            if !connections[i].health.is_active {
227                let mut client = EipClient::new(&address.to_string()).await?;
228                client.set_max_packet_size(config.max_packet_size as u32);
229                connections[i].client = client;
230                connections[i].health.is_active = true;
231                connections[i].health.last_success = Instant::now();
232                connections[i].health.failed_attempts = 0;
233                connections[i].health.latency = Duration::from_millis(0);
234                connections[i].last_used = Instant::now();
235                return Ok(&mut connections[i].client);
236            }
237        }
238
239        // If we have room for more connections, create a new one
240        if connections.len() < config.max_connections as usize {
241            let mut client = EipClient::new(&address.to_string()).await?;
242            client.set_max_packet_size(config.max_packet_size as u32);
243            let mut new_conn = PlcConnection::new(client);
244            new_conn.last_used = Instant::now();
245            connections.push(new_conn);
246            return Ok(&mut connections.last_mut().unwrap().client);
247        }
248
249        // Find the least recently used connection
250        let lru_index = connections
251            .iter()
252            .enumerate()
253            .min_by_key(|(_, conn)| conn.last_used)
254            .map(|(i, _)| i)
255            .unwrap();
256
257        // Update the LRU connection
258        let mut client = EipClient::new(&address.to_string()).await?;
259        client.set_max_packet_size(config.max_packet_size as u32);
260        connections[lru_index].client = client;
261        connections[lru_index].health.is_active = true;
262        connections[lru_index].health.last_success = Instant::now();
263        connections[lru_index].health.failed_attempts = 0;
264        connections[lru_index].health.latency = Duration::from_millis(0);
265        connections[lru_index].last_used = Instant::now();
266        Ok(&mut connections[lru_index].client)
267    }
268}
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273
274    #[test]
275    fn test_plc_config_default() {
276        let config = PlcConfig::default();
277        assert_eq!(config.max_connections, 5);
278        assert_eq!(config.max_packet_size, 4000);
279    }
280
281    #[tokio::test]
282    async fn test_plc_manager_connection_pool() {
283        let mut manager = PlcManager::new();
284        let config = PlcConfig {
285            address: "127.0.0.1:44818".parse().unwrap(),
286            max_connections: 2,
287            ..Default::default()
288        };
289        manager.add_plc(config.clone());
290
291        // This will fail in tests since there's no actual PLC
292        // but it demonstrates the connection pool logic
293        let result = manager.get_connection(config.address).await;
294        assert!(result.is_err());
295    }
296}