1use 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
31const MAX_RECENT_ERRORS: usize = 100;
33
34const MAX_RECENT_OPERATIONS: usize = 50;
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
39pub enum AdapterState {
40 Available,
42 PoweredOff,
44 NotFound,
46 Unknown,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct AdapterInfo {
53 pub state: AdapterState,
55 pub name: Option<String>,
57 pub supports_ble: bool,
59 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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
76pub struct ConnectionStats {
77 pub total_attempts: u64,
79 pub successful: u64,
81 pub failed: u64,
83 pub avg_connection_time_ms: Option<u64>,
85 pub min_connection_time_ms: Option<u64>,
87 pub max_connection_time_ms: Option<u64>,
89 pub disconnection_reasons: HashMap<String, u64>,
91 pub reconnect_attempts: u64,
93 pub reconnect_successes: u64,
95}
96
97impl ConnectionStats {
98 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 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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
119pub struct OperationStats {
120 pub total_reads: u64,
122 pub successful_reads: u64,
124 pub failed_reads: u64,
126 pub total_writes: u64,
128 pub successful_writes: u64,
130 pub failed_writes: u64,
132 pub avg_read_time_ms: Option<u64>,
134 pub avg_write_time_ms: Option<u64>,
136 pub timeout_count: u64,
138}
139
140impl OperationStats {
141 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct RecordedError {
163 pub timestamp_ms: u64,
165 pub message: String,
167 pub category: ErrorCategory,
169 pub device_id: Option<String>,
171}
172
173#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
175pub enum ErrorCategory {
176 Connection,
178 Operation,
180 Timeout,
182 DeviceNotFound,
184 DataParsing,
186 Configuration,
188 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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
236pub struct BluetoothDiagnostics {
237 pub platform: String,
239 pub platform_config: PlatformConfigSnapshot,
241 pub adapter_info: AdapterInfo,
243 pub connection_stats: ConnectionStats,
245 pub operation_stats: OperationStats,
247 pub recent_errors: Vec<RecordedError>,
249 pub collected_at: u64,
251 pub uptime_secs: u64,
253}
254
255#[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
276pub struct DiagnosticsCollector {
281 start_time: Instant,
283 connection_attempts: AtomicU64,
285 connection_successes: AtomicU64,
286 connection_failures: AtomicU64,
287 reconnect_attempts: AtomicU64,
288 reconnect_successes: AtomicU64,
289 read_attempts: AtomicU64,
291 read_successes: AtomicU64,
292 write_attempts: AtomicU64,
293 write_successes: AtomicU64,
294 timeout_count: AtomicU64,
295 connection_times: RwLock<Vec<u64>>,
297 read_times: RwLock<Vec<u64>>,
298 write_times: RwLock<Vec<u64>>,
299 disconnection_reasons: RwLock<HashMap<String, u64>>,
301 recent_errors: RwLock<VecDeque<RecordedError>>,
303 recent_operations: RwLock<VecDeque<RecordedOperation>>,
305}
306
307impl Default for DiagnosticsCollector {
308 fn default() -> Self {
309 Self::new()
310 }
311}
312
313impl DiagnosticsCollector {
314 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 pub fn record_connection_attempt(&self) {
339 self.connection_attempts.fetch_add(1, Ordering::Relaxed);
340 }
341
342 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 pub fn record_connection_failure(&self) {
353 self.connection_failures.fetch_add(1, Ordering::Relaxed);
354 }
355
356 pub fn record_reconnect_attempt(&self) {
358 self.reconnect_attempts.fetch_add(1, Ordering::Relaxed);
359 }
360
361 pub fn record_reconnect_success(&self) {
363 self.reconnect_successes.fetch_add(1, Ordering::Relaxed);
364 }
365
366 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 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 pub fn record_timeout(&self) {
390 self.timeout_count.fetch_add(1, Ordering::Relaxed);
391 }
392
393 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 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 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 pub async fn collect(&self) -> BluetoothDiagnostics {
426 let platform = Platform::current();
427 let platform_config = PlatformConfig::for_current_platform();
428
429 let connection_times = self.connection_times.read().await;
431 let (avg_conn, min_conn, max_conn) = calculate_time_stats(&connection_times);
432
433 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 let disconnection_reasons = self.disconnection_reasons.read().await.clone();
441
442 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(), 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 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 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
529fn 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
543pub static GLOBAL_DIAGNOSTICS: std::sync::LazyLock<Arc<DiagnosticsCollector>> =
547 std::sync::LazyLock::new(|| Arc::new(DiagnosticsCollector::new()));
548
549pub 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(¬_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}