Skip to main content

hidpp/feature/
mod.rs

1//! Specific device feature implementations.
2
3use std::{any::Any, sync::Arc};
4
5use crate::{
6    channel::{HidppChannel, HidppMessage, LONG_REPORT_LENGTH},
7    nibble::U4,
8    protocol::v20::{self, Hidpp20Error},
9};
10
11pub mod adjustable_dpi;
12pub mod backlight;
13pub mod battery_status;
14pub mod brightness_control;
15pub mod change_host;
16pub mod color_led_effects;
17pub mod crown;
18pub mod device_friendly_name;
19pub mod device_information;
20pub mod device_type_and_name;
21pub mod disable_keys;
22pub mod disable_keys_by_usage;
23pub mod dual_platform;
24pub mod equalizer;
25pub mod extended_dpi;
26pub mod extended_report_rate;
27pub mod feature_set;
28pub mod fn_inversion;
29pub mod gestures2;
30pub mod hires_wheel;
31pub mod hosts_info;
32pub mod illumination;
33pub mod mode_status;
34pub mod mouse_pointer;
35pub mod multi_platform;
36pub mod per_key_lighting;
37pub mod persistent_remappable_action;
38pub mod registry;
39pub mod report_rate;
40pub mod reprog_controls;
41pub mod rgb_effects;
42pub mod root;
43pub mod sidetone;
44pub mod smartshift;
45pub mod smartshift_enhanced;
46pub mod solar_dashboard;
47pub mod thumbwheel;
48pub mod touch_mouse_raw;
49pub mod touchpad_raw_xy;
50pub mod unified_battery;
51pub mod vertical_scrolling;
52pub mod wireless_device_status;
53
54/// Represents a concrete implementation of a HID++2.0 device feature.
55pub trait Feature: Any + Send + Sync {}
56
57/// Represents a [`Feature`] that can be instantiated automatically.
58pub trait CreatableFeature: Feature {
59    /// The protocol ID of the implemented feature.
60    const ID: u16;
61
62    /// The version of the feature the implementation starts to support.
63    const STARTING_VERSION: u8;
64
65    /// Creates a new instance of the feature implementation.
66    fn new(chan: Arc<HidppChannel>, device_index: u8, feature_index: u8) -> Self;
67}
68
69/// Represents a [`Feature`] that emits events of type `T`.
70pub trait EmittingFeature<T>: Feature {
71    /// Creates a receiver that is being notified whenever a new event of type
72    /// `T` is emitted by the feature.
73    fn listen(&self) -> async_channel::Receiver<T>;
74}
75
76/// A feature's addressable `(device, feature)` endpoint on a channel.
77///
78/// Embedding this in a feature replaces the three loose `chan` / `device_index`
79/// / `feature_index` fields every implementation used to carry, and centralises
80/// the HID++2.0 request framing that was otherwise hand-written at every call
81/// site.
82#[derive(Clone)]
83pub(crate) struct FeatureEndpoint {
84    /// The underlying HID++ channel.
85    chan: Arc<HidppChannel>,
86
87    /// The index of the device the feature belongs to.
88    device_index: u8,
89
90    /// The index of the feature in the device's feature table.
91    feature_index: u8,
92}
93
94impl FeatureEndpoint {
95    /// Binds an endpoint to `feature_index` on `device_index` of `chan`.
96    pub(crate) fn new(chan: Arc<HidppChannel>, device_index: u8, feature_index: u8) -> Self {
97        Self {
98            chan,
99            device_index,
100            feature_index,
101        }
102    }
103
104    /// The request header addressing `function` on this endpoint, stamped with
105    /// the channel's next software id.
106    ///
107    /// `function` is a HID++2.0 function id, which is 4-bit; only the low nibble
108    /// is sent. The assert keeps a stray out-of-range id from silently routing
109    /// to a different function in debug builds.
110    fn header(&self, function: u8) -> v20::MessageHeader {
111        debug_assert!(
112            function < 16,
113            "HID++2.0 function id {function} exceeds 4 bits"
114        );
115        v20::MessageHeader {
116            device_index: self.device_index,
117            feature_index: self.feature_index,
118            function_id: U4::from_lo(function),
119            software_id: self.chan.get_sw_id(),
120        }
121    }
122
123    /// Calls `function` with a 3-byte short-report payload and waits for the
124    /// matching response.
125    pub(crate) async fn call(
126        &self,
127        function: u8,
128        args: [u8; 3],
129    ) -> Result<v20::Message, Hidpp20Error> {
130        self.chan
131            .send_v20(v20::Message::Short(self.header(function), args))
132            .await
133    }
134
135    /// Calls `function` with a 16-byte long-report payload and waits for the
136    /// matching response.
137    pub(crate) async fn call_long(
138        &self,
139        function: u8,
140        args: [u8; 16],
141    ) -> Result<v20::Message, Hidpp20Error> {
142        self.chan
143            .send_v20(v20::Message::Long(self.header(function), args))
144            .await
145    }
146
147    /// Sends `function` with a 3-byte short-report payload without waiting for a
148    /// response.
149    ///
150    /// For functions the device answers normally use [`Self::call`]; this is for
151    /// the rare function whose side effect (e.g. a host switch that resets the
152    /// device) prevents a response from ever arriving.
153    pub(crate) async fn notify(&self, function: u8, args: [u8; 3]) -> Result<(), Hidpp20Error> {
154        self.chan
155            .send_and_forget(v20::Message::Short(self.header(function), args).into())
156            .await?;
157        Ok(())
158    }
159}
160
161/// Shared prelude for a feature's event listener.
162///
163/// Drops reports already matched to an outgoing request, parses the raw report
164/// as a HID++2.0 message, and keeps only unsolicited broadcasts addressed to
165/// this `(device_index, feature_index)` with a zero software id. Returns the
166/// event's function id (its sub-id) and extended payload, leaving sub-id
167/// dispatch to the caller — so a multi-event feature filters its sub-ids
168/// explicitly rather than folding the check into the header guard.
169pub(crate) fn event_payload(
170    raw: HidppMessage,
171    matched: bool,
172    device_index: u8,
173    feature_index: u8,
174) -> Option<(U4, [u8; LONG_REPORT_LENGTH - 4])> {
175    if matched {
176        return None;
177    }
178
179    let msg = v20::Message::from(raw);
180    let header = msg.header();
181    if header.device_index != device_index
182        || header.feature_index != feature_index
183        || header.software_id.to_lo() != 0
184    {
185        return None;
186    }
187
188    Some((header.function_id, msg.extend_payload()))
189}
190
191/// A bitfield describing some properties of a feature.
192///
193/// Documentation is taken from <https://drive.google.com/file/d/1ULmw9uJL8b8iwwUo5xjSS9F5Zvno-86y/view>.
194#[derive(Clone, Copy, Hash, Debug)]
195#[cfg_attr(feature = "serde", derive(serde::Serialize))]
196#[non_exhaustive]
197pub struct FeatureType {
198    /// An obsolete feature is a feature that has been replaced by a newer one,
199    /// but is advertised in order for older SWs to still be able to support the
200    /// feature (in case the old SW does not know yet the newer one).
201    pub obsolete: bool,
202
203    /// A SW hidden feature is a feature that should not be known/managed/used
204    /// by end user configuration SW. The host should ignore this type of
205    /// features.
206    pub hidden: bool,
207
208    /// A hidden feature that has been disabled for user software. Used for
209    /// internal testing and manufacturing.
210    pub engineering: bool,
211
212    /// A manufacturing feature that can be permanently deactivated. It is
213    /// usually also hidden and engineering.
214    ///
215    /// This field was added in feature version 2 and will be `false` for all
216    /// older versions.
217    pub manufacturing_deactivatable: bool,
218
219    /// A compliance feature that can be permanently deactivated. It is usually
220    /// also hidden and engineering.
221    ///
222    /// This field was added in feature version 2 and will be `false` for all
223    /// older versions.
224    pub compliance_deactivatable: bool,
225}
226
227impl From<u8> for FeatureType {
228    fn from(value: u8) -> Self {
229        Self {
230            obsolete: value & (1 << 7) != 0,
231            hidden: value & (1 << 6) != 0,
232            engineering: value & (1 << 5) != 0,
233            manufacturing_deactivatable: value & (1 << 4) != 0,
234            compliance_deactivatable: value & (1 << 3) != 0,
235        }
236    }
237}
238
239impl From<FeatureType> for u8 {
240    fn from(value: FeatureType) -> Self {
241        let mut raw = 0;
242
243        if value.obsolete {
244            raw |= 1 << 7
245        }
246        if value.hidden {
247            raw |= 1 << 6
248        }
249        if value.engineering {
250            raw |= 1 << 5
251        }
252        if value.manufacturing_deactivatable {
253            raw |= 1 << 4
254        }
255        if value.compliance_deactivatable {
256            raw |= 1 << 3
257        }
258
259        raw
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::event_payload;
266    use crate::{
267        channel::HidppMessage,
268        nibble::U4,
269        protocol::v20::{Message, MessageHeader},
270    };
271
272    /// Builds a raw long report carrying a HID++2.0 broadcast with the given
273    /// header fields and a recognisable payload.
274    fn broadcast(device_index: u8, feature_index: u8, function: u8, software: u8) -> HidppMessage {
275        Message::Long(
276            MessageHeader {
277                device_index,
278                feature_index,
279                function_id: U4::from_lo(function),
280                software_id: U4::from_lo(software),
281            },
282            [0xab; 16],
283        )
284        .into()
285    }
286
287    #[test]
288    fn accepts_matching_broadcast_and_returns_sub_id() {
289        let (func, payload) =
290            event_payload(broadcast(2, 5, 1, 0), false, 2, 5).expect("broadcast should pass");
291        assert_eq!(func.to_lo(), 1);
292        assert_eq!(payload, [0xab; 16]);
293    }
294
295    #[test]
296    fn rejects_request_matched_report() {
297        // A report already matched to an outgoing request is a response, not an
298        // event.
299        assert!(event_payload(broadcast(2, 5, 0, 0), true, 2, 5).is_none());
300    }
301
302    #[test]
303    fn rejects_other_device_or_feature() {
304        assert!(event_payload(broadcast(9, 5, 0, 0), false, 2, 5).is_none());
305        assert!(event_payload(broadcast(2, 9, 0, 0), false, 2, 5).is_none());
306    }
307
308    #[test]
309    fn gates_on_software_id_only_not_sub_id() {
310        // Only the software id gates a broadcast: a nonzero one is rejected, but
311        // a nonzero function id is a valid event sub-id the caller dispatches on
312        // and must still pass. This is the invariant the old per-feature
313        // `nibble::combine(software_id, function_id) != 0` guard got right only
314        // by accident (those features happened to emit a single sub-id 0 event).
315        assert!(event_payload(broadcast(2, 5, 0, 1), false, 2, 5).is_none());
316        assert!(event_payload(broadcast(2, 5, 7, 0), false, 2, 5).is_some());
317    }
318}