Skip to main content

hidpp/channel/
observation.rs

1//! Optional observation of reports and request lifecycles at the point where
2//! the channel owns correlation.
3
4use std::{
5    fmt,
6    panic::{AssertUnwindSafe, catch_unwind},
7};
8
9use tracing::trace;
10
11use super::MAX_RAW_REPORT_LENGTH;
12
13#[cfg(test)]
14mod tests;
15
16/// An owned raw HID report delivered to a [`ChannelObserver`].
17///
18/// Reports are bounded to the largest report accepted by the channel. The
19/// channel only constructs this copy when observation is enabled.
20#[derive(Clone, PartialEq, Eq)]
21pub struct ObservedReport {
22    bytes: [u8; MAX_RAW_REPORT_LENGTH],
23    len: usize,
24}
25
26impl ObservedReport {
27    pub(super) fn from_bytes(bytes: &[u8]) -> Self {
28        debug_assert!(bytes.len() <= MAX_RAW_REPORT_LENGTH);
29        let mut owned = [0; MAX_RAW_REPORT_LENGTH];
30        owned[..bytes.len()].copy_from_slice(bytes);
31        Self {
32            bytes: owned,
33            len: bytes.len(),
34        }
35    }
36
37    /// Returns the report bytes, including the report ID.
38    #[must_use]
39    pub fn as_bytes(&self) -> &[u8] {
40        &self.bytes[..self.len]
41    }
42
43    /// Returns the report length, including the report ID.
44    #[must_use]
45    pub fn len(&self) -> usize {
46        self.len
47    }
48
49    /// Returns whether the report is empty.
50    ///
51    /// Channel-generated observations are never empty; this method completes
52    /// the conventional collection API.
53    #[must_use]
54    pub fn is_empty(&self) -> bool {
55        self.len == 0
56    }
57}
58
59impl AsRef<[u8]> for ObservedReport {
60    fn as_ref(&self) -> &[u8] {
61        self.as_bytes()
62    }
63}
64
65impl fmt::Debug for ObservedReport {
66    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
67        formatter
68            .debug_tuple("ObservedReport")
69            .field(&self.as_bytes())
70            .finish()
71    }
72}
73
74/// The terminal outcome of a request that expected a response.
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76#[non_exhaustive]
77pub enum RequestOutcome {
78    /// A matching incoming report reached the request waiter.
79    Succeeded,
80    /// The request's total write-and-response budget elapsed.
81    TimedOut,
82    /// The raw transport rejected the outgoing report.
83    WriteFailed,
84    /// The response sender disappeared without delivering a report.
85    NoResponse,
86    /// The caller dropped the in-flight request future.
87    Cancelled,
88}
89
90/// A wire or request-lifecycle fact observed by a [`HidppChannel`](super::HidppChannel).
91///
92/// Request IDs are monotonically assigned within one channel lifetime. An
93/// outgoing report with `request_id: None` is a fire-and-forget HID++ write or
94/// a [`HidppChannel::write_raw_report`](super::HidppChannel::write_raw_report)
95/// write. A parsed incoming report has an ID only when the channel matched it
96/// to a pending request; `None` therefore identifies unsolicited or late
97/// input.
98#[derive(Debug, Clone, PartialEq, Eq)]
99#[non_exhaustive]
100pub enum ChannelObservation {
101    /// A post-normalization report submitted to the raw transport.
102    OutgoingReport {
103        /// The pending request this report belongs to, if it expects a response.
104        request_id: Option<u64>,
105        /// The exact bytes passed to the raw transport.
106        report: ObservedReport,
107    },
108    /// A valid HID++ report read from the raw transport.
109    IncomingReport {
110        /// The request whose predicate matched this report, if any.
111        request_id: Option<u64>,
112        /// The exact bytes returned by the raw transport.
113        report: ObservedReport,
114    },
115    /// Raw incoming bytes carrying a HID++ report ID that failed HID++ report
116    /// parsing.
117    MalformedIncomingReport {
118        /// The exact bytes returned by the raw transport.
119        report: ObservedReport,
120    },
121    /// An incoming report belonging to another protocol sharing this node —
122    /// Logitech DJ (`0x20`/`0x21`) on a Unifying receiver, for instance. It
123    /// says nothing about the HID++ exchange and the channel drops it.
124    ForeignIncomingReport {
125        /// The exact bytes returned by the raw transport.
126        report: ObservedReport,
127    },
128    /// The terminal outcome of a request that expected a response.
129    RequestOutcome {
130        /// The request whose lifecycle ended.
131        request_id: u64,
132        /// How the request ended.
133        outcome: RequestOutcome,
134    },
135}
136
137/// Receives optional channel observations.
138///
139/// The channel invokes observers synchronously and outside its internal locks,
140/// after correlation decisions have been committed. Implementations must
141/// return promptly and should hand events to their own worker if processing
142/// may block. Observer panics are caught so they cannot stop the reader thread
143/// or change channel shutdown.
144///
145/// Calls originate from request executors and the dedicated reader thread, so
146/// concurrent requests can invoke an observer concurrently. A successful
147/// request is woken before its matching incoming observation is delivered;
148/// consumers must associate events by request ID rather than callback order.
149pub trait ChannelObserver: Send + Sync + 'static {
150    /// Receives one owned observation.
151    fn observe(&self, observation: ChannelObservation);
152}
153
154impl<F> ChannelObserver for F
155where
156    F: Fn(ChannelObservation) + Send + Sync + 'static,
157{
158    fn observe(&self, observation: ChannelObservation) {
159        self(observation);
160    }
161}
162
163pub(super) fn emit(observer: &dyn ChannelObserver, observation: ChannelObservation) {
164    if catch_unwind(AssertUnwindSafe(|| observer.observe(observation))).is_err() {
165        trace!("channel observer panicked — observation ignored");
166    }
167}
168
169pub(super) fn emit_report(
170    observer: Option<&dyn ChannelObserver>,
171    bytes: &[u8],
172    observation: impl FnOnce(ObservedReport) -> ChannelObservation,
173) {
174    if let Some(observer) = observer {
175        emit(observer, observation(ObservedReport::from_bytes(bytes)));
176    }
177}
178
179pub(super) struct RequestObservation<'a> {
180    observer: Option<&'a dyn ChannelObserver>,
181    request_id: u64,
182    completed: bool,
183}
184
185impl<'a> RequestObservation<'a> {
186    pub(super) fn new(observer: Option<&'a dyn ChannelObserver>, request_id: u64) -> Self {
187        Self {
188            observer,
189            request_id,
190            completed: false,
191        }
192    }
193
194    pub(super) fn complete(&mut self, outcome: RequestOutcome) {
195        self.completed = true;
196        if let Some(observer) = self.observer {
197            emit(
198                observer,
199                ChannelObservation::RequestOutcome {
200                    request_id: self.request_id,
201                    outcome,
202                },
203            );
204        }
205    }
206}
207
208impl Drop for RequestObservation<'_> {
209    fn drop(&mut self) {
210        if !self.completed
211            && let Some(observer) = self.observer
212        {
213            emit(
214                observer,
215                ChannelObservation::RequestOutcome {
216                    request_id: self.request_id,
217                    outcome: RequestOutcome::Cancelled,
218                },
219            );
220        }
221    }
222}