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