Skip to main content

hidpp/feature/fn_inversion/
mod.rs

1//! Implements function-key inversion features.
2
3use std::sync::Arc;
4
5use num_enum::{IntoPrimitive, TryFromPrimitive};
6
7use crate::{
8    channel::HidppChannel,
9    feature::{CreatableFeature, Feature, FeatureEndpoint, hosts_info::HostIndex},
10    protocol::v20::Hidpp20Error,
11};
12
13bitflags::bitflags! {
14    /// Function-key inversion capabilities.
15    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
16    #[cfg_attr(feature = "serde", derive(serde::Serialize))]
17    pub struct FnInversionCapabilities: u8 {
18        /// The device supports manual Fn-lock control.
19        const MANUAL_FN_LOCK = 1 << 0;
20    }
21}
22
23/// Function-key inversion state.
24#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
25#[cfg_attr(feature = "serde", derive(serde::Serialize))]
26#[non_exhaustive]
27#[repr(u8)]
28pub enum FnInversionState {
29    /// Function-key inversion is disabled.
30    Off = 0,
31    /// Function-key inversion is enabled.
32    On = 1,
33}
34
35impl From<bool> for FnInversionState {
36    fn from(value: bool) -> Self {
37        if value { Self::On } else { Self::Off }
38    }
39}
40
41/// Function-key inversion state for a host slot.
42#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
43#[cfg_attr(feature = "serde", derive(serde::Serialize))]
44#[non_exhaustive]
45pub struct FnInversionInfo {
46    /// Host slot index returned by the device.
47    pub host_index: HostIndex,
48    /// Current inversion state.
49    pub state: FnInversionState,
50    /// Default inversion state.
51    pub default_state: FnInversionState,
52    /// Inversion capabilities.
53    pub capabilities: FnInversionCapabilities,
54}
55
56/// Implements `FnInversionForMultiHostDevices` / `0x40a3`.
57#[derive(Clone)]
58pub struct FnInversionMultiHostFeature {
59    /// The endpoint this feature talks to.
60    endpoint: FeatureEndpoint,
61}
62
63impl CreatableFeature for FnInversionMultiHostFeature {
64    const ID: u16 = 0x40a3;
65    const STARTING_VERSION: u8 = 0;
66
67    fn new(chan: Arc<HidppChannel>, device_index: u8, feature_index: u8) -> Self {
68        Self {
69            endpoint: FeatureEndpoint::new(chan, device_index, feature_index),
70        }
71    }
72}
73
74impl Feature for FnInversionMultiHostFeature {}
75
76impl FnInversionMultiHostFeature {
77    /// Retrieves global Fn inversion for `host`.
78    pub async fn get_global_fn_inversion(
79        &self,
80        host: HostIndex,
81    ) -> Result<FnInversionInfo, Hidpp20Error> {
82        let payload = self
83            .endpoint
84            .call(0, [u8::from(host), 0, 0])
85            .await?
86            .extend_payload();
87        FnInversionInfo::from_payload(payload)
88    }
89
90    /// Sets global Fn inversion for `host`.
91    ///
92    /// The setting is stored by the device for the selected host slot.
93    pub async fn set_global_fn_inversion(
94        &self,
95        host: HostIndex,
96        state: FnInversionState,
97    ) -> Result<FnInversionInfo, Hidpp20Error> {
98        let payload = self
99            .endpoint
100            .call(1, set_multi_host_fn_inversion_args(host, state))
101            .await?
102            .extend_payload();
103        FnInversionInfo::from_payload(payload)
104    }
105}
106
107fn set_multi_host_fn_inversion_args(host: HostIndex, state: FnInversionState) -> [u8; 3] {
108    [u8::from(host), u8::from(state), 0]
109}
110
111impl FnInversionInfo {
112    fn from_payload(payload: [u8; 16]) -> Result<Self, Hidpp20Error> {
113        Ok(Self {
114            host_index: HostIndex::from(payload[0]),
115            state: FnInversionState::try_from(payload[1])
116                .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
117            default_state: FnInversionState::try_from(payload[2])
118                .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
119            capabilities: FnInversionCapabilities::from_bits_retain(payload[3]),
120        })
121    }
122}
123
124/// Global function-key inversion state, common to all keys.
125#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
126#[cfg_attr(feature = "serde", derive(serde::Serialize))]
127#[non_exhaustive]
128pub struct GlobalFnInversion {
129    /// Current inversion state.
130    pub state: FnInversionState,
131    /// Default inversion state.
132    pub default_state: FnInversionState,
133}
134
135impl GlobalFnInversion {
136    fn from_payload(payload: [u8; 16]) -> Result<Self, Hidpp20Error> {
137        Ok(Self {
138            state: FnInversionState::try_from(payload[0])
139                .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
140            default_state: FnInversionState::try_from(payload[1])
141                .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
142        })
143    }
144}
145
146/// Implements `FnInversionWithDefaultState` / `0x40a2`.
147///
148/// This is the single-host predecessor of
149/// [`FnInversionMultiHostFeature`] (`0x40a3`): the inversion state is global
150/// rather than per host slot.
151#[derive(Clone)]
152pub struct FnInversionWithDefaultStateFeature {
153    /// The endpoint this feature talks to.
154    endpoint: FeatureEndpoint,
155}
156
157impl CreatableFeature for FnInversionWithDefaultStateFeature {
158    const ID: u16 = 0x40a2;
159    const STARTING_VERSION: u8 = 0;
160
161    fn new(chan: Arc<HidppChannel>, device_index: u8, feature_index: u8) -> Self {
162        Self {
163            endpoint: FeatureEndpoint::new(chan, device_index, feature_index),
164        }
165    }
166}
167
168impl Feature for FnInversionWithDefaultStateFeature {}
169
170impl FnInversionWithDefaultStateFeature {
171    /// Retrieves the global Fn inversion state and its default.
172    pub async fn get_global_fn_inversion(&self) -> Result<GlobalFnInversion, Hidpp20Error> {
173        let payload = self.endpoint.call(0, [0; 3]).await?.extend_payload();
174        GlobalFnInversion::from_payload(payload)
175    }
176
177    /// Sets the global Fn inversion state and returns the resulting state.
178    pub async fn set_global_fn_inversion(
179        &self,
180        state: FnInversionState,
181    ) -> Result<GlobalFnInversion, Hidpp20Error> {
182        let payload = self
183            .endpoint
184            .call(1, [u8::from(state), 0, 0])
185            .await?
186            .extend_payload();
187        GlobalFnInversion::from_payload(payload)
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::{
194        FnInversionInfo, FnInversionState, GlobalFnInversion, set_multi_host_fn_inversion_args,
195    };
196    use crate::feature::hosts_info::HostIndex;
197
198    #[test]
199    fn parses_fn_inversion_info() {
200        let mut payload = [0; 16];
201        payload[0] = 1;
202        payload[1] = 1;
203        payload[2] = 0;
204        payload[3] = 1;
205
206        let info = FnInversionInfo::from_payload(payload).unwrap();
207
208        assert_eq!(info.host_index, HostIndex::Slot(1));
209        assert_eq!(info.state, FnInversionState::On);
210        assert_eq!(info.default_state, FnInversionState::Off);
211    }
212
213    #[test]
214    fn parses_global_fn_inversion() {
215        let mut payload = [0; 16];
216        payload[0] = 1;
217        payload[1] = 0;
218
219        let global = GlobalFnInversion::from_payload(payload).unwrap();
220
221        assert_eq!(global.state, FnInversionState::On);
222        assert_eq!(global.default_state, FnInversionState::Off);
223    }
224
225    #[test]
226    fn encodes_multi_host_set_args_as_host_then_state() {
227        assert_eq!(
228            set_multi_host_fn_inversion_args(HostIndex::Slot(2), FnInversionState::On),
229            [2, 1, 0]
230        );
231    }
232}