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}