openlogi-hidpp 0.8.12

OpenLogi's hard fork of the `hidpp` crate (Logitech HID++ protocol).
Documentation
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
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
//! Specific device feature implementations.

use std::{any::Any, sync::Arc};

use crate::{
    channel::{
        AbandonedReply, HidppChannel, HidppMessage, LONG_REPORT_LENGTH, MessageListenerGuard,
    },
    emitter::EventEmitter,
    nibble::U4,
    protocol::v20::{self, Hidpp20Error},
};

pub mod adjustable_dpi;
pub mod backlight;
pub mod battery_status;
pub mod battery_voltage;
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 haptic_feedback;
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>;
}

/// Decodes a feature's event payload from its HID++2.0 event sub-id.
///
/// Implemented by a feature's event enum. [`EventSource::attach`] calls
/// [`Self::decode`] for every unsolicited broadcast addressed to the feature,
/// so this is the only part of a feature's event handling that has to vary —
/// [`EventSource`] owns the listener registration, the [`event_payload`]
/// filtering, and the emitting. Returning `None` drops the report exactly
/// like the hand-rolled per-feature listeners this replaced did on an
/// unrecognised sub-id or an unparsable payload.
pub(crate) trait DecodeEvent: Clone + Send + Sync + 'static {
    /// Decodes `payload` for event sub-id `sub_id`, or returns `None` to drop
    /// the report.
    fn decode(sub_id: u8, payload: &[u8; LONG_REPORT_LENGTH - 4]) -> Option<Self>
    where
        Self: Sized;
}

/// Owns the [`EventEmitter`] and message listener backing an
/// [`EmittingFeature`] implementation.
///
/// Every emitting feature used to hand-roll the same listener ceremony in its
/// `CreatableFeature::new`: build an `Arc<EventEmitter<E>>`, register a
/// guarded message listener, filter the raw report through [`event_payload`],
/// and emit whatever the feature's own decode step produced. A feature now
/// holds one `EventSource<E>` field, builds it with [`Self::attach`], and
/// implements [`EmittingFeature`] with a one-line [`Self::listen`] delegate.
pub(crate) struct EventSource<E: DecodeEvent> {
    emitter: Arc<EventEmitter<E>>,

    /// Removes the message listener when the source is dropped.
    _listener: MessageListenerGuard,
}

impl<E: DecodeEvent> EventSource<E> {
    /// Registers a listener on `chan` that decodes broadcasts addressed to
    /// `(device_index, feature_index)` via [`DecodeEvent::decode`] and emits
    /// them to receivers created by [`Self::listen`].
    pub(crate) fn attach(chan: &Arc<HidppChannel>, device_index: u8, feature_index: u8) -> Self {
        let emitter = Arc::new(EventEmitter::new());

        let listener = chan.add_msg_listener_guarded({
            let emitter = Arc::clone(&emitter);

            move |raw, matched| {
                let Some((func, payload)) =
                    event_payload(raw, matched, device_index, feature_index)
                else {
                    return;
                };
                if let Some(event) = E::decode(func.to_lo(), &payload) {
                    emitter.emit(event);
                }
            }
        });

        Self {
            emitter,
            _listener: listener,
        }
    }

    /// Creates a receiver notified of every event this source decodes.
    pub(crate) fn listen(&self) -> async_channel::Receiver<E> {
        self.emitter.create_receiver()
    }
}

/// 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.call_with(function, args, AbandonedReply::Quarantine)
            .await
    }

    /// [`Self::call`], choosing what to do about a reply still owed to an
    /// abandoned call with the same header. Only a function that reads
    /// state no write can change should pass
    /// [`AbandonedReply::AdoptIdentical`].
    pub(crate) async fn call_with(
        &self,
        function: u8,
        args: [u8; 3],
        abandoned: AbandonedReply,
    ) -> Result<v20::Message, Hidpp20Error> {
        self.chan
            .send_v20_with(v20::Message::Short(self.header(function), args), abandoned)
            .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
    }

    /// Calls a function that echoes all three request bytes, waiting through
    /// the native write before applying the response deadline. Differing
    /// payloads (e.g. a late ownership claim during rollback) do not match.
    ///
    /// The owner must drive this future to completion; see
    /// [`HidppChannel::send_v20_write_through`].
    pub(crate) async fn call_echoed_write_through(
        &self,
        function: u8,
        args: [u8; 3],
    ) -> Result<v20::Message, Hidpp20Error> {
        self.chan
            .send_v20_write_through(
                v20::Message::Short(self.header(function), args),
                move |response| response.extend_payload()[..3] == args,
            )
            .await
    }

    /// Calls `function` with a 16-byte long-report payload, waiting through the
    /// native write before applying the response deadline.
    ///
    /// The owner must drive this future to completion; see
    /// [`HidppChannel::send_v20_write_through`].
    pub(crate) async fn call_long_write_through(
        &self,
        function: u8,
        args: [u8; 16],
    ) -> Result<v20::Message, Hidpp20Error> {
        self.chan
            .send_v20_write_through(v20::Message::Long(self.header(function), args), |_| true)
            .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()))
}

bitflags::bitflags! {
    /// A bitfield describing some properties of a feature.
    ///
    /// Documentation is taken from <https://drive.google.com/file/d/1ULmw9uJL8b8iwwUo5xjSS9F5Zvno-86y/view>.
    #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
    #[cfg_attr(feature = "serde", derive(serde::Serialize))]
    pub struct FeatureType: u8 {
        /// 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).
        const OBSOLETE = 1 << 7;

        /// 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.
        const HIDDEN = 1 << 6;

        /// A hidden feature that has been disabled for user software. Used for
        /// internal testing and manufacturing.
        const ENGINEERING = 1 << 5;

        /// A manufacturing feature that can be permanently deactivated. It is
        /// usually also hidden and engineering.
        ///
        /// This flag was added in feature version 2 and is never set by older
        /// versions.
        const MANUFACTURING_DEACTIVATABLE = 1 << 4;

        /// A compliance feature that can be permanently deactivated. It is
        /// usually also hidden and engineering.
        ///
        /// This flag was added in feature version 2 and is never set by older
        /// versions.
        const COMPLIANCE_DEACTIVATABLE = 1 << 3;
    }
}

#[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());
    }
}