Skip to main content

hidpp/
lib.rs

1//! An implementation of Logitech's HID++ protocol.
2//!
3//! Many of Logitech's more modern peripheral devices (mice, keyboards etc.)
4//! support advanced features improving the user experience. These include, but
5//! are not limited to, things like:
6//!
7//! - scroll wheels dynamically switching between ratchet and freespin mode ([SmartShift](https://support.logi.com/hc/en-us/articles/360052340194-What-is-SmartShift-on-MX-Anywhere-3))
8//! - [mouse gestures](https://support.logi.com/hc/en-us/articles/360023359813-How-to-customize-mouse-buttons-with-Logitech-Options#gesture)
9//! - custom actions for specific mouse buttons
10//! - several customizability options for keyboards, audio devices and touchpads
11//!
12//! All of these features can be managed using their (more or less) proprietary
13//! HID++-protocol which extends standard [HID](https://en.wikipedia.org/wiki/Human_interface_device).
14//!
15//! Logitech kindly provided a [public Google Drive folder](https://drive.google.com/drive/folders/0BxbRzx7vEV7eWmgwazJ3NUFfQ28)
16//! with a lot of documentation on HID++ and several device features. These
17//! documents were heavily used during the development of this crate.
18//!
19//! I also made use of the excellent work already done by the
20//! [Solaar](https://github.com/pwr-Solaar/Solaar) team to grow my
21//! understanding of how things work. It's a great project perfectly usable to
22//! configure Logitech devices on Linux, so definitely check it out if you are
23//! looking for something like this.
24//!
25//! # Quickstart
26//!
27//! ## Establish HID communication
28//!
29//! This crate implements the HID++ protocol, not the underlying [HID](https://en.wikipedia.org/wiki/Human_interface_device)
30//! communication, which is left to an external crate of your choice.
31//! The trait used for bridging your HID implementation to this crate is
32//! [`channel::RawHidChannel`], so make sure to provide an implementation for
33//! it. The trait defines async methods using [`mod@async_trait`], which is
34//! re-exported for annotating your implementing type.
35//!
36//! The crate primarily used while testing and developing is [`async-hid`](https://crates.io/crates/async-hid).
37//! Providing an implementation for this crate behind a feature gate is
38//! planned and will be implemented once [retrieving the raw report descriptor](https://github.com/sidit77/async-hid/issues/17)
39//! is supported.
40//!
41//! ## Initialize HID++ communication
42//!
43//! Once you have a working implementation of [`channel::RawHidChannel`], you
44//! can start by creating a [`channel::HidppChannel`]:
45//!
46//! ```ignore
47//! use std::sync::Arc;
48//!
49//! use hidpp::{
50//!     channel::{HidppChannel, SwIdPolicy},
51//!     device::Device,
52//!     feature::{
53//!        CreatableFeature,
54//!        EmittingFeature,
55//!        feature_set::v0::FeatureSetFeatureV0,
56//!        thumbwheel::v0::{ThumbwheelEvent, ThumbwheelFeatureV0, ThumbwheelReportingMode},
57//!    },
58//!    nibble::U4,
59//!    receiver::{self, Receiver, bolt::BoltEvent},
60//! };
61//!
62//! // First, we will create the HID++ channel.
63//! // This function will return `ChannelError::HidppNotSupported`
64//! // if the passed HID channel does not support HID++.
65//! let mut channel = HidppChannel::from_raw_channel(my_hid_channel)
66//!     .await
67//!     .expect("could not establish HID++ communication");
68//!
69//! // HID++2.0 includes an arbitrary "software ID" in every message, used to
70//! // map responses back to requests. The channel's policy decides it per
71//! // request: `Fixed` (the default, id 1), `Rotating` (one id per request),
72//! // or `Leased` (a process-unique fixed id handed back on drop, for
73//! // concurrent opens of one node). Decide the policy while the channel is
74//! // still exclusively owned, then share it.
75//! channel.set_sw_id_policy(SwIdPolicy::rotating());
76//! let channel = Arc::new(channel);
77//!
78//! // If a wireless receiver is handling the HID++ communication,
79//! // we can detect it.
80//! let receiver = receiver::detect(Arc::clone(&channel)).expect("no receiver
81//! was found");
82//!
83//! // Assuming we have a Bolt receiver, we will now detect all connected
84//! devices. let Receiver::Bolt(bolt) = receiver else {
85//!     panic!("no Bolt receiver");
86//! };
87//! tokio::spawn({
88//!     let rx = bolt.listen();
89//!
90//!     async move {
91//!         while let Ok(BoltEvent::DeviceConnection(event)) = rx.recv() {
92//!             println!("Paired device found: {:x?}", event);
93//!         }
94//!     }
95//! });
96//! bolt.trigger_device_arrival()
97//!     .await
98//!     .expect("could not trigger device arrival notification");
99//!
100//! // Let's say we found a device with the index 0x02 using this enumeration.
101//! We can now initialize it:
102//! let mut device = Device::new(Arc::clone(&channel), 0x02)
103//!     .await
104//!     .expect("could not initialize device");
105//!
106//! // The device is a HID++2.0 one, meaning it supports so-called HID++2.0
107//! // features. Every device supports the standardized `IRoot` feature, which
108//! // we can access like this:
109//! let root = device.root();
110//! assert_eq!(0x2, root.ping(0x2).await.unwrap());
111//!
112//! // Additional features are accessed by their feature index in the
113//! // device-internal feature map, not by their globally unique feature ID.
114//! // The root feature also supports looking up a specific feature by its ID.
115//! // The resulting value will contain some information about the feature,
116//! // including its index:
117//! let info = root
118//!     .get_feature(FeatureSetFeatureV0::ID)
119//!     .await
120//!     .expect("could not look up feature")
121//!     .expect("FeatureSet feature is not supported");
122//!
123//! // As there are a lot of possible features and a given device only supports
124//! // a small subset of these, looking up every single feature ID using this
125//! // technique is not practicable. That's why the `IFeatureSet` feature can be
126//! // used to enumerate over all supported features, but only if this feature
127//! // itself is supported by the device.
128//! let infos = device
129//!     .enumerate_features()
130//!     .await
131//!     .expect("could not look up features")
132//!     .expect("FeatureSet feature is not supported");
133//!
134//! // This crate provides Rust implementations for many HID++2.0 features. A
135//! // registry in the `hidpp::feature::registry` module maintains a list of all
136//! // known features and, if provided, a link to its implementation. The
137//! // `enumerate_features` function we just called automatically registers
138//! // these implementations for our device and we can now access them like this:
139//! let thumbwheel = device
140//!     .get_feature::<ThumbwheelFeatureV0>()
141//!     .expect("Thumbwheel feature is not supported");
142//! thumbwheel
143//!     .set_thumbwheel_reporting(ThumbwheelReportingMode::Diverted, false)
144//!     .await
145//!     .expect("could not divert thumbwheel");
146//! ```
147
148#![deny(missing_docs)]
149#![deny(rustdoc::bare_urls)]
150#![deny(rustdoc::broken_intra_doc_links)]
151
152pub use async_trait::async_trait;
153
154pub mod channel;
155pub mod device;
156mod emitter;
157pub mod feature;
158pub mod nibble;
159pub mod protocol;
160pub mod receiver;
161mod sync;