Skip to main content

rust_ethernet_ip/client/
diagnostics.rs

1use super::EipClient;
2use tokio::time::Duration;
3
4impl EipClient {
5    /// Quick connection health check (no I/O).
6    ///
7    /// Returns `true` if the session handle is valid and there has been activity
8    /// within the last 150 seconds. Use this for cheap periodic checks; for a
9    /// definitive check that the PLC is still responding, use [`check_health_detailed`](Self::check_health_detailed).
10    pub async fn check_health(&self) -> bool {
11        self.session_handle() != 0
12            && self.last_activity.lock().await.elapsed() < Duration::from_secs(150)
13    }
14
15    /// Builds a lightweight diagnostics snapshot from the current client state.
16    pub async fn get_diagnostics_snapshot(&self) -> crate::DiagnosticsSnapshot {
17        self.build_diagnostics_snapshot(crate::HealthCheckMode::Passive, self.check_health().await)
18            .await
19    }
20
21    /// Verifies the connection by sending a keep-alive (and re-registering if needed).
22    ///
23    /// Use this when you need to confirm the PLC is still reachable (e.g. after
24    /// a long idle or before a critical operation). On failure, consider
25    /// reconnecting; check [`EtherNetIpError::is_retriable`](crate::error::EtherNetIpError::is_retriable) on errors.
26    pub async fn check_health_detailed(&mut self) -> crate::error::Result<bool> {
27        if self.session_handle() == 0 {
28            return Ok(false);
29        }
30
31        // Try sending a lightweight keep-alive command
32        match self.send_keep_alive().await {
33            Ok(()) => Ok(true),
34            Err(_) => {
35                // If keep-alive fails, try re-registering the shared session.
36                // The new handle is atomically visible to all clones using this stream.
37                match self.register_session().await {
38                    Ok(()) => Ok(true),
39                    Err(_) => Ok(false),
40                }
41            }
42        }
43    }
44
45    /// Builds a verified diagnostics snapshot by actively checking PLC connectivity.
46    pub async fn get_diagnostics_snapshot_detailed(
47        &mut self,
48    ) -> crate::error::Result<crate::DiagnosticsSnapshot> {
49        let is_healthy = self.check_health_detailed().await?;
50        Ok(self
51            .build_diagnostics_snapshot(crate::HealthCheckMode::Verified, is_healthy)
52            .await)
53    }
54
55    async fn build_diagnostics_snapshot(
56        &self,
57        health_mode: crate::HealthCheckMode,
58        is_healthy: bool,
59    ) -> crate::DiagnosticsSnapshot {
60        let now = std::time::SystemTime::now();
61        let session_active = self.session_handle() != 0;
62        let last_activity_elapsed = self.last_activity.lock().await.elapsed();
63        let last_success_time = if is_healthy || session_active {
64            Some(
65                now.checked_sub(last_activity_elapsed)
66                    .unwrap_or(std::time::SystemTime::UNIX_EPOCH),
67            )
68        } else {
69            None
70        };
71
72        let error_category = if session_active && !is_healthy {
73            Some(crate::ErrorCategory::Session)
74        } else {
75            None
76        };
77
78        let operations = self.diagnostic_counters.operation_metrics();
79        let mut errors = self.diagnostic_counters.error_metrics();
80        if error_category == Some(crate::ErrorCategory::Session) {
81            errors.session_errors += 1;
82            errors.retriable_errors += 1;
83            if errors.last_error_time.is_none() {
84                errors.last_error_time = Some(now);
85                errors.last_error_message = Some(
86                    "Detailed health check reported session-level connectivity failure".to_string(),
87                );
88                errors.last_error_category = error_category;
89                errors.last_retriable_error_time = Some(now);
90            }
91        }
92
93        crate::DiagnosticsSnapshot {
94            captured_at: now,
95            connections: crate::ConnectionMetrics {
96                active_connections: if session_active { 1 } else { 0 },
97                total_connections: if session_active { 1 } else { 0 },
98                failed_connections: 0,
99                connection_uptime_avg: Duration::ZERO,
100                last_connection_time: last_success_time,
101            },
102            operations,
103            performance: crate::PerformanceMetrics {
104                avg_read_latency_ms: 0.0,
105                avg_write_latency_ms: 0.0,
106                max_read_latency_ms: 0.0,
107                max_write_latency_ms: 0.0,
108                reads_per_second: 0.0,
109                writes_per_second: 0.0,
110                memory_usage_mb: 0.0,
111                cpu_usage_percent: 0.0,
112            },
113            errors,
114            health: crate::HealthMetrics {
115                overall_health: if is_healthy {
116                    crate::HealthStatus::Healthy
117                } else if session_active {
118                    crate::HealthStatus::Critical
119                } else {
120                    crate::HealthStatus::Unknown
121                },
122                last_health_check: now,
123                health_mode,
124                last_verified_health_check: if health_mode == crate::HealthCheckMode::Verified {
125                    Some(now)
126                } else {
127                    None
128                },
129                consecutive_failures: if is_healthy { 0 } else { 1 },
130                recovery_attempts: 0,
131                system_uptime: Duration::ZERO,
132                last_success_time,
133                last_failure_time: if session_active && !is_healthy {
134                    Some(now)
135                } else {
136                    None
137                },
138            },
139            system_metrics_are_placeholders: true,
140        }
141    }
142}