hidpp/receiver/bolt.rs
1//! Implements the Logi Bolt receiver.
2//!
3//! Bolt can be seen as a successor to the Unifying receiver. Both of them
4//! support up to 6 paired devices, but Bolt uses BTLE technology and introduces
5//! so-called passkeys for authenticating devices before pairing them.
6//!
7//! There is little to no public documentation about what registers Bolt
8//! supports (and they seem to differ quite substantially from registers
9//! supported by Unifying and other receivers), so this implementation is based
10//! largely on information gathered by looking at other codebases (primarily
11//! Solaar) and searching registers by fuzzing them.
12
13use std::sync::Arc;
14
15use derive_builder::Builder;
16use futures::{FutureExt, pin_mut, select};
17use num_enum::{IntoPrimitive, TryFromPrimitive};
18
19use super::{RECEIVER_DEVICE_INDEX, ReceiverError};
20use crate::{
21 channel::{HidppChannel, MessageListenerGuard},
22 event::EventEmitter,
23 protocol::v10::{self, Hidpp10Error},
24};
25
26mod event;
27
28pub use event::{DeviceConnection, DeviceKind, Event, PairingError, PairingPasskeyPressType};
29
30/// All USB vendor & product ID pairs that are known to identify Bolt receivers.
31pub const VPID_PAIRS: &[(u16, u16)] = &[(0x046d, 0xc548)];
32
33/// All known registers of the Bolt receiver.
34///
35/// In most cases you should not need to access these manually, as [`Receiver`]
36/// implements many features.
37#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, IntoPrimitive, TryFromPrimitive)]
38#[cfg_attr(feature = "serde", derive(serde::Serialize))]
39#[non_exhaustive]
40#[repr(u8)]
41pub enum Register {
42 /// Allows control over what notifications the receiver sends.
43 Notifications = 0x00,
44
45 /// Provides the amount of currently paired devices.
46 ///
47 /// This is exposed by [`Receiver::count_pairings`].
48 Connections = 0x02,
49
50 /// Provides information about the receiver and paired devices.
51 ///
52 /// It uses sub-registers, as defined in [`InfoSubRegister`], to
53 /// differentiate between different kinds of information.
54 ReceiverInfo = 0xb5,
55
56 /// Provides support for discovering devices that are ready to pair.
57 ///
58 /// Use [`Receiver::discover_devices`] and
59 /// [`Receiver::cancel_device_discovery`] to control device discovery.
60 DeviceDiscovery = 0xc0,
61
62 /// Provides pairing and unpairing support.
63 ///
64 /// Use [`Receiver::pair_device`] and [`Receiver::unpair_device`] for
65 /// pairing and unpairing.
66 Pairing = 0xc1,
67
68 /// Exposes the unique ID of the receiver. This seems to differ from the
69 /// serial number.
70 ///
71 /// Use [`Receiver::get_unique_id`] to query this value.
72 UniqueId = 0xfb,
73}
74
75/// All known sub-registers of the [`Register::ReceiverInfo`] register.
76#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, IntoPrimitive, TryFromPrimitive)]
77#[cfg_attr(feature = "serde", derive(serde::Serialize))]
78#[non_exhaustive]
79#[repr(u8)]
80pub enum InfoSubRegister {
81 /// Provides information about a specific paired device. The device index (4
82 /// bits) has to be added to the register address.
83 ///
84 /// Exposed by [`Receiver::get_device_pairing_information`].
85 DevicePairingInformation = 0x50, // 0x5N with N = device index
86
87 /// Provides the name of a paired device. The device index (4
88 /// bits) has to be added to the register address.
89 ///
90 /// Exposed by [`Receiver::get_device_codename`].
91 DeviceCodename = 0x60, // 0x6N with N = device index
92}
93
94/// Implements the Bolt receiver.
95#[derive(Clone)]
96pub struct Receiver {
97 chan: Arc<HidppChannel>,
98 emitter: Arc<EventEmitter<Event>>,
99 _listener: Arc<MessageListenerGuard>,
100}
101
102impl Receiver {
103 /// Tries to initialize a new [`Receiver`] from a raw HID++ channel.
104 ///
105 /// If no receiver could be found, or if the vendor and product IDs don't
106 /// match the ones of any known Bolt receiver, this function will return
107 /// [`ReceiverError::UnknownReceiver`].
108 pub fn new(chan: Arc<HidppChannel>) -> Result<Self, ReceiverError> {
109 if !VPID_PAIRS.contains(&(chan.vendor_id, chan.product_id)) {
110 return Err(ReceiverError::UnknownReceiver);
111 }
112
113 let emitter = Arc::new(EventEmitter::new());
114
115 let listener = chan.add_msg_listener_guarded({
116 let emitter = Arc::clone(&emitter);
117
118 move |raw, matched| {
119 // A report already matched to an outgoing request is a
120 // response, not a notification.
121 if matched {
122 return;
123 }
124
125 if let Some(event) = event::decode(&v10::Message::from(raw)) {
126 emitter.emit(event);
127 }
128 }
129 });
130
131 Ok(Receiver {
132 _listener: Arc::new(listener),
133 chan,
134 emitter,
135 })
136 }
137
138 /// Creates a new listener for receiving receiver events.
139 #[must_use]
140 pub fn listen(&self) -> async_channel::Receiver<Event> {
141 self.emitter.create_receiver()
142 }
143
144 /// Queries the current information about what notifications are enabled.
145 pub async fn get_notification_state(&self) -> Result<NotificationState, ReceiverError> {
146 let response = self
147 .chan
148 .read_register(
149 RECEIVER_DEVICE_INDEX,
150 Register::Notifications.into(),
151 [0u8; 3],
152 )
153 .await?;
154
155 Ok(NotificationState {
156 wireless_notifications: (response[1] & 1) != 0,
157 })
158 }
159
160 /// Configures what notifications are enabled and thus reported by the
161 /// receiver.
162 pub async fn set_notification_state(
163 &self,
164 state: NotificationState,
165 ) -> Result<(), ReceiverError> {
166 self.chan
167 .write_register(
168 RECEIVER_DEVICE_INDEX,
169 Register::Notifications.into(),
170 [0, u8::from(state.wireless_notifications), 0],
171 )
172 .await?;
173
174 Ok(())
175 }
176
177 /// Counts the amount of devices currently paired to this receiver. The
178 /// devices don't have to be online to be included here as pairings are
179 /// persistent.
180 pub async fn count_pairings(&self) -> Result<u8, ReceiverError> {
181 let response = self
182 .chan
183 .read_register(
184 RECEIVER_DEVICE_INDEX,
185 Register::Connections.into(),
186 [0u8; 3],
187 )
188 .await?;
189
190 Ok(response[1])
191 }
192
193 /// Triggers device arrival notifications for all devices currently
194 /// connected to the receiver. This is useful for device enumeration.
195 ///
196 /// Check [`Self::get_notification_state`] first to make sure that
197 /// [`NotificationState::wireless_notifications`] is enabled.
198 pub async fn trigger_device_arrival(&self) -> Result<(), ReceiverError> {
199 self.chan
200 .write_register(
201 RECEIVER_DEVICE_INDEX,
202 Register::Connections.into(),
203 [0x02, 0x00, 0x00],
204 )
205 .await?;
206
207 Ok(())
208 }
209
210 /// Collects information about all paired devices by calling
211 /// [`Self::trigger_device_arrival`] and collecting incoming
212 /// [`Event::DeviceConnection`] events.
213 ///
214 /// Check [`Self::get_notification_state`] first to make sure that
215 /// [`NotificationState::wireless_notifications`] is enabled.
216 pub async fn collect_paired_devices(&self) -> Result<Vec<DeviceConnection>, ReceiverError> {
217 // The idea here is that, when triggering fake device arrival notifications, the
218 // receiver will send the register write confirmation message only AFTER sending
219 // all arrival notifications.
220 // So we will trigger device arrival notifications and continue collecting those
221 // until the original future has completed.
222
223 let mut devices = vec![];
224
225 let rx = self.listen();
226 let fin = self.trigger_device_arrival().fuse();
227 pin_mut!(fin);
228
229 loop {
230 select! {
231 _ = fin => break,
232 res = rx.recv().fuse() => {
233 let Ok(Event::DeviceConnection(connection)) = res else {
234 continue;
235 };
236
237 devices.push(connection);
238 }
239 }
240 }
241
242 Ok(devices)
243 }
244
245 /// Retrieves the unique ID of the receiver. This is not the same as the
246 /// serial number.
247 pub async fn get_unique_id(&self) -> Result<String, ReceiverError> {
248 let response = self
249 .chan
250 .read_long_register(RECEIVER_DEVICE_INDEX, Register::UniqueId.into(), [0u8; 3])
251 .await?;
252
253 // When decoding the last 8 bytes of the response to their ASCII representation
254 // we seem to get a valid hex string representing 4 bytes of data.
255 // Interpreting this hex string as little endian we seem to get the same decimal
256 // value the Options+ software calls `udid` (unique device identifier?). I am
257 // not sure what this is about and it may be a (major) coincidence that these
258 // values match for my receiver, but it could be worth keeping this in mind.
259
260 // I have no clue how to retrieve the serial number of the receiver.
261
262 Ok(str::from_utf8(&response)
263 .map_err(|_| Hidpp10Error::UnsupportedResponse)?
264 .to_string())
265 }
266
267 /// Provides the pairing information of a specific paired device by its
268 /// index.
269 pub async fn get_device_pairing_information(
270 &self,
271 device_index: u8,
272 ) -> Result<DevicePairingInformation, ReceiverError> {
273 let response = self
274 .chan
275 .read_long_register(
276 RECEIVER_DEVICE_INDEX,
277 Register::ReceiverInfo.into(),
278 [
279 u8::from(InfoSubRegister::DevicePairingInformation) + (device_index & 0x0f),
280 0x00,
281 0x00,
282 ],
283 )
284 .await?;
285
286 Ok(DevicePairingInformation {
287 wpid: u16::from_le_bytes([response[2], response[3]]),
288 // Kind is identity-only: an unrecognised nibble folds to
289 // `Unknown` instead of failing the whole pairing-info read.
290 kind: DeviceKind::from(response[1] & 0x0f),
291 encrypted: response[1] & (1 << 5) != 0,
292 online: response[1] & (1 << 6) == 0,
293 unit_id: [response[4], response[5], response[6], response[7]],
294 })
295 }
296
297 /// Provides the codename of a specific paired device by its index.
298 pub async fn get_device_codename(&self, device_index: u8) -> Result<String, ReceiverError> {
299 // For device names longer than 13 characters this may need to be called
300 // multiple times with different parameters. I don't have a device with
301 // such a name to be able to test this.
302
303 let response = self
304 .chan
305 .read_long_register(
306 RECEIVER_DEVICE_INDEX,
307 Register::ReceiverInfo.into(),
308 [
309 u8::from(InfoSubRegister::DeviceCodename) + (device_index & 0x0f),
310 0x01,
311 0x00,
312 ],
313 )
314 .await?;
315
316 Ok(parse_codename(&response)
317 .ok_or(Hidpp10Error::UnsupportedResponse)?
318 .to_string())
319 }
320
321 /// Unpairs a device from the receiver by its index.
322 pub async fn unpair_device(&self, device_index: u8) -> Result<(), ReceiverError> {
323 let mut payload = [0u8; 16];
324 payload[0] = 0x03;
325 payload[1] = device_index;
326
327 self.chan
328 .write_long_register(RECEIVER_DEVICE_INDEX, Register::Pairing.into(), payload)
329 .await?;
330
331 Ok(())
332 }
333
334 /// Starts the pairing process for a new device.
335 ///
336 /// The required `address` and `authentication` values are usually
337 /// discovered from the [`Event::DeviceDiscoveryDeviceDetails`] event which
338 /// is emitted regularly when actively discovering available devices
339 /// ([`Self::discover_devices`]).
340 ///
341 /// `entropy` specifies how complex the authentication passkey should be.
342 /// For mice, this defines the amount of keypresses (left or right) the user
343 /// has to perform. Not all values seem to be supported.
344 pub async fn pair_device(
345 &self,
346 slot: u8,
347 address: [u8; 6],
348 authentication: u8,
349 entropy: u8,
350 ) -> Result<(), ReceiverError> {
351 let mut payload = [0u8; 16];
352 payload[0] = 0x01;
353 payload[1] = slot;
354 payload[2..=7].copy_from_slice(&address);
355 payload[8] = authentication;
356 payload[9] = entropy;
357
358 self.chan
359 .write_long_register(RECEIVER_DEVICE_INDEX, Register::Pairing.into(), payload)
360 .await?;
361
362 Ok(())
363 }
364
365 /// Starts device discovery for `timeout` seconds ([`None`] = default, seems
366 /// to be 30s). The maximum supported value is 60s.
367 ///
368 /// While device discovery is enabled,
369 /// [`Event::DeviceDiscoveryDeviceDetails`] and
370 /// [`Event::DeviceDiscoveryDeviceName`] events are emitted for every
371 /// discovered device.
372 pub async fn discover_devices(&self, timeout: Option<u8>) -> Result<(), ReceiverError> {
373 self.chan
374 .write_register(
375 RECEIVER_DEVICE_INDEX,
376 Register::DeviceDiscovery.into(),
377 [timeout.unwrap_or(0x00), 0x01, 0x00],
378 )
379 .await?;
380
381 Ok(())
382 }
383
384 /// Cancels the device discovery process.
385 pub async fn cancel_device_discovery(&self) -> Result<(), ReceiverError> {
386 self.chan
387 .write_register(
388 RECEIVER_DEVICE_INDEX,
389 Register::DeviceDiscovery.into(),
390 [0x00, 0x02, 0x00],
391 )
392 .await?;
393
394 Ok(())
395 }
396}
397
398/// Extract the codename chunk from a `DeviceCodename` register read.
399///
400/// `response[2]` is the device-reported name length. A name longer than the
401/// 13 bytes one response carries is clamped to the chunk present (fetching
402/// the rest takes further reads with different parameters); a length byte
403/// pointing past the response must not panic. `None` for non-UTF-8 bytes.
404fn parse_codename(response: &[u8; 16]) -> Option<&str> {
405 let end = 3usize.saturating_add(usize::from(response[2]));
406 let raw = response.get(3..end.min(response.len()))?;
407 str::from_utf8(raw).ok()
408}
409
410/// Indicates which notifications are enabled and thus sent by the receiver.
411///
412/// This information can be queried using [`Receiver::get_notification_state`].
413#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Builder)]
414#[cfg_attr(feature = "serde", derive(serde::Serialize))]
415#[non_exhaustive]
416pub struct NotificationState {
417 /// Whether the receiver sends device arrival/removal notifications.
418 pub wireless_notifications: bool,
419}
420
421/// Represents information about a paired device.
422///
423/// This information can be queried using
424/// [`Receiver::get_device_pairing_information`].
425#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
426#[cfg_attr(feature = "serde", derive(serde::Serialize))]
427#[non_exhaustive]
428pub struct DevicePairingInformation {
429 /// Wireless product ID of the paired device.
430 pub wpid: u16,
431 /// Device kind reported by the receiver.
432 pub kind: DeviceKind,
433 /// Whether the link is encrypted.
434 pub encrypted: bool,
435 /// Whether the device is currently online.
436 pub online: bool,
437 /// Device unit ID.
438 pub unit_id: [u8; 4],
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444
445 #[test]
446 fn codename_with_oversized_length_clamps_to_available_chunk() {
447 let mut response = [0u8; 16];
448 response[2] = 200;
449 response[3..16].copy_from_slice(b"MX Anywhere 3");
450
451 assert_eq!(parse_codename(&response), Some("MX Anywhere 3"));
452 }
453
454 #[test]
455 fn codename_within_bounds_parses() {
456 let mut response = [0u8; 16];
457 response[2] = 5;
458 response[3..8].copy_from_slice(b"Casa!");
459
460 assert_eq!(parse_codename(&response), Some("Casa!"));
461 }
462
463 #[test]
464 fn codename_rejects_invalid_utf8() {
465 let mut response = [0u8; 16];
466 response[2] = 2;
467 response[3] = 0xff;
468 response[4] = 0xfe;
469
470 assert_eq!(parse_codename(&response), None);
471 }
472}