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_register(
278 RECEIVER_DEVICE_INDEX,
279 Register::ReceiverInfo.into(),
280 [
281 u8::from(InfoSubRegister::DevicePairingInformation) + (device_index & 0x0f),
282 0x00,
283 0x00,
284 ],
285 )
286 .await?;
287
288 Ok(DevicePairingInformation {
289 wpid: u16::from_le_bytes([response[2], response[3]]),
290 // Kind is identity-only: an unrecognised nibble folds to
291 // `Unknown` instead of failing the whole pairing-info read.
292 kind: DeviceKind::from(response[1] & 0x0f),
293 encrypted: response[1] & (1 << 5) != 0,
294 online: response[1] & (1 << 6) == 0,
295 unit_id: [response[4], response[5], response[6], response[7]],
296 })
297 }
298
299 /// Provides the codename of a specific paired device by its index.
300 pub async fn get_device_codename(&self, device_index: u8) -> Result<String, ReceiverError> {
301 // For device names longer than 13 characters this may need to be called
302 // multiple times with different parameters. I don't have a device with
303 // such a name to be able to test this.
304
305 let response = self
306 .chan
307 .read_long_register(
308 RECEIVER_DEVICE_INDEX,
309 Register::ReceiverInfo.into(),
310 [
311 u8::from(InfoSubRegister::DeviceCodename) + (device_index & 0x0f),
312 0x01,
313 0x00,
314 ],
315 )
316 .await?;
317
318 Ok(parse_codename(&response)
319 .ok_or(Hidpp10Error::UnsupportedResponse)?
320 .to_string())
321 }
322
323 /// Unpairs a device from the receiver by its index.
324 pub async fn unpair_device(&self, device_index: u8) -> Result<(), ReceiverError> {
325 let mut payload = [0u8; 16];
326 payload[0] = 0x03;
327 payload[1] = device_index;
328
329 self.chan
330 .write_long_register(RECEIVER_DEVICE_INDEX, Register::Pairing.into(), payload)
331 .await?;
332
333 Ok(())
334 }
335
336 /// Starts the pairing process for a new device.
337 ///
338 /// The required `address` and `authentication` values are usually
339 /// discovered from the [`Event::DeviceDiscoveryDeviceDetails`] event which
340 /// is emitted regularly when actively discovering available devices
341 /// ([`Self::discover_devices`]).
342 ///
343 /// `entropy` specifies how complex the authentication passkey should be.
344 /// For mice, this defines the amount of keypresses (left or right) the user
345 /// has to perform. Not all values seem to be supported.
346 pub async fn pair_device(
347 &self,
348 slot: u8,
349 address: [u8; 6],
350 authentication: u8,
351 entropy: u8,
352 ) -> Result<(), ReceiverError> {
353 let mut payload = [0u8; 16];
354 payload[0] = 0x01;
355 payload[1] = slot;
356 payload[2..=7].copy_from_slice(&address);
357 payload[8] = authentication;
358 payload[9] = entropy;
359
360 self.chan
361 .write_long_register(RECEIVER_DEVICE_INDEX, Register::Pairing.into(), payload)
362 .await?;
363
364 Ok(())
365 }
366
367 /// Starts device discovery for `timeout` seconds ([`None`] = default, seems
368 /// to be 30s). The maximum supported value is 60s.
369 ///
370 /// While device discovery is enabled,
371 /// [`Event::DeviceDiscoveryDeviceDetails`] and
372 /// [`Event::DeviceDiscoveryDeviceName`] events are emitted for every
373 /// discovered device.
374 pub async fn discover_devices(&self, timeout: Option<u8>) -> Result<(), ReceiverError> {
375 self.chan
376 .write_register(
377 RECEIVER_DEVICE_INDEX,
378 Register::DeviceDiscovery.into(),
379 [timeout.unwrap_or(0x00), 0x01, 0x00],
380 )
381 .await?;
382
383 Ok(())
384 }
385
386 /// Cancels the device discovery process.
387 pub async fn cancel_device_discovery(&self) -> Result<(), ReceiverError> {
388 self.chan
389 .write_register(
390 RECEIVER_DEVICE_INDEX,
391 Register::DeviceDiscovery.into(),
392 [0x00, 0x02, 0x00],
393 )
394 .await?;
395
396 Ok(())
397 }
398}
399
400/// Extract the codename chunk from a `DeviceCodename` register read.
401///
402/// `response[2]` is the device-reported name length. A name longer than the
403/// 13 bytes one response carries is clamped to the chunk present (fetching
404/// the rest takes further reads with different parameters); a length byte
405/// pointing past the response must not panic. `None` for non-UTF-8 bytes.
406fn parse_codename(response: &[u8; 16]) -> Option<&str> {
407 let end = 3usize.saturating_add(usize::from(response[2]));
408 let raw = response.get(3..end.min(response.len()))?;
409 str::from_utf8(raw).ok()
410}
411
412/// Indicates which notifications are enabled and thus sent by the receiver.
413///
414/// This information can be queried using [`Receiver::get_notification_state`].
415#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Builder)]
416#[cfg_attr(feature = "serde", derive(serde::Serialize))]
417#[non_exhaustive]
418pub struct NotificationState {
419 /// Whether the receiver sends device arrival/removal notifications.
420 pub wireless_notifications: bool,
421}
422
423/// Represents information about a paired device.
424///
425/// This information can be queried using
426/// [`Receiver::get_device_pairing_information`].
427#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
428#[cfg_attr(feature = "serde", derive(serde::Serialize))]
429#[non_exhaustive]
430pub struct DevicePairingInformation {
431 /// Wireless product ID of the paired device.
432 pub wpid: u16,
433 /// Device kind reported by the receiver.
434 pub kind: DeviceKind,
435 /// Whether the link is encrypted.
436 pub encrypted: bool,
437 /// Whether the device is currently online.
438 pub online: bool,
439 /// Device unit ID.
440 pub unit_id: [u8; 4],
441}
442
443#[cfg(test)]
444mod tests {
445 use super::*;
446
447 #[test]
448 fn codename_with_oversized_length_clamps_to_available_chunk() {
449 let mut response = [0u8; 16];
450 response[2] = 200;
451 response[3..16].copy_from_slice(b"MX Anywhere 3");
452
453 assert_eq!(parse_codename(&response), Some("MX Anywhere 3"));
454 }
455
456 #[test]
457 fn codename_within_bounds_parses() {
458 let mut response = [0u8; 16];
459 response[2] = 5;
460 response[3..8].copy_from_slice(b"Casa!");
461
462 assert_eq!(parse_codename(&response), Some("Casa!"));
463 }
464
465 #[test]
466 fn codename_rejects_invalid_utf8() {
467 let mut response = [0u8; 16];
468 response[2] = 2;
469 response[3] = 0xff;
470 response[4] = 0xfe;
471
472 assert_eq!(parse_codename(&response), None);
473 }
474}