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