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