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};
18use openlogi_device_registry::receiver::{ReceiverProtocol, find_receiver};
19
20use super::{RECEIVER_DEVICE_INDEX, ReceiverError};
21use crate::{
22 channel::{HidppChannel, MessageListenerGuard},
23 emitter::EventEmitter,
24 protocol::v10::{self, Hidpp10Error},
25};
26
27mod event;
28
29pub use event::{
30 DeviceConnection, DeviceKind, Event, PairingPasskeyPressType, decode as decode_notification,
31};
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 find_receiver(chan.vendor_id, chan.product_id)
110 .is_none_or(|receiver| receiver.protocol != ReceiverProtocol::Bolt)
111 {
112 return Err(ReceiverError::UnknownReceiver);
113 }
114
115 let emitter = Arc::new(EventEmitter::new());
116
117 let listener = chan.add_msg_listener_guarded({
118 let emitter = Arc::clone(&emitter);
119
120 move |raw, matched| {
121 // A report already matched to an outgoing request is a
122 // response, not a notification.
123 if matched {
124 return;
125 }
126
127 if let Some(event) = event::decode(&v10::Message::from(raw)) {
128 emitter.emit(event);
129 }
130 }
131 });
132
133 Ok(Receiver {
134 _listener: Arc::new(listener),
135 chan,
136 emitter,
137 })
138 }
139
140 /// Creates a new listener for receiving receiver events.
141 #[must_use]
142 pub fn listen(&self) -> async_channel::Receiver<Event> {
143 self.emitter.create_receiver()
144 }
145
146 /// Queries the current information about what notifications are enabled.
147 pub async fn get_notification_state(&self) -> Result<NotificationState, ReceiverError> {
148 let response = self
149 .chan
150 .read_register(
151 RECEIVER_DEVICE_INDEX,
152 Register::Notifications.into(),
153 [0u8; 3],
154 )
155 .await?;
156
157 Ok(NotificationState {
158 wireless_notifications: (response[1] & 1) != 0,
159 })
160 }
161
162 /// Configures what notifications are enabled and thus reported by the
163 /// receiver.
164 pub async fn set_notification_state(
165 &self,
166 state: NotificationState,
167 ) -> Result<(), ReceiverError> {
168 self.chan
169 .write_register(
170 RECEIVER_DEVICE_INDEX,
171 Register::Notifications.into(),
172 [0, u8::from(state.wireless_notifications), 0],
173 )
174 .await?;
175
176 Ok(())
177 }
178
179 /// Counts the amount of devices currently paired to this receiver. The
180 /// devices don't have to be online to be included here as pairings are
181 /// persistent.
182 pub async fn count_pairings(&self) -> Result<u8, ReceiverError> {
183 let response = self
184 .chan
185 .read_register(
186 RECEIVER_DEVICE_INDEX,
187 Register::Connections.into(),
188 [0u8; 3],
189 )
190 .await?;
191
192 Ok(response[1])
193 }
194
195 /// Triggers device arrival notifications for all devices currently
196 /// connected to the receiver. This is useful for device enumeration.
197 ///
198 /// Check [`Self::get_notification_state`] first to make sure that
199 /// [`NotificationState::wireless_notifications`] is enabled.
200 pub async fn trigger_device_arrival(&self) -> Result<(), ReceiverError> {
201 self.chan
202 .write_register(
203 RECEIVER_DEVICE_INDEX,
204 Register::Connections.into(),
205 [0x02, 0x00, 0x00],
206 )
207 .await?;
208
209 Ok(())
210 }
211
212 /// Collects information about all paired devices by calling
213 /// [`Self::trigger_device_arrival`] and collecting incoming
214 /// [`Event::DeviceConnection`] events.
215 ///
216 /// Check [`Self::get_notification_state`] first to make sure that
217 /// [`NotificationState::wireless_notifications`] is enabled.
218 pub async fn collect_paired_devices(&self) -> Result<Vec<DeviceConnection>, ReceiverError> {
219 // The idea here is that, when triggering fake device arrival notifications, the
220 // receiver will send the register write confirmation message only AFTER sending
221 // all arrival notifications.
222 // So we will trigger device arrival notifications and continue collecting those
223 // until the original future has completed.
224
225 let mut devices = vec![];
226
227 let rx = self.listen();
228 let fin = self.trigger_device_arrival().fuse();
229 pin_mut!(fin);
230
231 loop {
232 select! {
233 _ = fin => break,
234 res = rx.recv().fuse() => {
235 let Ok(Event::DeviceConnection(connection)) = res else {
236 continue;
237 };
238
239 devices.push(connection);
240 }
241 }
242 }
243
244 Ok(devices)
245 }
246
247 /// Retrieves the unique ID of the receiver. This is not the same as the
248 /// serial number.
249 pub async fn get_unique_id(&self) -> Result<String, ReceiverError> {
250 let response = self
251 .chan
252 .read_long_register(RECEIVER_DEVICE_INDEX, Register::UniqueId.into(), [0u8; 3])
253 .await?;
254
255 // When decoding the last 8 bytes of the response to their ASCII representation
256 // we seem to get a valid hex string representing 4 bytes of data.
257 // Interpreting this hex string as little endian we seem to get the same decimal
258 // value the Options+ software calls `udid` (unique device identifier?). I am
259 // not sure what this is about and it may be a (major) coincidence that these
260 // values match for my receiver, but it could be worth keeping this in mind.
261
262 // I have no clue how to retrieve the serial number of the receiver.
263
264 Ok(str::from_utf8(&response)
265 .map_err(|_| Hidpp10Error::UnsupportedResponse)?
266 .to_string())
267 }
268
269 /// Provides the pairing information of a specific paired device by its
270 /// index.
271 pub async fn get_device_pairing_information(
272 &self,
273 device_index: u8,
274 ) -> Result<DevicePairingInformation, ReceiverError> {
275 let response = self
276 .chan
277 .read_long_sub_register(
278 RECEIVER_DEVICE_INDEX,
279 Register::ReceiverInfo.into(),
280 u8::from(InfoSubRegister::DevicePairingInformation) + (device_index & 0x0f),
281 [0x00, 0x00],
282 )
283 .await?;
284
285 Ok(DevicePairingInformation {
286 wpid: u16::from_le_bytes([response[2], response[3]]),
287 // Kind is identity-only: an unrecognised nibble folds to
288 // `Unknown` instead of failing the whole pairing-info read.
289 kind: DeviceKind::from(response[1] & 0x0f),
290 encrypted: response[1] & (1 << 5) != 0,
291 online: response[1] & (1 << 6) == 0,
292 unit_id: [response[4], response[5], response[6], response[7]],
293 })
294 }
295
296 /// Provides the codename of a specific paired device by its index.
297 pub async fn get_device_codename(&self, device_index: u8) -> Result<String, ReceiverError> {
298 // For device names longer than 13 characters this may need to be called
299 // multiple times with different parameters. I don't have a device with
300 // such a name to be able to test this.
301
302 let response = self
303 .chan
304 .read_long_sub_register(
305 RECEIVER_DEVICE_INDEX,
306 Register::ReceiverInfo.into(),
307 u8::from(InfoSubRegister::DeviceCodename) + (device_index & 0x0f),
308 [0x01, 0x00],
309 )
310 .await?;
311
312 Ok(parse_codename(&response)
313 .ok_or(Hidpp10Error::UnsupportedResponse)?
314 .to_string())
315 }
316
317 /// Unpairs a device from the receiver by its index.
318 pub async fn unpair_device(&self, device_index: u8) -> Result<(), ReceiverError> {
319 let mut payload = [0u8; 16];
320 payload[0] = 0x03;
321 payload[1] = device_index;
322
323 self.chan
324 .write_long_register(RECEIVER_DEVICE_INDEX, Register::Pairing.into(), payload)
325 .await?;
326
327 Ok(())
328 }
329
330 /// Starts the pairing process for a new device.
331 ///
332 /// The required `address` and `authentication` values are usually
333 /// discovered from the [`Event::DeviceDiscoveryDeviceDetails`] event which
334 /// is emitted regularly when actively discovering available devices
335 /// ([`Self::discover_devices`]).
336 ///
337 /// `entropy` specifies how complex the authentication passkey should be.
338 /// For mice, this defines the amount of keypresses (left or right) the user
339 /// has to perform. Not all values seem to be supported.
340 pub async fn pair_device(
341 &self,
342 slot: u8,
343 address: [u8; 6],
344 authentication: u8,
345 entropy: u8,
346 ) -> Result<(), ReceiverError> {
347 let mut payload = [0u8; 16];
348 payload[0] = 0x01;
349 payload[1] = slot;
350 payload[2..=7].copy_from_slice(&address);
351 payload[8] = authentication;
352 payload[9] = entropy;
353
354 self.chan
355 .write_long_register(RECEIVER_DEVICE_INDEX, Register::Pairing.into(), payload)
356 .await?;
357
358 Ok(())
359 }
360
361 /// Starts device discovery for `timeout` seconds ([`None`] = default, seems
362 /// to be 30s). The maximum supported value is 60s.
363 ///
364 /// While device discovery is enabled,
365 /// [`Event::DeviceDiscoveryDeviceDetails`] and
366 /// [`Event::DeviceDiscoveryDeviceName`] events are emitted for every
367 /// discovered device.
368 pub async fn discover_devices(&self, timeout: Option<u8>) -> Result<(), ReceiverError> {
369 self.chan
370 .write_register(
371 RECEIVER_DEVICE_INDEX,
372 Register::DeviceDiscovery.into(),
373 [timeout.unwrap_or(0x00), 0x01, 0x00],
374 )
375 .await?;
376
377 Ok(())
378 }
379
380 /// Cancels the device discovery process.
381 pub async fn cancel_device_discovery(&self) -> Result<(), ReceiverError> {
382 self.chan
383 .write_register(
384 RECEIVER_DEVICE_INDEX,
385 Register::DeviceDiscovery.into(),
386 [0x00, 0x02, 0x00],
387 )
388 .await?;
389
390 Ok(())
391 }
392}
393
394/// Extract the codename chunk from a `DeviceCodename` register read.
395///
396/// `response[2]` is the device-reported name length. A name longer than the
397/// 13 bytes one response carries is clamped to the chunk present (fetching
398/// the rest takes further reads with different parameters); a length byte
399/// pointing past the response must not panic. `None` for non-UTF-8 bytes.
400fn parse_codename(response: &[u8; 16]) -> Option<&str> {
401 let end = 3usize.saturating_add(usize::from(response[2]));
402 let raw = response.get(3..end.min(response.len()))?;
403 str::from_utf8(raw).ok()
404}
405
406/// Indicates which notifications are enabled and thus sent by the receiver.
407///
408/// This information can be queried using [`Receiver::get_notification_state`].
409#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Builder)]
410#[cfg_attr(feature = "serde", derive(serde::Serialize))]
411#[non_exhaustive]
412pub struct NotificationState {
413 /// Whether the receiver sends device arrival/removal notifications.
414 pub wireless_notifications: bool,
415}
416
417/// Represents information about a paired device.
418///
419/// This information can be queried using
420/// [`Receiver::get_device_pairing_information`].
421#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
422#[cfg_attr(feature = "serde", derive(serde::Serialize))]
423#[non_exhaustive]
424pub struct DevicePairingInformation {
425 /// Wireless product ID of the paired device.
426 pub wpid: u16,
427 /// Device kind reported by the receiver.
428 pub kind: DeviceKind,
429 /// Whether the link is encrypted.
430 pub encrypted: bool,
431 /// Whether the device is currently online.
432 pub online: bool,
433 /// Device unit ID.
434 pub unit_id: [u8; 4],
435}
436
437#[cfg(test)]
438mod tests {
439 use super::*;
440
441 #[test]
442 fn codename_with_oversized_length_clamps_to_available_chunk() {
443 let mut response = [0u8; 16];
444 response[2] = 200;
445 response[3..16].copy_from_slice(b"MX Anywhere 3");
446
447 assert_eq!(parse_codename(&response), Some("MX Anywhere 3"));
448 }
449
450 #[test]
451 fn codename_within_bounds_parses() {
452 let mut response = [0u8; 16];
453 response[2] = 5;
454 response[3..8].copy_from_slice(b"Casa!");
455
456 assert_eq!(parse_codename(&response), Some("Casa!"));
457 }
458
459 #[test]
460 fn codename_rejects_invalid_utf8() {
461 let mut response = [0u8; 16];
462 response[2] = 2;
463 response[3] = 0xff;
464 response[4] = 0xfe;
465
466 assert_eq!(parse_codename(&response), None);
467 }
468}