Skip to main content

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