rust_ethernet_ip/
plc_manager.rs

1use std::collections::HashMap;
2use std::time::{Duration, Instant};
3use std::net::SocketAddr;
4use crate::error::{EtherNetIpError, Result};
5use crate::EipClient;
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 PlcManager {
100    /// Creates a new PLC manager
101    pub fn new() -> Self {
102        Self {
103            configs: HashMap::new(),
104            connections: HashMap::new(),
105            health_check_interval: Duration::from_secs(30),
106        }
107    }
108
109    /// Adds a PLC configuration
110    pub fn add_plc(&mut self, config: PlcConfig) {
111        self.configs.insert(config.address, config);
112    }
113
114    /// Gets a connection to a PLC
115    pub async fn get_connection(&mut self, address: SocketAddr) -> Result<&mut EipClient> {
116        let config = self.configs.get(&address)
117            .ok_or_else(|| EtherNetIpError::Connection("PLC not configured".to_string()))?;
118
119        // First check if we have any connections for this address
120        if !self.connections.contains_key(&address) {
121            // No connections exist, create a new one
122            let mut client = EipClient::new(&address.to_string()).await?;
123            client.set_max_packet_size(config.max_packet_size as u32);
124            let mut new_conn = PlcConnection::new(client);
125            new_conn.last_used = Instant::now();
126            self.connections.insert(address, vec![new_conn]);
127            return Ok(&mut self.connections.get_mut(&address).unwrap()[0].client);
128        }
129
130        // Get mutable access to the connections
131        let connections = self.connections.get_mut(&address).unwrap();
132
133        // First try to find an inactive connection
134        for i in 0..connections.len() {
135            if !connections[i].health.is_active {
136                let mut client = EipClient::new(&address.to_string()).await?;
137                client.set_max_packet_size(config.max_packet_size as u32);
138                connections[i].client = client;
139                connections[i].health.is_active = true;
140                connections[i].health.last_success = Instant::now();
141                connections[i].health.failed_attempts = 0;
142                connections[i].health.latency = Duration::from_millis(0);
143                connections[i].last_used = Instant::now();
144                return Ok(&mut connections[i].client);
145            }
146        }
147
148        // If we have room for more connections, create a new one
149        if connections.len() < config.max_connections as usize {
150            let mut client = EipClient::new(&address.to_string()).await?;
151            client.set_max_packet_size(config.max_packet_size as u32);
152            let mut new_conn = PlcConnection::new(client);
153            new_conn.last_used = Instant::now();
154            connections.push(new_conn);
155            return Ok(&mut connections.last_mut().unwrap().client);
156        }
157
158        // Find the least recently used connection
159        let lru_index = connections.iter()
160            .enumerate()
161            .min_by_key(|(_, conn)| conn.last_used)
162            .map(|(i, _)| i)
163            .unwrap();
164
165        // Update the LRU connection
166        let mut client = EipClient::new(&address.to_string()).await?;
167        client.set_max_packet_size(config.max_packet_size as u32);
168        connections[lru_index].client = client;
169        connections[lru_index].health.is_active = true;
170        connections[lru_index].health.last_success = Instant::now();
171        connections[lru_index].health.failed_attempts = 0;
172        connections[lru_index].health.latency = Duration::from_millis(0);
173        connections[lru_index].last_used = Instant::now();
174        Ok(&mut connections[lru_index].client)
175    }
176
177    /// Performs health checks on all connections
178    pub async fn check_health(&mut self) {
179        for (address, connections) in &mut self.connections {
180            let _config = self.configs.get(address).unwrap();
181            
182            for conn in connections.iter_mut() {
183                if !conn.health.is_active {
184                    if let Ok(new_client) = EipClient::new(&address.to_string()).await {
185                        conn.client = new_client;
186                        conn.health.is_active = true;
187                        conn.health.last_success = Instant::now();
188                        conn.health.failed_attempts = 0;
189                        conn.health.latency = Duration::from_millis(0);
190                        conn.last_used = Instant::now();
191                    }
192                }
193            }
194        }
195    }
196
197    /// Removes inactive connections
198    pub fn cleanup_connections(&mut self) {
199        for connections in self.connections.values_mut() {
200            connections.retain(|conn| conn.health.is_active);
201        }
202    }
203
204    pub async fn get_client(&mut self, address: &str) -> Result<&mut EipClient> {
205        let addr = address.parse::<SocketAddr>()
206            .map_err(|_| EtherNetIpError::Connection("Invalid address format".to_string()))?;
207        self.get_connection(addr).await
208    }
209
210    async fn _get_or_create_connection(&mut self, address: &SocketAddr) -> Result<&mut EipClient> {
211        let config = self.configs.get(address).cloned().unwrap();
212        let connections = self.connections.entry(*address).or_insert_with(Vec::new);
213
214        // Try to find an existing inactive connection
215        for i in 0..connections.len() {
216            if !connections[i].health.is_active {
217                let mut client = EipClient::new(&address.to_string()).await?;
218                client.set_max_packet_size(config.max_packet_size as u32);
219                connections[i].client = client;
220                connections[i].health.is_active = true;
221                connections[i].health.last_success = Instant::now();
222                connections[i].health.failed_attempts = 0;
223                connections[i].health.latency = Duration::from_millis(0);
224                connections[i].last_used = Instant::now();
225                return Ok(&mut connections[i].client);
226            }
227        }
228
229        // If we have room for more connections, create a new one
230        if connections.len() < config.max_connections as usize {
231            let mut client = EipClient::new(&address.to_string()).await?;
232            client.set_max_packet_size(config.max_packet_size as u32);
233            let mut new_conn = PlcConnection::new(client);
234            new_conn.last_used = Instant::now();
235            connections.push(new_conn);
236            return Ok(&mut connections.last_mut().unwrap().client);
237        }
238
239        // Find the least recently used connection
240        let lru_index = connections.iter()
241            .enumerate()
242            .min_by_key(|(_, conn)| conn.last_used)
243            .map(|(i, _)| i)
244            .unwrap();
245
246        // Update the LRU connection
247        let mut client = EipClient::new(&address.to_string()).await?;
248        client.set_max_packet_size(config.max_packet_size as u32);
249        connections[lru_index].client = client;
250        connections[lru_index].health.is_active = true;
251        connections[lru_index].health.last_success = Instant::now();
252        connections[lru_index].health.failed_attempts = 0;
253        connections[lru_index].health.latency = Duration::from_millis(0);
254        connections[lru_index].last_used = Instant::now();
255        Ok(&mut connections[lru_index].client)
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262
263    #[test]
264    fn test_plc_config_default() {
265        let config = PlcConfig::default();
266        assert_eq!(config.max_connections, 5);
267        assert_eq!(config.max_packet_size, 4000);
268    }
269
270    #[tokio::test]
271    async fn test_plc_manager_connection_pool() {
272        let mut manager = PlcManager::new();
273        let config = PlcConfig {
274            address: "127.0.0.1:44818".parse().unwrap(),
275            max_connections: 2,
276            ..Default::default()
277        };
278        manager.add_plc(config.clone());
279
280        // This will fail in tests since there's no actual PLC
281        // but it demonstrates the connection pool logic
282        let result = manager.get_connection(config.address).await;
283        assert!(result.is_err());
284    }
285}