openlogi_device/reprog_controls.rs
1//! HID++ `ReprogControlsV4` (feature `0x1b04`) — temporary control diversion
2//! and raw-XY reporting, the mechanism behind MX-line reprogrammable controls.
3//!
4//! The full protocol wrapper lives in `openlogi-hidpp`; this module keeps the
5//! raw-CID accessor the capture and host-switch sessions drive: `getCount` /
6//! `getCtrlIdInfo` (walk the control table and confirm a control can divert or
7//! report raw XY), `getCidReporting` (snapshot a control before arming it) and
8//! `setCidReporting` (turn diversion on or off, and hand the control back).
9//! While a control is diverted with raw-XY reporting, the device emits two
10//! unsolicited events, decoded by [`decode_event`]:
11//!
12//! - function `0` `divertedButtonsEvent` — up to four currently-pressed CIDs.
13//! - function `1` `rawXYEvent` — signed `dx`/`dy` while a raw-XY control is held.
14//!
15//! Wire formats cross-checked against Solaar's `hidpp20.py` and
16//! `notifications.py`.
17
18use std::sync::Arc;
19
20use hidpp::{
21 channel::HidppChannel,
22 feature::{CreatableFeature, reprog_controls as hidpp_reprog},
23 protocol::v20::Hidpp20Error,
24};
25
26mod event;
27
28pub use event::{RawControlEvent, decode_event};
29pub use hidpp_reprog::{
30 AnalyticsKeyEvent, CidFlags, CidInfo, CidReporting, CidReportingChange, CidReportingChangeEcho,
31 ControlId, GroupMask, RawWheelResolution, ReprogControlsCapabilities, ReprogControlsEvent,
32 TaskId, decode_event as decode_full_event,
33};
34pub use hidpp_reprog::{control_ids, task_ids};
35
36/// `ReprogControlsV4` HID++ feature ID.
37pub const FEATURE_ID: u16 = 0x1b04;
38
39/// Control ID of the MX-line dedicated gesture button (`Mouse_Gesture_Button`,
40/// Logitech "App_Switch_Gesture").
41///
42/// MX Master 4 also has a separate Haptic Sense Panel in the thumb area; that
43/// panel is [`HAPTIC_PANEL_CID`], not this CID.
44pub const GESTURE_BUTTON_CID: u16 = 0x00c3;
45
46/// Control ID of the MX Master 4 Haptic Sense Panel — the touch-sensitive
47/// thumb rest that replaces the dedicated gesture button on that model.
48///
49/// Reverse-engineered on real hardware (MX Master 4, Bolt receiver); not in
50/// the published `0x1b04` control-ID lists. Press/release arrive as
51/// `divertedButtonsEvent` with this CID, and while touched the panel streams
52/// relative raw-XY at ~125 Hz exactly like the dedicated gesture button —
53/// except that the first raw-XY sample after contact is a large position jump
54/// that must be discarded before feeding a swipe accumulator.
55///
56/// The typed source of truth is [`control_ids::HAPTIC_PANEL`].
57pub const HAPTIC_PANEL_CID: u16 = control_ids::HAPTIC_PANEL.0;
58
59/// Control IDs of the "DPI / ModeShift" button family. Whichever a device
60/// exposes (and can divert) is captured and mapped to
61/// [`ButtonId::DpiToggle`](openlogi_core::binding::ButtonId::DpiToggle): the MX
62/// wheel-mode "Smart Shift" button, plus the dedicated "DPI Change" / "DPI
63/// Switch" buttons on other models. Values from the `0x1b04` control-ID list,
64/// cross-checked against Solaar `special_keys.py`.
65pub const DPI_MODE_SHIFT_CIDS: [u16; 3] = [0x00c4, 0x00ed, 0x00fd];
66
67/// Control IDs of the Back button family. MX Vertical and similar devices
68/// report Back via HID++ `0x1b04` rather than a standard OS mouse button,
69/// so macOS never translates them into `OtherMouseDown` events. Whichever a
70/// device exposes (and can divert) is captured and mapped to
71/// [`ButtonId::Back`](openlogi_core::binding::ButtonId::Back).
72///
73/// Known CIDs (from the `0x1b04` control-ID list / Solaar `special_keys.py`):
74/// - `0x0053` — Back (classic mouse CID, used by MX Vertical)
75/// - `0x00BD` — MultiPlatform Back
76/// - `0x00CE` — Multiplatform Back (alternate)
77/// - `0x00DB` — Back (generic)
78pub const BACK_CIDS: [u16; 4] = [0x0053, 0x00BD, 0x00CE, 0x00DB];
79
80/// Control IDs of the Forward button family. Counterpart to [`BACK_CIDS`]:
81/// captured and mapped to
82/// [`ButtonId::Forward`](openlogi_core::binding::ButtonId::Forward).
83///
84/// Known CIDs:
85/// - `0x0056` — Forward (classic mouse CID, used by MX Vertical)
86/// - `0x00CF` — Multiplatform Forward
87pub const FORWARD_CIDS: [u16; 2] = [0x0056, 0x00CF];
88
89/// Identity and capabilities of one reprogrammable control, as returned by
90/// `getCtrlIdInfo`.
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub struct CtrlIdInfo {
93 /// Control ID — stable across firmware (e.g. [`GESTURE_BUTTON_CID`]).
94 pub cid: u16,
95 /// Task ID — the control's default on-device action.
96 pub task_id: u16,
97 /// `KeyFlag` capability bitfield (response bytes 4 and 8 combined).
98 pub flags: u16,
99}
100
101impl CtrlIdInfo {
102 /// Typed view of the legacy raw [`Self::flags`] field.
103 #[must_use]
104 pub fn typed_flags(self) -> CidFlags {
105 CidFlags::from_bits_retain(self.flags)
106 }
107
108 /// Whether the control can be temporarily diverted to HID++ events.
109 #[must_use]
110 pub fn is_divertable(self) -> bool {
111 self.typed_flags().is_divertable()
112 }
113
114 /// Whether the control can report raw XY movement while held — required to
115 /// decode a swipe into a direction.
116 #[must_use]
117 pub fn supports_raw_xy(self) -> bool {
118 self.typed_flags().supports_raw_xy()
119 }
120
121 /// Whether the control can report analytics key events.
122 #[must_use]
123 pub fn supports_analytics_events(self) -> bool {
124 self.typed_flags().supports_analytics_key_events()
125 }
126}
127
128impl From<CidInfo> for CtrlIdInfo {
129 fn from(info: CidInfo) -> Self {
130 Self {
131 cid: info.cid.into(),
132 task_id: info.task_id.0,
133 flags: info.flags.raw(),
134 }
135 }
136}
137
138/// `ReprogControlsV4` accessor bound to one device + resolved feature index.
139///
140/// Construct with the feature index obtained from the device's root feature
141/// (`get_feature(`[`FEATURE_ID`]`)`), then call the functions below. Cheap to
142/// clone (an `Arc` plus the feature index).
143#[derive(Clone)]
144pub struct ReprogControlsV4 {
145 inner: Arc<hidpp_reprog::ReprogControlsFeature>,
146 feature_index: u8,
147}
148
149impl ReprogControlsV4 {
150 /// Bind the feature to `(device_index, feature_index)` on `chan`.
151 #[must_use]
152 pub fn new(chan: Arc<HidppChannel>, device_index: u8, feature_index: u8) -> Self {
153 Self {
154 inner: Arc::new(hidpp_reprog::ReprogControlsFeature::new(
155 chan,
156 device_index,
157 feature_index,
158 )),
159 feature_index,
160 }
161 }
162
163 /// The feature index this accessor talks to — used to match unsolicited
164 /// events in [`decode_event`].
165 #[must_use]
166 pub fn feature_index(&self) -> u8 {
167 self.feature_index
168 }
169
170 /// Number of reprogrammable controls the device exposes.
171 pub async fn get_count(&self) -> Result<u8, Hidpp20Error> {
172 self.inner.get_count().await
173 }
174
175 /// Identity + capabilities of the control at `index` (`0..get_count`).
176 pub async fn get_cid_info(&self, index: u8) -> Result<CidInfo, Hidpp20Error> {
177 self.inner.get_cid_info(index).await
178 }
179
180 /// Compatibility projection of [`Self::get_cid_info`].
181 pub async fn get_ctrl_id_info(&self, index: u8) -> Result<CtrlIdInfo, Hidpp20Error> {
182 Ok(self.get_cid_info(index).await?.into())
183 }
184
185 /// Current reporting/remapping state for `cid`.
186 pub async fn get_cid_reporting(&self, cid: u16) -> Result<CidReporting, Hidpp20Error> {
187 self.inner.get_cid_reporting(ControlId(cid)).await
188 }
189
190 /// Apply the full `setCidReporting` packet.
191 pub async fn set_cid_reporting_full(
192 &self,
193 cid: u16,
194 change: CidReportingChange,
195 ) -> Result<CidReportingChangeEcho, Hidpp20Error> {
196 self.inner.set_cid_reporting(ControlId(cid), change).await
197 }
198
199 /// Divert `cid`'s reports to software. Raw XY stays off and remapping is
200 /// left untouched; the device then emits [`RawControlEvent`]s on this
201 /// feature index.
202 pub async fn divert_cid(&self, cid: u16) -> Result<(), Hidpp20Error> {
203 self.set_cid_reporting_full(
204 cid,
205 hidpp_reprog::CidReportingChange {
206 diverted: Some(true),
207 raw_xy: Some(false),
208 ..Default::default()
209 },
210 )
211 .await?;
212 Ok(())
213 }
214}