Skip to main content

hidpp/feature/
reprog_controls.rs

1//! Implements `SpecialKeysMseButtons` / `ReprogControlsV4` (feature `0x1b04`).
2//!
3//! Logitech's v6 document names this feature `SpecialKeysMseButtons`: it
4//! enumerates physical and virtual controls, lets host software divert or remap
5//! them, and emits notifications for diverted buttons, raw XY, analytics key
6//! events, and raw wheel movement.
7
8use std::sync::Arc;
9
10use crate::{
11    channel::{HidppChannel, MessageListenerGuard},
12    event::EventEmitter,
13    feature::{CreatableFeature, EmittingFeature, Feature, FeatureEndpoint, event_payload},
14    protocol::v20::Hidpp20Error,
15};
16
17pub mod control_ids;
18mod event;
19pub mod task_ids;
20
21use event::decode_event_payload;
22pub use event::{AnalyticsKeyEvent, RawWheelResolution, ReprogControlsEvent, decode_event};
23
24/// Implements the `SpecialKeysMseButtons` / `0x1b04` feature.
25pub struct ReprogControlsFeature {
26    endpoint: FeatureEndpoint,
27    emitter: Arc<EventEmitter<ReprogControlsEvent>>,
28    _msg_listener: MessageListenerGuard,
29}
30
31impl CreatableFeature for ReprogControlsFeature {
32    const ID: u16 = 0x1b04;
33    const STARTING_VERSION: u8 = 0;
34
35    fn new(chan: Arc<HidppChannel>, device_index: u8, feature_index: u8) -> Self {
36        let emitter = Arc::new(EventEmitter::new());
37
38        let listener = chan.add_msg_listener_guarded({
39            let emitter = Arc::clone(&emitter);
40
41            move |raw, matched| {
42                let Some((func, payload)) =
43                    event_payload(raw, matched, device_index, feature_index)
44                else {
45                    return;
46                };
47                let Some(event) = decode_event_payload(func.to_lo(), &payload) else {
48                    return;
49                };
50                emitter.emit(event);
51            }
52        });
53
54        Self {
55            endpoint: FeatureEndpoint::new(chan, device_index, feature_index),
56            emitter,
57            _msg_listener: listener,
58        }
59    }
60}
61
62impl Feature for ReprogControlsFeature {}
63
64impl EmittingFeature<ReprogControlsEvent> for ReprogControlsFeature {
65    fn listen(&self) -> async_channel::Receiver<ReprogControlsEvent> {
66        self.emitter.create_receiver()
67    }
68}
69
70impl ReprogControlsFeature {
71    /// Returns the number of rows in the control ID table.
72    pub async fn get_count(&self) -> Result<u8, Hidpp20Error> {
73        Ok(self.endpoint.call(0, [0; 3]).await?.extend_payload()[0])
74    }
75
76    /// Returns one row from the control ID table.
77    pub async fn get_cid_info(&self, index: u8) -> Result<CidInfo, Hidpp20Error> {
78        let mut params = [0u8; 16];
79        params[0] = index;
80        let payload = self.endpoint.call_long(1, params).await?.extend_payload();
81        Ok(CidInfo::from_payload(payload))
82    }
83
84    /// Returns the current reporting/remapping state for `cid`.
85    pub async fn get_cid_reporting(&self, cid: ControlId) -> Result<CidReporting, Hidpp20Error> {
86        let [cid_hi, cid_lo] = cid.0.to_be_bytes();
87        let payload = self
88            .endpoint
89            .call(2, [cid_hi, cid_lo, 0])
90            .await?
91            .extend_payload();
92        Ok(CidReporting::from_payload(payload))
93    }
94
95    /// Applies reporting/remapping changes for `cid`.
96    ///
97    /// Optional boolean fields in [`CidReportingChange`] map to the corresponding
98    /// `*-valid` bit in Logitech's packet. Fields set to `None` are left
99    /// unchanged by the device. Remapping is carried as a value field rather
100    /// than a valid/value pair; `None` sends the documented `0` value.
101    pub async fn set_cid_reporting(
102        &self,
103        cid: ControlId,
104        change: CidReportingChange,
105    ) -> Result<CidReportingChangeEcho, Hidpp20Error> {
106        let payload = self
107            .endpoint
108            .call_long(3, change.to_payload(cid))
109            .await?
110            .extend_payload();
111        Ok(CidReportingChangeEcho::from_payload(payload))
112    }
113
114    /// Returns feature-level capabilities.
115    ///
116    /// This function exists on v6 devices. Older firmware may return
117    /// `InvalidFunctionId`.
118    pub async fn get_capabilities(&self) -> Result<ReprogControlsCapabilities, Hidpp20Error> {
119        let payload = self.endpoint.call(4, [0; 3]).await?.extend_payload();
120        Ok(ReprogControlsCapabilities {
121            reset_all_cid_report_settings: payload[0] & 1 != 0,
122        })
123    }
124
125    /// Resets all diverted or remapped control settings.
126    ///
127    /// This function exists on v6 devices that report
128    /// [`ReprogControlsCapabilities::reset_all_cid_report_settings`].
129    pub async fn reset_all_cid_report_settings(&self) -> Result<(), Hidpp20Error> {
130        self.endpoint.call(5, [0; 3]).await?;
131        Ok(())
132    }
133}
134
135/// A HID++ control ID.
136#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
137#[cfg_attr(feature = "serde", derive(serde::Serialize))]
138pub struct ControlId(pub u16);
139
140impl ControlId {
141    fn from_payload(bytes: &[u8]) -> Self {
142        Self(u16_from_be_payload(bytes))
143    }
144}
145
146impl From<u16> for ControlId {
147    fn from(value: u16) -> Self {
148        Self(value)
149    }
150}
151
152impl From<ControlId> for u16 {
153    fn from(value: ControlId) -> Self {
154        value.0
155    }
156}
157
158fn u16_from_be_payload(bytes: &[u8]) -> u16 {
159    u16::from_be_bytes(bytes.try_into().unwrap())
160}
161
162/// A HID++ task ID.
163#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
164#[cfg_attr(feature = "serde", derive(serde::Serialize))]
165pub struct TaskId(pub u16);
166
167impl From<u16> for TaskId {
168    fn from(value: u16) -> Self {
169        Self(value)
170    }
171}
172
173impl From<TaskId> for u16 {
174    fn from(value: TaskId) -> Self {
175        value.0
176    }
177}
178
179/// One `getCidInfo` row.
180#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
181#[cfg_attr(feature = "serde", derive(serde::Serialize))]
182pub struct CidInfo {
183    /// Control ID.
184    pub cid: ControlId,
185    /// Default task ID currently associated with the control.
186    pub task_id: TaskId,
187    /// Capability and classification flags.
188    pub flags: CidFlags,
189    /// Physical position value reported by the device.
190    pub position: u8,
191    /// Control group number.
192    pub group: u8,
193    /// Bit mask of groups this control belongs to.
194    pub group_mask: GroupMask,
195}
196
197impl CidInfo {
198    fn from_payload(payload: [u8; 16]) -> Self {
199        Self {
200            cid: ControlId::from_payload(&payload[0..=1]),
201            task_id: TaskId(u16_from_be_payload(&payload[2..=3])),
202            flags: CidFlags::from_bytes(payload[4], payload[8]),
203            position: payload[5],
204            group: payload[6],
205            group_mask: GroupMask(payload[7]),
206        }
207    }
208}
209
210bitflags::bitflags! {
211    /// Capability and classification flags for one control ID.
212    #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
213    #[cfg_attr(feature = "serde", derive(serde::Serialize))]
214    pub struct CidFlags: u16 {
215        /// Control belongs to a mouse/pointer device.
216        const MOUSE = 1 << 0;
217        /// Control is a keyboard function key.
218        const FUNCTION_KEY = 1 << 1;
219        /// Control is a hotkey.
220        const HOTKEY = 1 << 2;
221        /// Control toggles Fn behavior.
222        const FN_TOGGLE = 1 << 3;
223        /// Control can be reprogrammed.
224        const REPROGRAMMABLE = 1 << 4;
225        /// Control can be temporarily diverted to HID++ events.
226        const DIVERTABLE = 1 << 5;
227        /// Control can be persistently diverted.
228        const PERSISTENTLY_DIVERTABLE = 1 << 6;
229        /// Control is virtual rather than a physical input.
230        const VIRTUAL_CONTROL = 1 << 7;
231        /// Control supports raw XY reporting.
232        const RAW_XY = 1 << 8;
233        /// Control supports force raw XY reporting.
234        const FORCE_RAW_XY = 1 << 9;
235        /// Control supports analytics key events.
236        const ANALYTICS_KEY_EVENTS = 1 << 10;
237        /// Control supports raw wheel events.
238        const RAW_WHEEL = 1 << 11;
239    }
240}
241
242impl CidFlags {
243    fn from_bytes(primary: u8, additional: u8) -> Self {
244        Self::from_bits_retain(u16::from(primary) | (u16::from(additional) << 8))
245    }
246
247    /// Raw `flags` value used by older OpenLogi diagnostics: primary flags in
248    /// the low byte, additional flags in the high byte.
249    #[must_use]
250    pub fn raw(self) -> u16 {
251        self.bits()
252    }
253
254    /// Whether this is a mouse control.
255    #[must_use]
256    pub fn is_mouse(self) -> bool {
257        self.contains(Self::MOUSE)
258    }
259
260    /// Whether this control can be temporarily diverted to HID++ events.
261    #[must_use]
262    pub fn is_divertable(self) -> bool {
263        self.contains(Self::DIVERTABLE)
264    }
265
266    /// Whether this control can be persistently diverted.
267    #[must_use]
268    pub fn is_persistently_divertable(self) -> bool {
269        self.contains(Self::PERSISTENTLY_DIVERTABLE)
270    }
271
272    /// Whether this is a virtual control.
273    #[must_use]
274    pub fn is_virtual_control(self) -> bool {
275        self.contains(Self::VIRTUAL_CONTROL)
276    }
277
278    /// Whether this control can report raw XY movement while held.
279    #[must_use]
280    pub fn supports_raw_xy(self) -> bool {
281        self.contains(Self::RAW_XY)
282    }
283
284    /// Whether this control can report force raw XY movement while held.
285    #[must_use]
286    pub fn supports_force_raw_xy(self) -> bool {
287        self.contains(Self::FORCE_RAW_XY)
288    }
289
290    /// Whether this control can report analytics key events.
291    #[must_use]
292    pub fn supports_analytics_key_events(self) -> bool {
293        self.contains(Self::ANALYTICS_KEY_EVENTS)
294    }
295
296    /// Whether this control can report raw wheel events.
297    #[must_use]
298    pub fn supports_raw_wheel(self) -> bool {
299        self.contains(Self::RAW_WHEEL)
300    }
301}
302
303/// Group mask `g1..g8` from `getCidInfo`.
304#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
305#[cfg_attr(feature = "serde", derive(serde::Serialize))]
306pub struct GroupMask(pub u8);
307
308/// Current reporting/remapping state returned by `getCidReporting`.
309#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
310#[cfg_attr(feature = "serde", derive(serde::Serialize))]
311pub struct CidReporting {
312    /// Control ID whose reporting state was read.
313    pub cid: ControlId,
314    /// Whether temporary diversion is enabled.
315    pub diverted: bool,
316    /// Whether persistent diversion is enabled.
317    pub persistently_diverted: bool,
318    /// Whether force raw XY reporting is enabled.
319    pub force_raw_xy: bool,
320    /// Whether raw XY reporting is enabled.
321    pub raw_xy: bool,
322    /// Optional remapping target control ID.
323    pub remap: Option<ControlId>,
324    /// Whether analytics key events are enabled.
325    pub analytics_key_events: bool,
326    /// Whether raw wheel reporting is enabled.
327    pub raw_wheel: bool,
328}
329
330impl CidReporting {
331    fn from_payload(payload: [u8; 16]) -> Self {
332        let remap = ControlId::from_payload(&payload[3..=4]);
333        Self {
334            cid: ControlId::from_payload(&payload[0..=1]),
335            diverted: payload[2] & (1 << 0) != 0,
336            persistently_diverted: payload[2] & (1 << 2) != 0,
337            raw_xy: payload[2] & (1 << 4) != 0,
338            force_raw_xy: payload[2] & (1 << 6) != 0,
339            remap: (remap.0 != 0).then_some(remap),
340            analytics_key_events: payload[5] & (1 << 0) != 0,
341            raw_wheel: payload[5] & (1 << 2) != 0,
342        }
343    }
344}
345
346/// Changes for `setCidReporting`.
347///
348/// For boolean fields, `None` means "leave unchanged". Remapping is encoded as
349/// the packet's value field and defaults to the documented `0` value.
350#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
351#[cfg_attr(feature = "serde", derive(serde::Serialize))]
352pub struct CidReportingChange {
353    /// New temporary diversion state, or `None` to leave unchanged.
354    pub diverted: Option<bool>,
355    /// New persistent diversion state, or `None` to leave unchanged.
356    pub persistently_diverted: Option<bool>,
357    /// New force raw XY state, or `None` to leave unchanged.
358    pub force_raw_xy: Option<bool>,
359    /// New raw XY state, or `None` to leave unchanged.
360    pub raw_xy: Option<bool>,
361    /// Remaps to another control ID. `None` sends the documented `0` value,
362    /// which represents no persistent remapping.
363    pub remap: Option<ControlId>,
364    /// New analytics key event state, or `None` to leave unchanged.
365    pub analytics_key_events: Option<bool>,
366    /// New raw wheel state, or `None` to leave unchanged.
367    pub raw_wheel: Option<bool>,
368}
369
370impl CidReportingChange {
371    /// Change only the temporary diverted/raw-XY bits.
372    #[must_use]
373    pub fn temporary_diversion(diverted: bool, raw_xy: bool) -> Self {
374        Self {
375            diverted: Some(diverted),
376            raw_xy: Some(raw_xy),
377            ..Self::default()
378        }
379    }
380
381    fn to_payload(self, cid: ControlId) -> [u8; 16] {
382        let mut payload = [0u8; 16];
383        let [cid_hi, cid_lo] = cid.0.to_be_bytes();
384        payload[0] = cid_hi;
385        payload[1] = cid_lo;
386
387        if let Some(value) = self.diverted {
388            payload[2] |= 1 << 1;
389            payload[2] |= u8::from(value);
390        }
391        if let Some(value) = self.persistently_diverted {
392            payload[2] |= 1 << 3;
393            payload[2] |= u8::from(value) << 2;
394        }
395        if let Some(value) = self.raw_xy {
396            payload[2] |= 1 << 5;
397            payload[2] |= u8::from(value) << 4;
398        }
399        if let Some(value) = self.force_raw_xy {
400            payload[2] |= 1 << 7;
401            payload[2] |= u8::from(value) << 6;
402        }
403        if let Some(remap) = self.remap {
404            let [remap_hi, remap_lo] = remap.0.to_be_bytes();
405            payload[3] = remap_hi;
406            payload[4] = remap_lo;
407        }
408        if let Some(value) = self.analytics_key_events {
409            payload[5] |= 1 << 1;
410            payload[5] |= u8::from(value);
411        }
412        if let Some(value) = self.raw_wheel {
413            payload[5] |= 1 << 3;
414            payload[5] |= u8::from(value) << 2;
415        }
416
417        payload
418    }
419}
420
421/// Echo returned by `setCidReporting`.
422#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
423#[cfg_attr(feature = "serde", derive(serde::Serialize))]
424pub struct CidReportingChangeEcho {
425    /// Control ID whose reporting state was changed.
426    pub cid: ControlId,
427    /// Echoed temporary diversion state when changed.
428    pub diverted: Option<bool>,
429    /// Echoed persistent diversion state when changed.
430    pub persistently_diverted: Option<bool>,
431    /// Echoed force raw XY state when changed.
432    pub force_raw_xy: Option<bool>,
433    /// Echoed raw XY state when changed.
434    pub raw_xy: Option<bool>,
435    /// Echoed remapping target when present.
436    pub remap: Option<ControlId>,
437    /// Echoed analytics key event state when changed.
438    pub analytics_key_events: Option<bool>,
439    /// Echoed raw wheel state when changed.
440    pub raw_wheel: Option<bool>,
441}
442
443impl CidReportingChangeEcho {
444    fn from_payload(payload: [u8; 16]) -> Self {
445        let remap = ControlId::from_payload(&payload[3..=4]);
446        Self {
447            cid: ControlId::from_payload(&payload[0..=1]),
448            diverted: (payload[2] & (1 << 1) != 0).then_some(payload[2] & (1 << 0) != 0),
449            persistently_diverted: (payload[2] & (1 << 3) != 0)
450                .then_some(payload[2] & (1 << 2) != 0),
451            raw_xy: (payload[2] & (1 << 5) != 0).then_some(payload[2] & (1 << 4) != 0),
452            force_raw_xy: (payload[2] & (1 << 7) != 0).then_some(payload[2] & (1 << 6) != 0),
453            remap: (remap.0 != 0).then_some(remap),
454            analytics_key_events: (payload[5] & (1 << 1) != 0)
455                .then_some(payload[5] & (1 << 0) != 0),
456            raw_wheel: (payload[5] & (1 << 3) != 0).then_some(payload[5] & (1 << 2) != 0),
457        }
458    }
459}
460
461/// Feature-level capabilities returned by `getCapabilities` on v6 devices.
462#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
463#[cfg_attr(feature = "serde", derive(serde::Serialize))]
464pub struct ReprogControlsCapabilities {
465    /// Whether `resetAllCidReportSettings` is supported.
466    pub reset_all_cid_report_settings: bool,
467}
468
469#[cfg(test)]
470mod tests;