Skip to main content

hidpp/feature/disable_keys_by_usage/
mod.rs

1//! Implements the `DisableKeysByUsage` feature (ID `0x4522`) that disables or
2//! enables arbitrary keyboard keys by HID usage.
3//!
4//! Unlike [`DisableKeys`](super::disable_keys) (`0x4521`), which toggles a fixed
5//! set of lock keys, this feature operates on any 8-bit keyboard HID usage.
6
7use std::sync::Arc;
8
9use crate::{
10    channel::HidppChannel,
11    feature::{CreatableFeature, Feature, FeatureEndpoint},
12    protocol::v20::{ErrorType, Hidpp20Error},
13};
14
15/// Number of usage bytes carried by one long-report request.
16const USAGES_PER_PACKET: usize = 16;
17
18/// Implements the `DisableKeysByUsage` / `0x4522` feature.
19#[derive(Clone)]
20pub struct DisableKeysByUsageFeature {
21    /// The endpoint this feature talks to.
22    endpoint: FeatureEndpoint,
23}
24
25impl CreatableFeature for DisableKeysByUsageFeature {
26    const ID: u16 = 0x4522;
27    const STARTING_VERSION: u8 = 0;
28
29    fn new(chan: Arc<HidppChannel>, device_index: u8, feature_index: u8) -> Self {
30        Self {
31            endpoint: FeatureEndpoint::new(chan, device_index, feature_index),
32        }
33    }
34}
35
36impl Feature for DisableKeysByUsageFeature {}
37
38impl DisableKeysByUsageFeature {
39    /// Retrieves the maximum number of usages that can be disabled at once.
40    pub async fn get_capabilities(&self) -> Result<u8, Hidpp20Error> {
41        Ok(self.endpoint.call(0, [0; 3]).await?.extend_payload()[0])
42    }
43
44    /// Disables the given 8-bit keyboard HID `usages`.
45    ///
46    /// Disabling is **cumulative**: these usages are added to the disabled set
47    /// rather than replacing it. A usage of `0` is the list terminator and cannot
48    /// itself be disabled. More usages than fit in one request are sent over
49    /// several requests, which the device still accumulates.
50    pub async fn disable_keys(&self, usages: &[u8]) -> Result<(), Hidpp20Error> {
51        validate_usages(usages)?;
52        for packet in usage_packets(usages) {
53            self.endpoint.call_long(1, packet).await?;
54        }
55        Ok(())
56    }
57
58    /// Enables (removes from the disabled set) the given 8-bit keyboard HID
59    /// `usages`.
60    ///
61    /// Enabling a usage that is not disabled is a no-op. A usage of `0`
62    /// terminates the list.
63    pub async fn enable_keys(&self, usages: &[u8]) -> Result<(), Hidpp20Error> {
64        validate_usages(usages)?;
65        for packet in usage_packets(usages) {
66            self.endpoint.call_long(2, packet).await?;
67        }
68        Ok(())
69    }
70
71    /// Re-enables every keyboard key.
72    pub async fn enable_all_keys(&self) -> Result<(), Hidpp20Error> {
73        self.endpoint.call(3, [0; 3]).await?;
74        Ok(())
75    }
76}
77
78fn validate_usages(usages: &[u8]) -> Result<(), Hidpp20Error> {
79    if usages.contains(&0) {
80        return Err(Hidpp20Error::Feature(ErrorType::InvalidArgument));
81    }
82    Ok(())
83}
84
85/// Splits `usages` into long-report packets of up to [`USAGES_PER_PACKET`]
86/// bytes.
87///
88/// A packet shorter than the full width is zero-padded, which doubles as the
89/// `0x00` end-of-list terminator; a packet filling every byte carries no
90/// terminator, as the device treats a full packet as exactly that many usages.
91fn usage_packets(usages: &[u8]) -> Vec<[u8; USAGES_PER_PACKET]> {
92    usages
93        .chunks(USAGES_PER_PACKET)
94        .map(|chunk| {
95            let mut packet = [0u8; USAGES_PER_PACKET];
96            packet[..chunk.len()].copy_from_slice(chunk);
97            packet
98        })
99        .collect()
100}
101
102#[cfg(test)]
103mod tests {
104    use std::assert_matches;
105
106    use super::{usage_packets, validate_usages};
107    use crate::protocol::v20::{ErrorType, Hidpp20Error};
108
109    #[test]
110    fn empty_usage_list_sends_no_packets() {
111        assert!(usage_packets(&[]).is_empty());
112    }
113
114    #[test]
115    fn short_list_is_zero_terminated() {
116        let packets = usage_packets(&[0x39, 0x3a, 0x3b]);
117        assert_eq!(packets.len(), 1);
118        assert_eq!(packets[0][..3], [0x39, 0x3a, 0x3b]);
119        // The remaining bytes are the 0x00 terminator / padding.
120        assert!(packets[0][3..].iter().all(|&b| b == 0));
121    }
122
123    #[test]
124    fn rejects_zero_usage_before_packetizing() {
125        assert_matches!(
126            validate_usages(&[0x39, 0, 0x3a]),
127            Err(Hidpp20Error::Feature(ErrorType::InvalidArgument))
128        );
129    }
130
131    #[test]
132    fn full_packet_has_no_terminator() {
133        let usages: Vec<u8> = (1..=16).collect();
134        let packets = usage_packets(&usages);
135        assert_eq!(packets.len(), 1);
136        assert_eq!(packets[0], usages.as_slice());
137    }
138
139    #[test]
140    fn overflow_splits_into_cumulative_packets() {
141        let usages: Vec<u8> = (1..=18).collect();
142        let packets = usage_packets(&usages);
143        assert_eq!(packets.len(), 2);
144        assert_eq!(packets[0], (1..=16).collect::<Vec<u8>>().as_slice());
145        assert_eq!(packets[1][..2], [17, 18]);
146        assert!(packets[1][2..].iter().all(|&b| b == 0));
147    }
148}