Skip to main content

instrument_core/
diagnostics.rs

1use serde::{Deserialize, Serialize};
2use std::sync::{Arc, Mutex};
3use std::time::{Duration, SystemTime, UNIX_EPOCH};
4
5/// Receives push notifications for instrument communication events.
6pub trait CommsObserver: Send + Sync {
7    fn on_event(&self, event: &CommsEvent);
8}
9
10/// Kind of communication event.
11#[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/// A single communication event for observer push and tracing.
23#[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/// Pollable health snapshot for a device address.
35#[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/// Shared diagnostics context injected into SCPI sessions.
52#[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}