Skip to main content

hidpp/feature/
persistent_remappable_action.rs

1//! Implements the `PersistentRemappableAction` feature (ID `0x1c00`) that
2//! persistently remaps a device control to a different HID action.
3//!
4//! Controls are identified by the same [`ControlId`]s as
5//! [`ReprogControls`](super::reprog_controls) (`0x1b04`); when both features are
6//! present and `0x1b04` diverts a control, that takes precedence over a
7//! persistent remap here.
8
9#[cfg(test)]
10mod tests;
11
12use num_enum::{IntoPrimitive, TryFromPrimitive};
13use openlogi_hidpp_derive::Feature;
14
15use crate::{
16    feature::{FeatureEndpoint, hosts_info::HostIndex, reprog_controls::ControlId},
17    protocol::v20::Hidpp20Error,
18};
19
20bitflags::bitflags! {
21    /// What HID outputs a device's persistent remapping can produce, from
22    /// [`get_feature_info`](PersistentRemappableActionFeature::get_feature_info).
23    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
24    #[cfg_attr(feature = "serde", derive(serde::Serialize))]
25    pub struct RemappableCapabilities: u16 {
26        /// Can send keyboard/keypad keys.
27        const KEYBOARD_REPORT = 1 << 0;
28        /// Can send mouse buttons.
29        const MOUSE_BUTTONS = 1 << 1;
30        /// Can send mouse X displacement.
31        const X_DISPLACEMENT = 1 << 2;
32        /// Can send mouse Y displacement.
33        const Y_DISPLACEMENT = 1 << 3;
34        /// Can send vertical roller increments.
35        const VERTICAL_ROLLER = 1 << 4;
36        /// Can send horizontal roller (AC pan) increments.
37        const HORIZONTAL_ROLLER = 1 << 5;
38        /// Can send consumer-control codes.
39        const CONSUMER_CONTROL = 1 << 6;
40        /// Can execute internal functions.
41        const INTERNAL_FUNCTION = 1 << 7;
42        /// Can send power keys.
43        const POWER_KEY = 1 << 8;
44    }
45}
46
47bitflags::bitflags! {
48    /// Standard keyboard modifier keys for a remapped keyboard action.
49    ///
50    /// Modifiers only apply to keyboard reports.
51    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
52    #[cfg_attr(feature = "serde", derive(serde::Serialize))]
53    pub struct ModifierMask: u8 {
54        /// Left Control.
55        const LEFT_CTRL = 1 << 0;
56        /// Left Shift.
57        const LEFT_SHIFT = 1 << 1;
58        /// Left Alt.
59        const LEFT_ALT = 1 << 2;
60        /// Left GUI (Win/Command).
61        const LEFT_GUI = 1 << 3;
62        /// Right Control.
63        const RIGHT_CTRL = 1 << 4;
64        /// Right Shift.
65        const RIGHT_SHIFT = 1 << 5;
66        /// Right Alt.
67        const RIGHT_ALT = 1 << 6;
68        /// Right GUI (Win/Command).
69        const RIGHT_GUI = 1 << 7;
70    }
71}
72
73bitflags::bitflags! {
74    /// A set of host slots for
75    /// [`reset_to_factory_settings`](PersistentRemappableActionFeature::reset_to_factory_settings).
76    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
77    #[cfg_attr(feature = "serde", derive(serde::Serialize))]
78    pub struct HostMask: u8 {
79        /// Host 1.
80        const HOST_1 = 1 << 0;
81        /// Host 2.
82        const HOST_2 = 1 << 1;
83        /// Host 3.
84        const HOST_3 = 1 << 2;
85    }
86}
87
88/// The action a control performs when triggered.
89#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
90#[cfg_attr(feature = "serde", derive(serde::Serialize))]
91#[non_exhaustive]
92#[repr(u8)]
93pub enum ActionId {
94    /// Send a keyboard/keypad report (HID usage page 7).
95    SendKeyboard = 0x01,
96    /// Send a mouse-button report (usage page 9).
97    SendMouseButton = 0x02,
98    /// Send mouse X displacement (usage page 1, code 0x30).
99    SendXDisplacement = 0x03,
100    /// Send mouse Y displacement (usage page 1, code 0x31).
101    SendYDisplacement = 0x04,
102    /// Send vertical roller/wheel displacement (usage page 1, code 0x38).
103    SendVerticalRoller = 0x05,
104    /// Send horizontal roller / AC pan displacement (usage page 12, code 0x0238).
105    SendHorizontalRoller = 0x06,
106    /// Send a consumer-control report (usage page 12).
107    SendConsumerControl = 0x07,
108    /// Execute an internal function (the value is the function index).
109    ExecuteInternalFunction = 0x08,
110    /// Send a power-key report (usage page 1).
111    SendPowerKey = 0x09,
112}
113
114/// Control-table sizing from
115/// [`get_count`](PersistentRemappableActionFeature::get_count).
116#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
117#[cfg_attr(feature = "serde", derive(serde::Serialize))]
118#[non_exhaustive]
119pub struct RemappableInfo {
120    /// Number of control IDs in the table.
121    pub count: u8,
122    /// Number of hosts the device supports.
123    pub host_count: u8,
124}
125
126/// The action mapped to a control, from
127/// [`get_persistent_action`](PersistentRemappableActionFeature::get_persistent_action).
128#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
129#[cfg_attr(feature = "serde", derive(serde::Serialize))]
130#[non_exhaustive]
131pub struct PersistentAction {
132    /// The control the action belongs to.
133    pub cid: ControlId,
134    /// The host slot the mapping applies to.
135    pub host: HostIndex,
136    /// The action performed when triggered.
137    pub action_id: ActionId,
138    /// The HID usage code, displacement, or internal-function index sent.
139    pub value: u16,
140    /// Keyboard modifiers applied (keyboard actions only).
141    pub modifier_mask: ModifierMask,
142    /// Whether the control is remapped away from its default behaviour.
143    pub remapped: bool,
144}
145
146impl PersistentAction {
147    fn from_payload(payload: &[u8; 16]) -> Result<Self, Hidpp20Error> {
148        Ok(Self {
149            cid: ControlId::from(u16::from_be_bytes([payload[0], payload[1]])),
150            host: HostIndex::from(payload[2]),
151            action_id: ActionId::try_from(payload[3])
152                .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
153            value: u16::from_be_bytes([payload[4], payload[5]]),
154            modifier_mask: ModifierMask::from_bits_retain(payload[6]),
155            remapped: payload[7] & 1 != 0,
156        })
157    }
158}
159
160/// The action to assign with
161/// [`set_persistent_action`](PersistentRemappableActionFeature::set_persistent_action).
162#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
163#[cfg_attr(feature = "serde", derive(serde::Serialize))]
164pub struct PersistentActionConfig {
165    /// The action to perform when triggered.
166    pub action_id: ActionId,
167    /// The HID usage code, displacement, or internal-function index to send.
168    pub value: u16,
169    /// Keyboard modifiers to apply (keyboard actions only).
170    pub modifier_mask: ModifierMask,
171}
172
173/// Implements the `PersistentRemappableAction` / `0x1c00` feature.
174#[derive(Clone, Feature)]
175#[creatable(id = 0x1c00, version = 0)]
176pub struct PersistentRemappableActionFeature {
177    /// The endpoint this feature talks to.
178    endpoint: FeatureEndpoint,
179}
180
181impl PersistentRemappableActionFeature {
182    /// Retrieves which HID outputs the device's remapping can produce.
183    pub async fn get_feature_info(&self) -> Result<RemappableCapabilities, Hidpp20Error> {
184        let payload = self.endpoint.call(0, [0; 3]).await?.extend_payload();
185        Ok(RemappableCapabilities::from_bits_retain(
186            u16::from_be_bytes([payload[0], payload[1]]),
187        ))
188    }
189
190    /// Retrieves the control-ID count and host count.
191    pub async fn get_count(&self) -> Result<RemappableInfo, Hidpp20Error> {
192        let payload = self.endpoint.call(1, [0; 3]).await?.extend_payload();
193        Ok(RemappableInfo {
194            count: payload[0],
195            host_count: payload[1],
196        })
197    }
198
199    /// Retrieves the control ID at table `index` for `host`.
200    pub async fn get_cid_info(
201        &self,
202        index: u8,
203        host: HostIndex,
204    ) -> Result<ControlId, Hidpp20Error> {
205        let payload = self
206            .endpoint
207            .call(2, [index, u8::from(host), 0])
208            .await?
209            .extend_payload();
210        Ok(ControlId::from(u16::from_be_bytes([
211            payload[0], payload[1],
212        ])))
213    }
214
215    /// Retrieves the persistent action mapped to `cid` on `host`.
216    pub async fn get_persistent_action(
217        &self,
218        cid: ControlId,
219        host: HostIndex,
220    ) -> Result<PersistentAction, Hidpp20Error> {
221        let [cid_hi, cid_lo] = u16::from(cid).to_be_bytes();
222        let payload = self
223            .endpoint
224            .call(3, [cid_hi, cid_lo, u8::from(host)])
225            .await?
226            .extend_payload();
227        PersistentAction::from_payload(&payload)
228    }
229
230    /// Persistently remaps `cid` on `host` to `config`.
231    ///
232    /// This writes to the device's non-volatile memory and changes the control's
233    /// behaviour until reset (see [`Self::reset_persistent_action`]).
234    pub async fn set_persistent_action(
235        &self,
236        cid: ControlId,
237        host: HostIndex,
238        config: PersistentActionConfig,
239    ) -> Result<(), Hidpp20Error> {
240        let [cid_hi, cid_lo] = u16::from(cid).to_be_bytes();
241        let [value_hi, value_lo] = config.value.to_be_bytes();
242        let mut args = [0; 16];
243        args[..7].copy_from_slice(&[
244            cid_hi,
245            cid_lo,
246            u8::from(host),
247            config.action_id.into(),
248            value_hi,
249            value_lo,
250            config.modifier_mask.bits(),
251        ]);
252        self.endpoint.call_long(4, args).await?;
253        Ok(())
254    }
255
256    /// Resets `cid` on `host` to its factory default action.
257    pub async fn reset_persistent_action(
258        &self,
259        cid: ControlId,
260        host: HostIndex,
261    ) -> Result<(), Hidpp20Error> {
262        let [cid_hi, cid_lo] = u16::from(cid).to_be_bytes();
263        self.endpoint
264            .call(5, [cid_hi, cid_lo, u8::from(host)])
265            .await?;
266        Ok(())
267    }
268
269    /// Resets every control to its factory default for the hosts in `hosts`.
270    pub async fn reset_to_factory_settings(&self, hosts: HostMask) -> Result<(), Hidpp20Error> {
271        self.endpoint.call(6, [hosts.bits(), 0, 0]).await?;
272        Ok(())
273    }
274}