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}