1use serde::{Deserialize, Serialize};
2use std::sync::{Arc, Mutex};
3use std::time::{Duration, SystemTime, UNIX_EPOCH};
4
5pub trait CommsObserver: Send + Sync {
7 fn on_event(&self, event: &CommsEvent);
8}
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12#[non_exhaustive]
13pub enum CommsEventKind {
14 WriteOk,
15 WriteFailed,
16 ReadOk,
17 ReadFailed,
18 Timeout,
19 Reconnect,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24#[non_exhaustive]
25pub struct CommsEvent {
26 pub address: String,
27 pub kind: CommsEventKind,
28 pub command: Option<String>,
29 pub attempt: u32,
30 pub elapsed_ms: u64,
31 pub detail: Option<String>,
32}
33
34#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
36pub struct DeviceHealth {
37 pub consecutive_failures: u32,
38 pub total_operations: u64,
39 pub total_failures: u64,
40 pub last_error: Option<String>,
41 pub last_success_unix_ms: Option<u64>,
42 pub last_failure_unix_ms: Option<u64>,
43}
44
45impl DeviceHealth {
46 pub fn is_healthy(&self) -> bool {
47 self.consecutive_failures == 0
48 }
49}
50
51#[derive(Clone)]
53pub struct Diagnostics {
54 address: String,
55 health: Option<Arc<Mutex<DeviceHealth>>>,
56 observer: Option<Arc<dyn CommsObserver>>,
57}
58
59impl Diagnostics {
60 pub fn new(address: impl Into<String>) -> Self {
61 Self {
62 address: address.into(),
63 health: None,
64 observer: None,
65 }
66 }
67
68 pub fn with_health(mut self, health: Arc<Mutex<DeviceHealth>>) -> Self {
69 self.health = Some(health);
70 self
71 }
72
73 pub fn with_observer(mut self, observer: Arc<dyn CommsObserver>) -> Self {
74 self.observer = Some(observer);
75 self
76 }
77
78 pub fn address(&self) -> &str {
79 &self.address
80 }
81
82 pub fn record_success(
83 &self,
84 kind: CommsEventKind,
85 command: Option<&str>,
86 attempt: u32,
87 elapsed: Duration,
88 ) {
89 if let Some(health) = &self.health {
90 let mut h = health.lock().unwrap();
91 h.consecutive_failures = 0;
92 h.total_operations += 1;
93 h.last_success_unix_ms = Some(now_unix_ms());
94 }
95 self.emit(kind, command, attempt, elapsed, None);
96 }
97
98 pub fn record_failure(
99 &self,
100 kind: CommsEventKind,
101 command: Option<&str>,
102 attempt: u32,
103 elapsed: Duration,
104 detail: &str,
105 ) {
106 if let Some(health) = &self.health {
107 let mut h = health.lock().unwrap();
108 h.consecutive_failures += 1;
109 h.total_operations += 1;
110 h.total_failures += 1;
111 h.last_error = Some(detail.to_string());
112 h.last_failure_unix_ms = Some(now_unix_ms());
113 }
114 self.emit(kind, command, attempt, elapsed, Some(detail.to_string()));
115 }
116
117 fn emit(
118 &self,
119 kind: CommsEventKind,
120 command: Option<&str>,
121 attempt: u32,
122 elapsed: Duration,
123 detail: Option<String>,
124 ) {
125 let should_trace = tracing::enabled!(tracing::Level::DEBUG);
126 let should_push = self.observer.is_some();
127 if !should_trace && !should_push {
128 return;
129 }
130
131 let event = CommsEvent {
132 address: self.address.clone(),
133 kind,
134 command: command.map(str::to_string),
135 attempt,
136 elapsed_ms: elapsed.as_millis() as u64,
137 detail,
138 };
139
140 if should_trace {
141 match kind {
142 CommsEventKind::WriteFailed
143 | CommsEventKind::ReadFailed
144 | CommsEventKind::Timeout => tracing::warn!(
145 address = %event.address,
146 command = ?event.command,
147 attempt = event.attempt,
148 detail = ?event.detail,
149 "instrument comms failure"
150 ),
151 _ => tracing::debug!(
152 address = %event.address,
153 command = ?event.command,
154 kind = ?event.kind,
155 "instrument comms"
156 ),
157 }
158 }
159
160 if let Some(observer) = &self.observer {
161 observer.on_event(&event);
162 }
163 }
164}
165
166fn now_unix_ms() -> u64 {
167 SystemTime::now()
168 .duration_since(UNIX_EPOCH)
169 .map(|d| d.as_millis() as u64)
170 .unwrap_or(0)
171}