hidpp/feature.rs
1//! Specific device feature implementations.
2
3use std::{any::Any, sync::Arc};
4
5use crate::{
6 channel::{HidppChannel, HidppMessage, LONG_REPORT_LENGTH, MessageListenerGuard},
7 emitter::EventEmitter,
8 nibble::U4,
9 protocol::v20::{self, Hidpp20Error},
10};
11
12pub mod adjustable_dpi;
13pub mod backlight;
14pub mod battery_status;
15pub mod battery_voltage;
16pub mod brightness_control;
17pub mod change_host;
18pub mod color_led_effects;
19pub mod crown;
20pub mod device_friendly_name;
21pub mod device_information;
22pub mod device_type_and_name;
23pub mod disable_keys;
24pub mod disable_keys_by_usage;
25pub mod dual_platform;
26pub mod equalizer;
27pub mod extended_dpi;
28pub mod extended_report_rate;
29pub mod feature_set;
30pub mod fn_inversion;
31pub mod gestures2;
32pub mod haptic_feedback;
33pub mod hires_wheel;
34pub mod hosts_info;
35pub mod illumination;
36pub mod mode_status;
37pub mod mouse_pointer;
38pub mod multi_platform;
39pub mod per_key_lighting;
40pub mod persistent_remappable_action;
41pub mod registry;
42pub mod report_rate;
43pub mod reprog_controls;
44pub mod rgb_effects;
45pub mod root;
46pub mod sidetone;
47pub mod smartshift;
48pub mod smartshift_enhanced;
49pub mod solar_dashboard;
50pub mod thumbwheel;
51pub mod touch_mouse_raw;
52pub mod touchpad_raw_xy;
53pub mod unified_battery;
54pub mod vertical_scrolling;
55pub mod wireless_device_status;
56
57/// Represents a concrete implementation of a HID++2.0 device feature.
58pub trait Feature: Any + Send + Sync {}
59
60/// Represents a [`Feature`] that can be instantiated automatically.
61pub trait CreatableFeature: Feature {
62 /// The protocol ID of the implemented feature.
63 const ID: u16;
64
65 /// The version of the feature the implementation starts to support.
66 const STARTING_VERSION: u8;
67
68 /// Creates a new instance of the feature implementation.
69 fn new(chan: Arc<HidppChannel>, device_index: u8, feature_index: u8) -> Self;
70}
71
72/// Represents a [`Feature`] that emits events of type `T`.
73pub trait EmittingFeature<T>: Feature {
74 /// Creates a receiver that is being notified whenever a new event of type
75 /// `T` is emitted by the feature.
76 fn listen(&self) -> async_channel::Receiver<T>;
77}
78
79/// Decodes a feature's event payload from its HID++2.0 event sub-id.
80///
81/// Implemented by a feature's event enum. [`EventSource::attach`] calls
82/// [`Self::decode`] for every unsolicited broadcast addressed to the feature,
83/// so this is the only part of a feature's event handling that has to vary —
84/// [`EventSource`] owns the listener registration, the [`event_payload`]
85/// filtering, and the emitting. Returning `None` drops the report exactly
86/// like the hand-rolled per-feature listeners this replaced did on an
87/// unrecognised sub-id or an unparsable payload.
88pub(crate) trait DecodeEvent: Clone + Send + Sync + 'static {
89 /// Decodes `payload` for event sub-id `sub_id`, or returns `None` to drop
90 /// the report.
91 fn decode(sub_id: u8, payload: &[u8; LONG_REPORT_LENGTH - 4]) -> Option<Self>
92 where
93 Self: Sized;
94}
95
96/// Owns the [`EventEmitter`] and message listener backing an
97/// [`EmittingFeature`] implementation.
98///
99/// Every emitting feature used to hand-roll the same listener ceremony in its
100/// `CreatableFeature::new`: build an `Arc<EventEmitter<E>>`, register a
101/// guarded message listener, filter the raw report through [`event_payload`],
102/// and emit whatever the feature's own decode step produced. A feature now
103/// holds one `EventSource<E>` field, builds it with [`Self::attach`], and
104/// implements [`EmittingFeature`] with a one-line [`Self::listen`] delegate.
105pub(crate) struct EventSource<E: DecodeEvent> {
106 emitter: Arc<EventEmitter<E>>,
107
108 /// Removes the message listener when the source is dropped.
109 _listener: MessageListenerGuard,
110}
111
112impl<E: DecodeEvent> EventSource<E> {
113 /// Registers a listener on `chan` that decodes broadcasts addressed to
114 /// `(device_index, feature_index)` via [`DecodeEvent::decode`] and emits
115 /// them to receivers created by [`Self::listen`].
116 pub(crate) fn attach(chan: &Arc<HidppChannel>, device_index: u8, feature_index: u8) -> Self {
117 let emitter = Arc::new(EventEmitter::new());
118
119 let listener = chan.add_msg_listener_guarded({
120 let emitter = Arc::clone(&emitter);
121
122 move |raw, matched| {
123 let Some((func, payload)) =
124 event_payload(raw, matched, device_index, feature_index)
125 else {
126 return;
127 };
128 if let Some(event) = E::decode(func.to_lo(), &payload) {
129 emitter.emit(event);
130 }
131 }
132 });
133
134 Self {
135 emitter,
136 _listener: listener,
137 }
138 }
139
140 /// Creates a receiver notified of every event this source decodes.
141 pub(crate) fn listen(&self) -> async_channel::Receiver<E> {
142 self.emitter.create_receiver()
143 }
144}
145
146/// A feature's addressable `(device, feature)` endpoint on a channel.
147///
148/// Embedding this in a feature replaces the three loose `chan` / `device_index`
149/// / `feature_index` fields every implementation used to carry, and centralises
150/// the HID++2.0 request framing that was otherwise hand-written at every call
151/// site.
152#[derive(Clone)]
153pub(crate) struct FeatureEndpoint {
154 /// The underlying HID++ channel.
155 chan: Arc<HidppChannel>,
156
157 /// The index of the device the feature belongs to.
158 device_index: u8,
159
160 /// The index of the feature in the device's feature table.
161 feature_index: u8,
162}
163
164impl FeatureEndpoint {
165 /// Binds an endpoint to `feature_index` on `device_index` of `chan`.
166 pub(crate) fn new(chan: Arc<HidppChannel>, device_index: u8, feature_index: u8) -> Self {
167 Self {
168 chan,
169 device_index,
170 feature_index,
171 }
172 }
173
174 /// The request header addressing `function` on this endpoint, stamped with
175 /// the channel's next software id.
176 ///
177 /// `function` is a HID++2.0 function id, which is 4-bit; only the low nibble
178 /// is sent. The assert keeps a stray out-of-range id from silently routing
179 /// to a different function in debug builds.
180 fn header(&self, function: u8) -> v20::MessageHeader {
181 debug_assert!(
182 function < 16,
183 "HID++2.0 function id {function} exceeds 4 bits"
184 );
185 v20::MessageHeader {
186 device_index: self.device_index,
187 feature_index: self.feature_index,
188 function_id: U4::from_lo(function),
189 software_id: self.chan.get_sw_id(),
190 }
191 }
192
193 /// Calls `function` with a 3-byte short-report payload and waits for the
194 /// matching response.
195 pub(crate) async fn call(
196 &self,
197 function: u8,
198 args: [u8; 3],
199 ) -> Result<v20::Message, Hidpp20Error> {
200 self.chan
201 .send_v20(v20::Message::Short(self.header(function), args))
202 .await
203 }
204
205 /// Calls `function` with a 16-byte long-report payload and waits for the
206 /// matching response.
207 pub(crate) async fn call_long(
208 &self,
209 function: u8,
210 args: [u8; 16],
211 ) -> Result<v20::Message, Hidpp20Error> {
212 self.chan
213 .send_v20(v20::Message::Long(self.header(function), args))
214 .await
215 }
216
217 /// Calls a function that echoes all three request bytes, waiting through
218 /// the native write before applying the response deadline. Differing
219 /// payloads (e.g. a late ownership claim during rollback) do not match.
220 ///
221 /// The owner must drive this future to completion; see
222 /// [`HidppChannel::send_v20_write_through`].
223 pub(crate) async fn call_echoed_write_through(
224 &self,
225 function: u8,
226 args: [u8; 3],
227 ) -> Result<v20::Message, Hidpp20Error> {
228 self.chan
229 .send_v20_write_through(
230 v20::Message::Short(self.header(function), args),
231 move |response| response.extend_payload()[..3] == args,
232 )
233 .await
234 }
235
236 /// Calls `function` with a 16-byte long-report payload, waiting through the
237 /// native write before applying the response deadline.
238 ///
239 /// The owner must drive this future to completion; see
240 /// [`HidppChannel::send_v20_write_through`].
241 pub(crate) async fn call_long_write_through(
242 &self,
243 function: u8,
244 args: [u8; 16],
245 ) -> Result<v20::Message, Hidpp20Error> {
246 self.chan
247 .send_v20_write_through(v20::Message::Long(self.header(function), args), |_| true)
248 .await
249 }
250
251 /// Sends `function` with a 3-byte short-report payload without waiting for a
252 /// response.
253 ///
254 /// For functions the device answers normally use [`Self::call`]; this is for
255 /// the rare function whose side effect (e.g. a host switch that resets the
256 /// device) prevents a response from ever arriving.
257 pub(crate) async fn notify(&self, function: u8, args: [u8; 3]) -> Result<(), Hidpp20Error> {
258 self.chan
259 .send_and_forget(v20::Message::Short(self.header(function), args).into())
260 .await?;
261 Ok(())
262 }
263}
264
265/// Shared prelude for a feature's event listener.
266///
267/// Drops reports already matched to an outgoing request, parses the raw report
268/// as a HID++2.0 message, and keeps only unsolicited broadcasts addressed to
269/// this `(device_index, feature_index)` with a zero software id. Returns the
270/// event's function id (its sub-id) and extended payload, leaving sub-id
271/// dispatch to the caller — so a multi-event feature filters its sub-ids
272/// explicitly rather than folding the check into the header guard.
273pub(crate) fn event_payload(
274 raw: HidppMessage,
275 matched: bool,
276 device_index: u8,
277 feature_index: u8,
278) -> Option<(U4, [u8; LONG_REPORT_LENGTH - 4])> {
279 if matched {
280 return None;
281 }
282
283 let msg = v20::Message::from(raw);
284 let header = msg.header();
285 if header.device_index != device_index
286 || header.feature_index != feature_index
287 || header.software_id.to_lo() != 0
288 {
289 return None;
290 }
291
292 Some((header.function_id, msg.extend_payload()))
293}
294
295bitflags::bitflags! {
296 /// A bitfield describing some properties of a feature.
297 ///
298 /// Documentation is taken from <https://drive.google.com/file/d/1ULmw9uJL8b8iwwUo5xjSS9F5Zvno-86y/view>.
299 #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
300 #[cfg_attr(feature = "serde", derive(serde::Serialize))]
301 pub struct FeatureType: u8 {
302 /// An obsolete feature is a feature that has been replaced by a newer
303 /// one, but is advertised in order for older SWs to still be able to
304 /// support the feature (in case the old SW does not know yet the newer
305 /// one).
306 const OBSOLETE = 1 << 7;
307
308 /// A SW hidden feature is a feature that should not be
309 /// known/managed/used by end user configuration SW. The host should
310 /// ignore this type of features.
311 const HIDDEN = 1 << 6;
312
313 /// A hidden feature that has been disabled for user software. Used for
314 /// internal testing and manufacturing.
315 const ENGINEERING = 1 << 5;
316
317 /// A manufacturing feature that can be permanently deactivated. It is
318 /// usually also hidden and engineering.
319 ///
320 /// This flag was added in feature version 2 and is never set by older
321 /// versions.
322 const MANUFACTURING_DEACTIVATABLE = 1 << 4;
323
324 /// A compliance feature that can be permanently deactivated. It is
325 /// usually also hidden and engineering.
326 ///
327 /// This flag was added in feature version 2 and is never set by older
328 /// versions.
329 const COMPLIANCE_DEACTIVATABLE = 1 << 3;
330 }
331}
332
333#[cfg(test)]
334mod tests {
335 use super::event_payload;
336 use crate::{
337 channel::HidppMessage,
338 nibble::U4,
339 protocol::v20::{Message, MessageHeader},
340 };
341
342 /// Builds a raw long report carrying a HID++2.0 broadcast with the given
343 /// header fields and a recognisable payload.
344 fn broadcast(device_index: u8, feature_index: u8, function: u8, software: u8) -> HidppMessage {
345 Message::Long(
346 MessageHeader {
347 device_index,
348 feature_index,
349 function_id: U4::from_lo(function),
350 software_id: U4::from_lo(software),
351 },
352 [0xab; 16],
353 )
354 .into()
355 }
356
357 #[test]
358 fn accepts_matching_broadcast_and_returns_sub_id() {
359 let (func, payload) =
360 event_payload(broadcast(2, 5, 1, 0), false, 2, 5).expect("broadcast should pass");
361 assert_eq!(func.to_lo(), 1);
362 assert_eq!(payload, [0xab; 16]);
363 }
364
365 #[test]
366 fn rejects_request_matched_report() {
367 // A report already matched to an outgoing request is a response, not an
368 // event.
369 assert!(event_payload(broadcast(2, 5, 0, 0), true, 2, 5).is_none());
370 }
371
372 #[test]
373 fn rejects_other_device_or_feature() {
374 assert!(event_payload(broadcast(9, 5, 0, 0), false, 2, 5).is_none());
375 assert!(event_payload(broadcast(2, 9, 0, 0), false, 2, 5).is_none());
376 }
377
378 #[test]
379 fn gates_on_software_id_only_not_sub_id() {
380 // Only the software id gates a broadcast: a nonzero one is rejected, but
381 // a nonzero function id is a valid event sub-id the caller dispatches on
382 // and must still pass. This is the invariant the old per-feature
383 // `nibble::combine(software_id, function_id) != 0` guard got right only
384 // by accident (those features happened to emit a single sub-id 0 event).
385 assert!(event_payload(broadcast(2, 5, 0, 1), false, 2, 5).is_none());
386 assert!(event_payload(broadcast(2, 5, 7, 0), false, 2, 5).is_some());
387 }
388}