use std::collections::{HashMap, VecDeque};
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
use crate::error::Error;
use crate::events::DisconnectReason;
use crate::platform::{Platform, PlatformConfig};
const MAX_RECENT_ERRORS: usize = 100;
const MAX_RECENT_OPERATIONS: usize = 50;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AdapterState {
Available,
PoweredOff,
NotFound,
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdapterInfo {
pub state: AdapterState,
pub name: Option<String>,
pub supports_ble: bool,
pub connected_device_count: Option<usize>,
}
impl Default for AdapterInfo {
fn default() -> Self {
Self {
state: AdapterState::Unknown,
name: None,
supports_ble: true,
connected_device_count: None,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ConnectionStats {
pub total_attempts: u64,
pub successful: u64,
pub failed: u64,
pub avg_connection_time_ms: Option<u64>,
pub min_connection_time_ms: Option<u64>,
pub max_connection_time_ms: Option<u64>,
pub disconnection_reasons: HashMap<String, u64>,
pub reconnect_attempts: u64,
pub reconnect_successes: u64,
}
impl ConnectionStats {
pub fn success_rate(&self) -> f64 {
if self.total_attempts == 0 {
0.0
} else {
(self.successful as f64 / self.total_attempts as f64) * 100.0
}
}
pub fn reconnect_success_rate(&self) -> f64 {
if self.reconnect_attempts == 0 {
0.0
} else {
(self.reconnect_successes as f64 / self.reconnect_attempts as f64) * 100.0
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct OperationStats {
pub total_reads: u64,
pub successful_reads: u64,
pub failed_reads: u64,
pub total_writes: u64,
pub successful_writes: u64,
pub failed_writes: u64,
pub avg_read_time_ms: Option<u64>,
pub avg_write_time_ms: Option<u64>,
pub timeout_count: u64,
}
impl OperationStats {
pub fn read_success_rate(&self) -> f64 {
if self.total_reads == 0 {
0.0
} else {
(self.successful_reads as f64 / self.total_reads as f64) * 100.0
}
}
pub fn write_success_rate(&self) -> f64 {
if self.total_writes == 0 {
0.0
} else {
(self.successful_writes as f64 / self.total_writes as f64) * 100.0
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecordedError {
pub timestamp_ms: u64,
pub message: String,
pub category: ErrorCategory,
pub device_id: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ErrorCategory {
Connection,
Operation,
Timeout,
DeviceNotFound,
DataParsing,
Configuration,
Other,
}
impl From<&Error> for ErrorCategory {
fn from(error: &Error) -> Self {
match error {
Error::ConnectionFailed { .. } | Error::NotConnected => ErrorCategory::Connection,
Error::Timeout { .. } => ErrorCategory::Timeout,
Error::DeviceNotFound(_) => ErrorCategory::DeviceNotFound,
Error::InvalidData(_)
| Error::InvalidHistoryData { .. }
| Error::InvalidReadingFormat { .. } => ErrorCategory::DataParsing,
Error::InvalidConfig(_) => ErrorCategory::Configuration,
Error::CharacteristicNotFound { .. } | Error::WriteFailed { .. } => {
ErrorCategory::Operation
}
Error::Unsupported(_) | Error::Bluetooth(_) | Error::Io(_) | Error::Cancelled => {
ErrorCategory::Other
}
}
}
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
struct RecordedOperation {
operation_type: OperationType,
start_time: Instant,
duration_ms: u64,
success: bool,
device_id: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
enum OperationType {
Connect,
Disconnect,
Read,
Write,
Scan,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BluetoothDiagnostics {
pub platform: String,
pub platform_config: PlatformConfigSnapshot,
pub adapter_info: AdapterInfo,
pub connection_stats: ConnectionStats,
pub operation_stats: OperationStats,
pub recent_errors: Vec<RecordedError>,
pub collected_at: u64,
pub uptime_secs: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlatformConfigSnapshot {
pub recommended_scan_duration_ms: u64,
pub recommended_connection_timeout_ms: u64,
pub max_concurrent_connections: usize,
pub exposes_mac_address: bool,
}
impl From<&PlatformConfig> for PlatformConfigSnapshot {
fn from(config: &PlatformConfig) -> Self {
Self {
recommended_scan_duration_ms: config.recommended_scan_duration.as_millis() as u64,
recommended_connection_timeout_ms: config.recommended_connection_timeout.as_millis()
as u64,
max_concurrent_connections: config.max_concurrent_connections,
exposes_mac_address: config.exposes_mac_address,
}
}
}
pub struct DiagnosticsCollector {
start_time: Instant,
connection_attempts: AtomicU64,
connection_successes: AtomicU64,
connection_failures: AtomicU64,
reconnect_attempts: AtomicU64,
reconnect_successes: AtomicU64,
read_attempts: AtomicU64,
read_successes: AtomicU64,
write_attempts: AtomicU64,
write_successes: AtomicU64,
timeout_count: AtomicU64,
connection_times: RwLock<Vec<u64>>,
read_times: RwLock<Vec<u64>>,
write_times: RwLock<Vec<u64>>,
disconnection_reasons: RwLock<HashMap<String, u64>>,
recent_errors: RwLock<VecDeque<RecordedError>>,
recent_operations: RwLock<VecDeque<RecordedOperation>>,
}
impl Default for DiagnosticsCollector {
fn default() -> Self {
Self::new()
}
}
impl DiagnosticsCollector {
pub fn new() -> Self {
Self {
start_time: Instant::now(),
connection_attempts: AtomicU64::new(0),
connection_successes: AtomicU64::new(0),
connection_failures: AtomicU64::new(0),
reconnect_attempts: AtomicU64::new(0),
reconnect_successes: AtomicU64::new(0),
read_attempts: AtomicU64::new(0),
read_successes: AtomicU64::new(0),
write_attempts: AtomicU64::new(0),
write_successes: AtomicU64::new(0),
timeout_count: AtomicU64::new(0),
connection_times: RwLock::new(Vec::new()),
read_times: RwLock::new(Vec::new()),
write_times: RwLock::new(Vec::new()),
disconnection_reasons: RwLock::new(HashMap::new()),
recent_errors: RwLock::new(VecDeque::with_capacity(MAX_RECENT_ERRORS)),
recent_operations: RwLock::new(VecDeque::with_capacity(MAX_RECENT_OPERATIONS)),
}
}
pub fn record_connection_attempt(&self) {
self.connection_attempts.fetch_add(1, Ordering::Relaxed);
}
pub async fn record_connection_success(&self, duration: Duration) {
self.connection_successes.fetch_add(1, Ordering::Relaxed);
self.connection_times
.write()
.await
.push(duration.as_millis() as u64);
}
pub fn record_connection_failure(&self) {
self.connection_failures.fetch_add(1, Ordering::Relaxed);
}
pub fn record_reconnect_attempt(&self) {
self.reconnect_attempts.fetch_add(1, Ordering::Relaxed);
}
pub fn record_reconnect_success(&self) {
self.reconnect_successes.fetch_add(1, Ordering::Relaxed);
}
pub async fn record_read(&self, success: bool, duration: Option<Duration>) {
self.read_attempts.fetch_add(1, Ordering::Relaxed);
if success {
self.read_successes.fetch_add(1, Ordering::Relaxed);
if let Some(d) = duration {
self.read_times.write().await.push(d.as_millis() as u64);
}
}
}
pub async fn record_write(&self, success: bool, duration: Option<Duration>) {
self.write_attempts.fetch_add(1, Ordering::Relaxed);
if success {
self.write_successes.fetch_add(1, Ordering::Relaxed);
if let Some(d) = duration {
self.write_times.write().await.push(d.as_millis() as u64);
}
}
}
pub fn record_timeout(&self) {
self.timeout_count.fetch_add(1, Ordering::Relaxed);
}
pub async fn record_disconnection(&self, reason: &DisconnectReason) {
let reason_str = format!("{:?}", reason);
let mut reasons = self.disconnection_reasons.write().await;
*reasons.entry(reason_str).or_insert(0) += 1;
}
pub async fn record_error(&self, error: &Error, device_id: Option<String>) {
let recorded = RecordedError {
timestamp_ms: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64,
message: error.to_string(),
category: ErrorCategory::from(error),
device_id,
};
if matches!(error, Error::Timeout { .. }) {
self.record_timeout();
}
let mut errors = self.recent_errors.write().await;
if errors.len() >= MAX_RECENT_ERRORS {
errors.pop_back();
}
errors.push_front(recorded);
}
pub async fn collect(&self) -> BluetoothDiagnostics {
let platform = Platform::current();
let platform_config = PlatformConfig::for_current_platform();
let connection_times = self.connection_times.read().await;
let (avg_conn, min_conn, max_conn) = calculate_time_stats(&connection_times);
let read_times = self.read_times.read().await;
let (avg_read, _, _) = calculate_time_stats(&read_times);
let write_times = self.write_times.read().await;
let (avg_write, _, _) = calculate_time_stats(&write_times);
let disconnection_reasons = self.disconnection_reasons.read().await.clone();
let recent_errors: Vec<RecordedError> =
self.recent_errors.read().await.iter().cloned().collect();
BluetoothDiagnostics {
platform: format!("{:?}", platform),
platform_config: PlatformConfigSnapshot::from(&platform_config),
adapter_info: AdapterInfo::default(), connection_stats: ConnectionStats {
total_attempts: self.connection_attempts.load(Ordering::Relaxed),
successful: self.connection_successes.load(Ordering::Relaxed),
failed: self.connection_failures.load(Ordering::Relaxed),
avg_connection_time_ms: avg_conn,
min_connection_time_ms: min_conn,
max_connection_time_ms: max_conn,
disconnection_reasons,
reconnect_attempts: self.reconnect_attempts.load(Ordering::Relaxed),
reconnect_successes: self.reconnect_successes.load(Ordering::Relaxed),
},
operation_stats: OperationStats {
total_reads: self.read_attempts.load(Ordering::Relaxed),
successful_reads: self.read_successes.load(Ordering::Relaxed),
failed_reads: self.read_attempts.load(Ordering::Relaxed)
- self.read_successes.load(Ordering::Relaxed),
total_writes: self.write_attempts.load(Ordering::Relaxed),
successful_writes: self.write_successes.load(Ordering::Relaxed),
failed_writes: self.write_attempts.load(Ordering::Relaxed)
- self.write_successes.load(Ordering::Relaxed),
avg_read_time_ms: avg_read,
avg_write_time_ms: avg_write,
timeout_count: self.timeout_count.load(Ordering::Relaxed),
},
recent_errors,
collected_at: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64,
uptime_secs: self.start_time.elapsed().as_secs(),
}
}
pub async fn reset(&self) {
self.connection_attempts.store(0, Ordering::Relaxed);
self.connection_successes.store(0, Ordering::Relaxed);
self.connection_failures.store(0, Ordering::Relaxed);
self.reconnect_attempts.store(0, Ordering::Relaxed);
self.reconnect_successes.store(0, Ordering::Relaxed);
self.read_attempts.store(0, Ordering::Relaxed);
self.read_successes.store(0, Ordering::Relaxed);
self.write_attempts.store(0, Ordering::Relaxed);
self.write_successes.store(0, Ordering::Relaxed);
self.timeout_count.store(0, Ordering::Relaxed);
self.connection_times.write().await.clear();
self.read_times.write().await.clear();
self.write_times.write().await.clear();
self.disconnection_reasons.write().await.clear();
self.recent_errors.write().await.clear();
self.recent_operations.write().await.clear();
}
pub async fn summary(&self) -> String {
let diag = self.collect().await;
format!(
"Connections: {}/{} ({:.1}% success), Reconnects: {}/{} ({:.1}% success), \
Reads: {}/{} ({:.1}% success), Writes: {}/{} ({:.1}% success), \
Timeouts: {}, Errors: {}",
diag.connection_stats.successful,
diag.connection_stats.total_attempts,
diag.connection_stats.success_rate(),
diag.connection_stats.reconnect_successes,
diag.connection_stats.reconnect_attempts,
diag.connection_stats.reconnect_success_rate(),
diag.operation_stats.successful_reads,
diag.operation_stats.total_reads,
diag.operation_stats.read_success_rate(),
diag.operation_stats.successful_writes,
diag.operation_stats.total_writes,
diag.operation_stats.write_success_rate(),
diag.operation_stats.timeout_count,
diag.recent_errors.len(),
)
}
}
fn calculate_time_stats(times: &[u64]) -> (Option<u64>, Option<u64>, Option<u64>) {
if times.is_empty() {
return (None, None, None);
}
let sum: u64 = times.iter().sum();
let len = times.len() as f64;
let avg = (sum as f64 / len).round() as u64;
let min = *times.iter().min().expect("checked non-empty above");
let max = *times.iter().max().expect("checked non-empty above");
(Some(avg), Some(min), Some(max))
}
pub static GLOBAL_DIAGNOSTICS: std::sync::LazyLock<Arc<DiagnosticsCollector>> =
std::sync::LazyLock::new(|| Arc::new(DiagnosticsCollector::new()));
pub fn global_diagnostics() -> &'static Arc<DiagnosticsCollector> {
&GLOBAL_DIAGNOSTICS
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_connection_stats_success_rate() {
let mut stats = ConnectionStats::default();
assert_eq!(stats.success_rate(), 0.0);
stats.total_attempts = 10;
stats.successful = 8;
assert!((stats.success_rate() - 80.0).abs() < 0.01);
}
#[test]
fn test_operation_stats_success_rate() {
let mut stats = OperationStats::default();
assert_eq!(stats.read_success_rate(), 0.0);
stats.total_reads = 100;
stats.successful_reads = 95;
assert!((stats.read_success_rate() - 95.0).abs() < 0.01);
}
#[test]
fn test_error_category_from_error() {
let timeout_err = Error::Timeout {
operation: "test".to_string(),
duration: Duration::from_secs(1),
};
assert_eq!(ErrorCategory::from(&timeout_err), ErrorCategory::Timeout);
let not_connected = Error::NotConnected;
assert_eq!(
ErrorCategory::from(¬_connected),
ErrorCategory::Connection
);
}
#[tokio::test]
async fn test_diagnostics_collector() {
let collector = DiagnosticsCollector::new();
collector.record_connection_attempt();
collector
.record_connection_success(Duration::from_millis(500))
.await;
let diag = collector.collect().await;
assert_eq!(diag.connection_stats.total_attempts, 1);
assert_eq!(diag.connection_stats.successful, 1);
assert_eq!(diag.connection_stats.avg_connection_time_ms, Some(500));
}
#[tokio::test]
async fn test_diagnostics_collector_reset() {
let collector = DiagnosticsCollector::new();
collector.record_connection_attempt();
collector.record_connection_failure();
let diag = collector.collect().await;
assert_eq!(diag.connection_stats.failed, 1);
collector.reset().await;
let diag = collector.collect().await;
assert_eq!(diag.connection_stats.failed, 0);
}
}