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