Skip to main content

hidpp/
feature.rs

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