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