openlogi-hidpp 0.7.1

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
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
//! Implements the Logi Bolt receiver.
//!
//! Bolt can be seen as a successor to the Unifying receiver. Both of them
//! support up to 6 paired devices, but Bolt uses BTLE technology and introduces
//! so-called passkeys for authenticating devices before pairing them.
//!
//! There is little to no public documentation about what registers Bolt
//! supports (and they seem to differ quite substantially from registers
//! supported by Unifying and other receivers), so this implementation is based
//! largely on information gathered by looking at other codebases (primarily
//! Solaar) and searching registers by fuzzing them.

use std::sync::Arc;

use derive_builder::Builder;
use futures::{FutureExt, pin_mut, select};
use num_enum::{IntoPrimitive, TryFromPrimitive};

use super::{RECEIVER_DEVICE_INDEX, ReceiverError};
use crate::{
    channel::{HidppChannel, MessageListenerGuard},
    event::EventEmitter,
    protocol::v10::{self, Hidpp10Error},
};

mod event;

pub use event::{DeviceConnection, DeviceKind, Event, PairingError, PairingPasskeyPressType};

/// All USB vendor & product ID pairs that are known to identify Bolt receivers.
pub const VPID_PAIRS: &[(u16, u16)] = &[(0x046d, 0xc548)];

/// All known registers of the Bolt receiver.
///
/// In most cases you should not need to access these manually, as [`Receiver`]
/// implements many features.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, IntoPrimitive, TryFromPrimitive)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
#[repr(u8)]
pub enum Register {
    /// Allows control over what notifications the receiver sends.
    Notifications = 0x00,

    /// Provides the amount of currently paired devices.
    ///
    /// This is exposed by [`Receiver::count_pairings`].
    Connections = 0x02,

    /// Provides information about the receiver and paired devices.
    ///
    /// It uses sub-registers, as defined in [`InfoSubRegister`], to
    /// differentiate between different kinds of information.
    ReceiverInfo = 0xb5,

    /// Provides support for discovering devices that are ready to pair.
    ///
    /// Use [`Receiver::discover_devices`] and
    /// [`Receiver::cancel_device_discovery`] to control device discovery.
    DeviceDiscovery = 0xc0,

    /// Provides pairing and unpairing support.
    ///
    /// Use [`Receiver::pair_device`] and [`Receiver::unpair_device`] for
    /// pairing and unpairing.
    Pairing = 0xc1,

    /// Exposes the unique ID of the receiver. This seems to differ from the
    /// serial number.
    ///
    /// Use [`Receiver::get_unique_id`] to query this value.
    UniqueId = 0xfb,
}

/// All known sub-registers of the [`Register::ReceiverInfo`] register.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, IntoPrimitive, TryFromPrimitive)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
#[repr(u8)]
pub enum InfoSubRegister {
    /// Provides information about a specific paired device. The device index (4
    /// bits) has to be added to the register address.
    ///
    /// Exposed by [`Receiver::get_device_pairing_information`].
    DevicePairingInformation = 0x50, // 0x5N with N = device index

    /// Provides the name of a paired device. The device index (4
    /// bits) has to be added to the register address.
    ///
    /// Exposed by [`Receiver::get_device_codename`].
    DeviceCodename = 0x60, // 0x6N with N = device index
}

/// Implements the Bolt receiver.
#[derive(Clone)]
pub struct Receiver {
    chan: Arc<HidppChannel>,
    emitter: Arc<EventEmitter<Event>>,
    _listener: Arc<MessageListenerGuard>,
}

impl Receiver {
    /// Tries to initialize a new [`Receiver`] from a raw HID++ channel.
    ///
    /// If no receiver could be found, or if the vendor and product IDs don't
    /// match the ones of any known Bolt receiver, this function will return
    /// [`ReceiverError::UnknownReceiver`].
    pub fn new(chan: Arc<HidppChannel>) -> Result<Self, ReceiverError> {
        if !VPID_PAIRS.contains(&(chan.vendor_id, chan.product_id)) {
            return Err(ReceiverError::UnknownReceiver);
        }

        let emitter = Arc::new(EventEmitter::new());

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

            move |raw, matched| {
                // A report already matched to an outgoing request is a
                // response, not a notification.
                if matched {
                    return;
                }

                if let Some(event) = event::decode(&v10::Message::from(raw)) {
                    emitter.emit(event);
                }
            }
        });

        Ok(Receiver {
            _listener: Arc::new(listener),
            chan,
            emitter,
        })
    }

    /// Creates a new listener for receiving receiver events.
    #[must_use]
    pub fn listen(&self) -> async_channel::Receiver<Event> {
        self.emitter.create_receiver()
    }

    /// Queries the current information about what notifications are enabled.
    pub async fn get_notification_state(&self) -> Result<NotificationState, ReceiverError> {
        let response = self
            .chan
            .read_register(
                RECEIVER_DEVICE_INDEX,
                Register::Notifications.into(),
                [0u8; 3],
            )
            .await?;

        Ok(NotificationState {
            wireless_notifications: (response[1] & 1) != 0,
        })
    }

    /// Configures what notifications are enabled and thus reported by the
    /// receiver.
    pub async fn set_notification_state(
        &self,
        state: NotificationState,
    ) -> Result<(), ReceiverError> {
        self.chan
            .write_register(
                RECEIVER_DEVICE_INDEX,
                Register::Notifications.into(),
                [0, u8::from(state.wireless_notifications), 0],
            )
            .await?;

        Ok(())
    }

    /// Counts the amount of devices currently paired to this receiver. The
    /// devices don't have to be online to be included here as pairings are
    /// persistent.
    pub async fn count_pairings(&self) -> Result<u8, ReceiverError> {
        let response = self
            .chan
            .read_register(
                RECEIVER_DEVICE_INDEX,
                Register::Connections.into(),
                [0u8; 3],
            )
            .await?;

        Ok(response[1])
    }

    /// Triggers device arrival notifications for all devices currently
    /// connected to the receiver. This is useful for device enumeration.
    ///
    /// Check [`Self::get_notification_state`] first to make sure that
    /// [`NotificationState::wireless_notifications`] is enabled.
    pub async fn trigger_device_arrival(&self) -> Result<(), ReceiverError> {
        self.chan
            .write_register(
                RECEIVER_DEVICE_INDEX,
                Register::Connections.into(),
                [0x02, 0x00, 0x00],
            )
            .await?;

        Ok(())
    }

    /// Collects information about all paired devices by calling
    /// [`Self::trigger_device_arrival`] and collecting incoming
    /// [`Event::DeviceConnection`] events.
    ///
    /// Check [`Self::get_notification_state`] first to make sure that
    /// [`NotificationState::wireless_notifications`] is enabled.
    pub async fn collect_paired_devices(&self) -> Result<Vec<DeviceConnection>, ReceiverError> {
        // The idea here is that, when triggering fake device arrival notifications, the
        // receiver will send the register write confirmation message only AFTER sending
        // all arrival notifications.
        // So we will trigger device arrival notifications and continue collecting those
        // until the original future has completed.

        let mut devices = vec![];

        let rx = self.listen();
        let fin = self.trigger_device_arrival().fuse();
        pin_mut!(fin);

        loop {
            select! {
                _ = fin => break,
                res = rx.recv().fuse() => {
                    let Ok(Event::DeviceConnection(connection)) = res else {
                        continue;
                    };

                    devices.push(connection);
                }
            }
        }

        Ok(devices)
    }

    /// Retrieves the unique ID of the receiver. This is not the same as the
    /// serial number.
    pub async fn get_unique_id(&self) -> Result<String, ReceiverError> {
        let response = self
            .chan
            .read_long_register(RECEIVER_DEVICE_INDEX, Register::UniqueId.into(), [0u8; 3])
            .await?;

        // When decoding the last 8 bytes of the response to their ASCII representation
        // we seem to get a valid hex string representing 4 bytes of data.
        // Interpreting this hex string as little endian we seem to get the same decimal
        // value the Options+ software calls `udid` (unique device identifier?). I am
        // not sure what this is about and it may be a (major) coincidence that these
        // values match for my receiver, but it could be worth keeping this in mind.

        // I have no clue how to retrieve the serial number of the receiver.

        Ok(str::from_utf8(&response)
            .map_err(|_| Hidpp10Error::UnsupportedResponse)?
            .to_string())
    }

    /// Provides the pairing information of a specific paired device by its
    /// index.
    pub async fn get_device_pairing_information(
        &self,
        device_index: u8,
    ) -> Result<DevicePairingInformation, ReceiverError> {
        let response = self
            .chan
            .read_long_register(
                RECEIVER_DEVICE_INDEX,
                Register::ReceiverInfo.into(),
                [
                    u8::from(InfoSubRegister::DevicePairingInformation) + (device_index & 0x0f),
                    0x00,
                    0x00,
                ],
            )
            .await?;

        Ok(DevicePairingInformation {
            wpid: u16::from_le_bytes([response[2], response[3]]),
            // Kind is identity-only: an unrecognised nibble folds to
            // `Unknown` instead of failing the whole pairing-info read.
            kind: DeviceKind::from(response[1] & 0x0f),
            encrypted: response[1] & (1 << 5) != 0,
            online: response[1] & (1 << 6) == 0,
            unit_id: [response[4], response[5], response[6], response[7]],
        })
    }

    /// Provides the codename of a specific paired device by its index.
    pub async fn get_device_codename(&self, device_index: u8) -> Result<String, ReceiverError> {
        // For device names longer than 13 characters this may need to be called
        // multiple times with different parameters. I don't have a device with
        // such a name to be able to test this.

        let response = self
            .chan
            .read_long_register(
                RECEIVER_DEVICE_INDEX,
                Register::ReceiverInfo.into(),
                [
                    u8::from(InfoSubRegister::DeviceCodename) + (device_index & 0x0f),
                    0x01,
                    0x00,
                ],
            )
            .await?;

        Ok(parse_codename(&response)
            .ok_or(Hidpp10Error::UnsupportedResponse)?
            .to_string())
    }

    /// Unpairs a device from the receiver by its index.
    pub async fn unpair_device(&self, device_index: u8) -> Result<(), ReceiverError> {
        let mut payload = [0u8; 16];
        payload[0] = 0x03;
        payload[1] = device_index;

        self.chan
            .write_long_register(RECEIVER_DEVICE_INDEX, Register::Pairing.into(), payload)
            .await?;

        Ok(())
    }

    /// Starts the pairing process for a new device.
    ///
    /// The required `address` and `authentication` values are usually
    /// discovered from the [`Event::DeviceDiscoveryDeviceDetails`] event which
    /// is emitted regularly when actively discovering available devices
    /// ([`Self::discover_devices`]).
    ///
    /// `entropy` specifies how complex the authentication passkey should be.
    /// For mice, this defines the amount of keypresses (left or right) the user
    /// has to perform. Not all values seem to be supported.
    pub async fn pair_device(
        &self,
        slot: u8,
        address: [u8; 6],
        authentication: u8,
        entropy: u8,
    ) -> Result<(), ReceiverError> {
        let mut payload = [0u8; 16];
        payload[0] = 0x01;
        payload[1] = slot;
        payload[2..=7].copy_from_slice(&address);
        payload[8] = authentication;
        payload[9] = entropy;

        self.chan
            .write_long_register(RECEIVER_DEVICE_INDEX, Register::Pairing.into(), payload)
            .await?;

        Ok(())
    }

    /// Starts device discovery for `timeout` seconds ([`None`] = default, seems
    /// to be 30s). The maximum supported value is 60s.
    ///
    /// While device discovery is enabled,
    /// [`Event::DeviceDiscoveryDeviceDetails`] and
    /// [`Event::DeviceDiscoveryDeviceName`] events are emitted for every
    /// discovered device.
    pub async fn discover_devices(&self, timeout: Option<u8>) -> Result<(), ReceiverError> {
        self.chan
            .write_register(
                RECEIVER_DEVICE_INDEX,
                Register::DeviceDiscovery.into(),
                [timeout.unwrap_or(0x00), 0x01, 0x00],
            )
            .await?;

        Ok(())
    }

    /// Cancels the device discovery process.
    pub async fn cancel_device_discovery(&self) -> Result<(), ReceiverError> {
        self.chan
            .write_register(
                RECEIVER_DEVICE_INDEX,
                Register::DeviceDiscovery.into(),
                [0x00, 0x02, 0x00],
            )
            .await?;

        Ok(())
    }
}

/// Extract the codename chunk from a `DeviceCodename` register read.
///
/// `response[2]` is the device-reported name length. A name longer than the
/// 13 bytes one response carries is clamped to the chunk present (fetching
/// the rest takes further reads with different parameters); a length byte
/// pointing past the response must not panic. `None` for non-UTF-8 bytes.
fn parse_codename(response: &[u8; 16]) -> Option<&str> {
    let end = 3usize.saturating_add(usize::from(response[2]));
    let raw = response.get(3..end.min(response.len()))?;
    str::from_utf8(raw).ok()
}

/// Indicates which notifications are enabled and thus sent by the receiver.
///
/// This information can be queried using [`Receiver::get_notification_state`].
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Builder)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct NotificationState {
    /// Whether the receiver sends device arrival/removal notifications.
    pub wireless_notifications: bool,
}

/// Represents information about a paired device.
///
/// This information can be queried using
/// [`Receiver::get_device_pairing_information`].
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct DevicePairingInformation {
    /// Wireless product ID of the paired device.
    pub wpid: u16,
    /// Device kind reported by the receiver.
    pub kind: DeviceKind,
    /// Whether the link is encrypted.
    pub encrypted: bool,
    /// Whether the device is currently online.
    pub online: bool,
    /// Device unit ID.
    pub unit_id: [u8; 4],
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn codename_with_oversized_length_clamps_to_available_chunk() {
        let mut response = [0u8; 16];
        response[2] = 200;
        response[3..16].copy_from_slice(b"MX Anywhere 3");

        assert_eq!(parse_codename(&response), Some("MX Anywhere 3"));
    }

    #[test]
    fn codename_within_bounds_parses() {
        let mut response = [0u8; 16];
        response[2] = 5;
        response[3..8].copy_from_slice(b"Casa!");

        assert_eq!(parse_codename(&response), Some("Casa!"));
    }

    #[test]
    fn codename_rejects_invalid_utf8() {
        let mut response = [0u8; 16];
        response[2] = 2;
        response[3] = 0xff;
        response[4] = 0xfe;

        assert_eq!(parse_codename(&response), None);
    }
}