Skip to main content

aranet_core/
diagnostics.rs

1//! Bluetooth diagnostics and troubleshooting utilities.
2//!
3//! This module provides tools for diagnosing Bluetooth connectivity issues
4//! and gathering information about the BLE environment.
5//!
6//! # Example
7//!
8//! ```ignore
9//! use aranet_core::diagnostics::{BluetoothDiagnostics, DiagnosticsCollector};
10//!
11//! let collector = DiagnosticsCollector::new();
12//! let diagnostics = collector.collect().await?;
13//!
14//! println!("Platform: {:?}", diagnostics.platform);
15//! println!("Adapter: {:?}", diagnostics.adapter_info);
16//! println!("Connection stats: {:?}", diagnostics.connection_stats);
17//! ```
18
19use std::collections::{HashMap, VecDeque};
20use std::sync::Arc;
21use std::sync::atomic::{AtomicU64, Ordering};
22use std::time::{Duration, Instant};
23
24use serde::{Deserialize, Serialize};
25use tokio::sync::RwLock;
26
27use crate::error::Error;
28use crate::events::DisconnectReason;
29use crate::platform::{Platform, PlatformConfig};
30
31/// Maximum number of recent errors to keep in the diagnostics buffer.
32const MAX_RECENT_ERRORS: usize = 100;
33
34/// Maximum number of recent operations to track.
35const MAX_RECENT_OPERATIONS: usize = 50;
36
37/// Bluetooth adapter state.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
39pub enum AdapterState {
40    /// Adapter is available and powered on.
41    Available,
42    /// Adapter is available but powered off.
43    PoweredOff,
44    /// No adapter found.
45    NotFound,
46    /// Adapter state is unknown.
47    Unknown,
48}
49
50/// Information about the Bluetooth adapter.
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct AdapterInfo {
53    /// Adapter state.
54    pub state: AdapterState,
55    /// Adapter name/identifier if available.
56    pub name: Option<String>,
57    /// Whether the adapter supports BLE.
58    pub supports_ble: bool,
59    /// Number of currently connected devices (if known).
60    pub connected_device_count: Option<usize>,
61}
62
63impl Default for AdapterInfo {
64    fn default() -> Self {
65        Self {
66            state: AdapterState::Unknown,
67            name: None,
68            supports_ble: true,
69            connected_device_count: None,
70        }
71    }
72}
73
74/// Statistics about connection operations.
75#[derive(Debug, Clone, Default, Serialize, Deserialize)]
76pub struct ConnectionStats {
77    /// Total number of connection attempts.
78    pub total_attempts: u64,
79    /// Number of successful connections.
80    pub successful: u64,
81    /// Number of failed connections.
82    pub failed: u64,
83    /// Average connection time in milliseconds (for successful connections).
84    pub avg_connection_time_ms: Option<u64>,
85    /// Minimum connection time in milliseconds.
86    pub min_connection_time_ms: Option<u64>,
87    /// Maximum connection time in milliseconds.
88    pub max_connection_time_ms: Option<u64>,
89    /// Count of disconnection reasons.
90    pub disconnection_reasons: HashMap<String, u64>,
91    /// Number of reconnection attempts.
92    pub reconnect_attempts: u64,
93    /// Number of successful reconnections.
94    pub reconnect_successes: u64,
95}
96
97impl ConnectionStats {
98    /// Calculate the success rate as a percentage.
99    pub fn success_rate(&self) -> f64 {
100        if self.total_attempts == 0 {
101            0.0
102        } else {
103            (self.successful as f64 / self.total_attempts as f64) * 100.0
104        }
105    }
106
107    /// Calculate the reconnection success rate as a percentage.
108    pub fn reconnect_success_rate(&self) -> f64 {
109        if self.reconnect_attempts == 0 {
110            0.0
111        } else {
112            (self.reconnect_successes as f64 / self.reconnect_attempts as f64) * 100.0
113        }
114    }
115}
116
117/// Statistics about read/write operations.
118#[derive(Debug, Clone, Default, Serialize, Deserialize)]
119pub struct OperationStats {
120    /// Total read operations.
121    pub total_reads: u64,
122    /// Successful read operations.
123    pub successful_reads: u64,
124    /// Failed read operations.
125    pub failed_reads: u64,
126    /// Total write operations.
127    pub total_writes: u64,
128    /// Successful write operations.
129    pub successful_writes: u64,
130    /// Failed write operations.
131    pub failed_writes: u64,
132    /// Average read time in milliseconds.
133    pub avg_read_time_ms: Option<u64>,
134    /// Average write time in milliseconds.
135    pub avg_write_time_ms: Option<u64>,
136    /// Number of timeout errors.
137    pub timeout_count: u64,
138}
139
140impl OperationStats {
141    /// Calculate the read success rate as a percentage.
142    pub fn read_success_rate(&self) -> f64 {
143        if self.total_reads == 0 {
144            0.0
145        } else {
146            (self.successful_reads as f64 / self.total_reads as f64) * 100.0
147        }
148    }
149
150    /// Calculate the write success rate as a percentage.
151    pub fn write_success_rate(&self) -> f64 {
152        if self.total_writes == 0 {
153            0.0
154        } else {
155            (self.successful_writes as f64 / self.total_writes as f64) * 100.0
156        }
157    }
158}
159
160/// A recorded error with timestamp.
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct RecordedError {
163    /// When the error occurred (Unix timestamp millis).
164    pub timestamp_ms: u64,
165    /// Error message.
166    pub message: String,
167    /// Error category.
168    pub category: ErrorCategory,
169    /// Device identifier if applicable.
170    pub device_id: Option<String>,
171}
172
173/// Categories of errors for classification.
174#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
175pub enum ErrorCategory {
176    /// Connection-related errors.
177    Connection,
178    /// Read/write operation errors.
179    Operation,
180    /// Timeout errors.
181    Timeout,
182    /// Device not found errors.
183    DeviceNotFound,
184    /// Data parsing errors.
185    DataParsing,
186    /// Configuration errors.
187    Configuration,
188    /// Other/unknown errors.
189    Other,
190}
191
192impl From<&Error> for ErrorCategory {
193    fn from(error: &Error) -> Self {
194        match error {
195            Error::ConnectionFailed { .. } | Error::NotConnected => ErrorCategory::Connection,
196            Error::Timeout { .. } => ErrorCategory::Timeout,
197            Error::DeviceNotFound(_) => ErrorCategory::DeviceNotFound,
198            Error::InvalidData(_)
199            | Error::InvalidHistoryData { .. }
200            | Error::InvalidReadingFormat { .. } => ErrorCategory::DataParsing,
201            Error::InvalidConfig(_) => ErrorCategory::Configuration,
202            Error::CharacteristicNotFound { .. } | Error::WriteFailed { .. } => {
203                ErrorCategory::Operation
204            }
205            Error::Bluetooth(_) | Error::Io(_) | Error::Cancelled => ErrorCategory::Other,
206        }
207    }
208}
209
210/// A recorded operation for timing analysis.
211/// Reserved for future use in operation timing analysis.
212#[derive(Debug, Clone)]
213#[allow(dead_code)]
214struct RecordedOperation {
215    operation_type: OperationType,
216    start_time: Instant,
217    duration_ms: u64,
218    success: bool,
219    device_id: Option<String>,
220}
221
222/// Types of operations being tracked.
223/// Reserved for future use in operation timing analysis.
224#[derive(Debug, Clone, Copy, PartialEq, Eq)]
225#[allow(dead_code)]
226enum OperationType {
227    Connect,
228    Disconnect,
229    Read,
230    Write,
231    Scan,
232}
233
234/// Complete Bluetooth diagnostics snapshot.
235#[derive(Debug, Clone, Serialize, Deserialize)]
236pub struct BluetoothDiagnostics {
237    /// Current platform.
238    pub platform: String,
239    /// Platform-specific configuration.
240    pub platform_config: PlatformConfigSnapshot,
241    /// Adapter information.
242    pub adapter_info: AdapterInfo,
243    /// Connection statistics.
244    pub connection_stats: ConnectionStats,
245    /// Operation statistics.
246    pub operation_stats: OperationStats,
247    /// Recent errors (most recent first).
248    pub recent_errors: Vec<RecordedError>,
249    /// Timestamp when diagnostics were collected (Unix millis).
250    pub collected_at: u64,
251    /// Uptime of the diagnostics collector in seconds.
252    pub uptime_secs: u64,
253}
254
255/// Serializable snapshot of platform configuration.
256#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct PlatformConfigSnapshot {
258    pub recommended_scan_duration_ms: u64,
259    pub recommended_connection_timeout_ms: u64,
260    pub max_concurrent_connections: usize,
261    pub exposes_mac_address: bool,
262}
263
264impl From<&PlatformConfig> for PlatformConfigSnapshot {
265    fn from(config: &PlatformConfig) -> Self {
266        Self {
267            recommended_scan_duration_ms: config.recommended_scan_duration.as_millis() as u64,
268            recommended_connection_timeout_ms: config.recommended_connection_timeout.as_millis()
269                as u64,
270            max_concurrent_connections: config.max_concurrent_connections,
271            exposes_mac_address: config.exposes_mac_address,
272        }
273    }
274}
275
276/// Collector for Bluetooth diagnostics.
277///
278/// This struct accumulates statistics and errors over time, providing
279/// insights into Bluetooth connectivity patterns.
280pub struct DiagnosticsCollector {
281    /// When the collector was created.
282    start_time: Instant,
283    /// Connection statistics (atomic counters).
284    connection_attempts: AtomicU64,
285    connection_successes: AtomicU64,
286    connection_failures: AtomicU64,
287    reconnect_attempts: AtomicU64,
288    reconnect_successes: AtomicU64,
289    /// Operation statistics (atomic counters).
290    read_attempts: AtomicU64,
291    read_successes: AtomicU64,
292    write_attempts: AtomicU64,
293    write_successes: AtomicU64,
294    timeout_count: AtomicU64,
295    /// Connection times for averaging (protected by RwLock).
296    connection_times: RwLock<Vec<u64>>,
297    read_times: RwLock<Vec<u64>>,
298    write_times: RwLock<Vec<u64>>,
299    /// Disconnection reason counts.
300    disconnection_reasons: RwLock<HashMap<String, u64>>,
301    /// Recent errors buffer.
302    recent_errors: RwLock<VecDeque<RecordedError>>,
303    /// Recent operations for timing analysis.
304    recent_operations: RwLock<VecDeque<RecordedOperation>>,
305}
306
307impl Default for DiagnosticsCollector {
308    fn default() -> Self {
309        Self::new()
310    }
311}
312
313impl DiagnosticsCollector {
314    /// Create a new diagnostics collector.
315    pub fn new() -> Self {
316        Self {
317            start_time: Instant::now(),
318            connection_attempts: AtomicU64::new(0),
319            connection_successes: AtomicU64::new(0),
320            connection_failures: AtomicU64::new(0),
321            reconnect_attempts: AtomicU64::new(0),
322            reconnect_successes: AtomicU64::new(0),
323            read_attempts: AtomicU64::new(0),
324            read_successes: AtomicU64::new(0),
325            write_attempts: AtomicU64::new(0),
326            write_successes: AtomicU64::new(0),
327            timeout_count: AtomicU64::new(0),
328            connection_times: RwLock::new(Vec::new()),
329            read_times: RwLock::new(Vec::new()),
330            write_times: RwLock::new(Vec::new()),
331            disconnection_reasons: RwLock::new(HashMap::new()),
332            recent_errors: RwLock::new(VecDeque::with_capacity(MAX_RECENT_ERRORS)),
333            recent_operations: RwLock::new(VecDeque::with_capacity(MAX_RECENT_OPERATIONS)),
334        }
335    }
336
337    /// Record a connection attempt.
338    pub fn record_connection_attempt(&self) {
339        self.connection_attempts.fetch_add(1, Ordering::Relaxed);
340    }
341
342    /// Record a successful connection with duration.
343    pub async fn record_connection_success(&self, duration: Duration) {
344        self.connection_successes.fetch_add(1, Ordering::Relaxed);
345        self.connection_times
346            .write()
347            .await
348            .push(duration.as_millis() as u64);
349    }
350
351    /// Record a failed connection.
352    pub fn record_connection_failure(&self) {
353        self.connection_failures.fetch_add(1, Ordering::Relaxed);
354    }
355
356    /// Record a reconnection attempt.
357    pub fn record_reconnect_attempt(&self) {
358        self.reconnect_attempts.fetch_add(1, Ordering::Relaxed);
359    }
360
361    /// Record a successful reconnection.
362    pub fn record_reconnect_success(&self) {
363        self.reconnect_successes.fetch_add(1, Ordering::Relaxed);
364    }
365
366    /// Record a read operation.
367    pub async fn record_read(&self, success: bool, duration: Option<Duration>) {
368        self.read_attempts.fetch_add(1, Ordering::Relaxed);
369        if success {
370            self.read_successes.fetch_add(1, Ordering::Relaxed);
371            if let Some(d) = duration {
372                self.read_times.write().await.push(d.as_millis() as u64);
373            }
374        }
375    }
376
377    /// Record a write operation.
378    pub async fn record_write(&self, success: bool, duration: Option<Duration>) {
379        self.write_attempts.fetch_add(1, Ordering::Relaxed);
380        if success {
381            self.write_successes.fetch_add(1, Ordering::Relaxed);
382            if let Some(d) = duration {
383                self.write_times.write().await.push(d.as_millis() as u64);
384            }
385        }
386    }
387
388    /// Record a timeout.
389    pub fn record_timeout(&self) {
390        self.timeout_count.fetch_add(1, Ordering::Relaxed);
391    }
392
393    /// Record a disconnection with reason.
394    pub async fn record_disconnection(&self, reason: &DisconnectReason) {
395        let reason_str = format!("{:?}", reason);
396        let mut reasons = self.disconnection_reasons.write().await;
397        *reasons.entry(reason_str).or_insert(0) += 1;
398    }
399
400    /// Record an error.
401    pub async fn record_error(&self, error: &Error, device_id: Option<String>) {
402        let recorded = RecordedError {
403            timestamp_ms: std::time::SystemTime::now()
404                .duration_since(std::time::UNIX_EPOCH)
405                .unwrap_or_default()
406                .as_millis() as u64,
407            message: error.to_string(),
408            category: ErrorCategory::from(error),
409            device_id,
410        };
411
412        // Track timeout specifically
413        if matches!(error, Error::Timeout { .. }) {
414            self.record_timeout();
415        }
416
417        let mut errors = self.recent_errors.write().await;
418        if errors.len() >= MAX_RECENT_ERRORS {
419            errors.pop_back();
420        }
421        errors.push_front(recorded);
422    }
423
424    /// Collect current diagnostics snapshot.
425    pub async fn collect(&self) -> BluetoothDiagnostics {
426        let platform = Platform::current();
427        let platform_config = PlatformConfig::for_current_platform();
428
429        // Calculate connection time statistics
430        let connection_times = self.connection_times.read().await;
431        let (avg_conn, min_conn, max_conn) = calculate_time_stats(&connection_times);
432
433        // Calculate read/write time statistics
434        let read_times = self.read_times.read().await;
435        let (avg_read, _, _) = calculate_time_stats(&read_times);
436        let write_times = self.write_times.read().await;
437        let (avg_write, _, _) = calculate_time_stats(&write_times);
438
439        // Build disconnection reasons map
440        let disconnection_reasons = self.disconnection_reasons.read().await.clone();
441
442        // Collect recent errors
443        let recent_errors: Vec<RecordedError> =
444            self.recent_errors.read().await.iter().cloned().collect();
445
446        BluetoothDiagnostics {
447            platform: format!("{:?}", platform),
448            platform_config: PlatformConfigSnapshot::from(&platform_config),
449            adapter_info: AdapterInfo::default(), // Would need async adapter query
450            connection_stats: ConnectionStats {
451                total_attempts: self.connection_attempts.load(Ordering::Relaxed),
452                successful: self.connection_successes.load(Ordering::Relaxed),
453                failed: self.connection_failures.load(Ordering::Relaxed),
454                avg_connection_time_ms: avg_conn,
455                min_connection_time_ms: min_conn,
456                max_connection_time_ms: max_conn,
457                disconnection_reasons,
458                reconnect_attempts: self.reconnect_attempts.load(Ordering::Relaxed),
459                reconnect_successes: self.reconnect_successes.load(Ordering::Relaxed),
460            },
461            operation_stats: OperationStats {
462                total_reads: self.read_attempts.load(Ordering::Relaxed),
463                successful_reads: self.read_successes.load(Ordering::Relaxed),
464                failed_reads: self.read_attempts.load(Ordering::Relaxed)
465                    - self.read_successes.load(Ordering::Relaxed),
466                total_writes: self.write_attempts.load(Ordering::Relaxed),
467                successful_writes: self.write_successes.load(Ordering::Relaxed),
468                failed_writes: self.write_attempts.load(Ordering::Relaxed)
469                    - self.write_successes.load(Ordering::Relaxed),
470                avg_read_time_ms: avg_read,
471                avg_write_time_ms: avg_write,
472                timeout_count: self.timeout_count.load(Ordering::Relaxed),
473            },
474            recent_errors,
475            collected_at: std::time::SystemTime::now()
476                .duration_since(std::time::UNIX_EPOCH)
477                .unwrap_or_default()
478                .as_millis() as u64,
479            uptime_secs: self.start_time.elapsed().as_secs(),
480        }
481    }
482
483    /// Reset all statistics.
484    pub async fn reset(&self) {
485        self.connection_attempts.store(0, Ordering::Relaxed);
486        self.connection_successes.store(0, Ordering::Relaxed);
487        self.connection_failures.store(0, Ordering::Relaxed);
488        self.reconnect_attempts.store(0, Ordering::Relaxed);
489        self.reconnect_successes.store(0, Ordering::Relaxed);
490        self.read_attempts.store(0, Ordering::Relaxed);
491        self.read_successes.store(0, Ordering::Relaxed);
492        self.write_attempts.store(0, Ordering::Relaxed);
493        self.write_successes.store(0, Ordering::Relaxed);
494        self.timeout_count.store(0, Ordering::Relaxed);
495
496        self.connection_times.write().await.clear();
497        self.read_times.write().await.clear();
498        self.write_times.write().await.clear();
499        self.disconnection_reasons.write().await.clear();
500        self.recent_errors.write().await.clear();
501        self.recent_operations.write().await.clear();
502    }
503
504    /// Get a summary string suitable for logging.
505    pub async fn summary(&self) -> String {
506        let diag = self.collect().await;
507        format!(
508            "Connections: {}/{} ({:.1}% success), Reconnects: {}/{} ({:.1}% success), \
509             Reads: {}/{} ({:.1}% success), Writes: {}/{} ({:.1}% success), \
510             Timeouts: {}, Errors: {}",
511            diag.connection_stats.successful,
512            diag.connection_stats.total_attempts,
513            diag.connection_stats.success_rate(),
514            diag.connection_stats.reconnect_successes,
515            diag.connection_stats.reconnect_attempts,
516            diag.connection_stats.reconnect_success_rate(),
517            diag.operation_stats.successful_reads,
518            diag.operation_stats.total_reads,
519            diag.operation_stats.read_success_rate(),
520            diag.operation_stats.successful_writes,
521            diag.operation_stats.total_writes,
522            diag.operation_stats.write_success_rate(),
523            diag.operation_stats.timeout_count,
524            diag.recent_errors.len(),
525        )
526    }
527}
528
529/// Calculate min, max, and average from a slice of times.
530fn calculate_time_stats(times: &[u64]) -> (Option<u64>, Option<u64>, Option<u64>) {
531    if times.is_empty() {
532        return (None, None, None);
533    }
534
535    let sum: u64 = times.iter().sum();
536    let avg = sum / times.len() as u64;
537    let min = *times.iter().min().unwrap();
538    let max = *times.iter().max().unwrap();
539
540    (Some(avg), Some(min), Some(max))
541}
542
543/// Global diagnostics collector instance.
544///
545/// This can be used to collect diagnostics across the entire application.
546pub static GLOBAL_DIAGNOSTICS: std::sync::LazyLock<Arc<DiagnosticsCollector>> =
547    std::sync::LazyLock::new(|| Arc::new(DiagnosticsCollector::new()));
548
549/// Get a reference to the global diagnostics collector.
550pub fn global_diagnostics() -> &'static Arc<DiagnosticsCollector> {
551    &GLOBAL_DIAGNOSTICS
552}
553
554#[cfg(test)]
555mod tests {
556    use super::*;
557
558    #[test]
559    fn test_connection_stats_success_rate() {
560        let mut stats = ConnectionStats::default();
561        assert_eq!(stats.success_rate(), 0.0);
562
563        stats.total_attempts = 10;
564        stats.successful = 8;
565        assert!((stats.success_rate() - 80.0).abs() < 0.01);
566    }
567
568    #[test]
569    fn test_operation_stats_success_rate() {
570        let mut stats = OperationStats::default();
571        assert_eq!(stats.read_success_rate(), 0.0);
572
573        stats.total_reads = 100;
574        stats.successful_reads = 95;
575        assert!((stats.read_success_rate() - 95.0).abs() < 0.01);
576    }
577
578    #[test]
579    fn test_error_category_from_error() {
580        let timeout_err = Error::Timeout {
581            operation: "test".to_string(),
582            duration: Duration::from_secs(1),
583        };
584        assert_eq!(ErrorCategory::from(&timeout_err), ErrorCategory::Timeout);
585
586        let not_connected = Error::NotConnected;
587        assert_eq!(
588            ErrorCategory::from(&not_connected),
589            ErrorCategory::Connection
590        );
591    }
592
593    #[tokio::test]
594    async fn test_diagnostics_collector() {
595        let collector = DiagnosticsCollector::new();
596
597        collector.record_connection_attempt();
598        collector
599            .record_connection_success(Duration::from_millis(500))
600            .await;
601
602        let diag = collector.collect().await;
603        assert_eq!(diag.connection_stats.total_attempts, 1);
604        assert_eq!(diag.connection_stats.successful, 1);
605        assert_eq!(diag.connection_stats.avg_connection_time_ms, Some(500));
606    }
607
608    #[tokio::test]
609    async fn test_diagnostics_collector_reset() {
610        let collector = DiagnosticsCollector::new();
611
612        collector.record_connection_attempt();
613        collector.record_connection_failure();
614
615        let diag = collector.collect().await;
616        assert_eq!(diag.connection_stats.failed, 1);
617
618        collector.reset().await;
619
620        let diag = collector.collect().await;
621        assert_eq!(diag.connection_stats.failed, 0);
622    }
623}