Skip to main content

hidpp/feature/
fn_inversion.rs

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