1use 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 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
14 #[cfg_attr(feature = "serde", derive(serde::Serialize))]
15 pub struct FnInversionCapabilities: u8 {
16 const MANUAL_FN_LOCK = 1 << 0;
18 }
19}
20
21#[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 Off = 0,
29 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
41#[cfg_attr(feature = "serde", derive(serde::Serialize))]
42#[non_exhaustive]
43pub struct FnInversionInfo {
44 pub host_index: HostIndex,
46 pub state: FnInversionState,
48 pub default_state: FnInversionState,
50 pub capabilities: FnInversionCapabilities,
52}
53
54#[derive(Clone, Feature)]
56#[creatable(id = 0x40a3, version = 0)]
57pub struct FnInversionMultiHostFeature {
58 endpoint: FeatureEndpoint,
60}
61
62impl FnInversionMultiHostFeature {
63 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 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
112#[cfg_attr(feature = "serde", derive(serde::Serialize))]
113#[non_exhaustive]
114pub struct GlobalFnInversion {
115 pub state: FnInversionState,
117 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#[derive(Clone, Feature)]
138#[creatable(id = 0x40a2, version = 0)]
139pub struct FnInversionWithDefaultStateFeature {
140 endpoint: FeatureEndpoint,
142}
143
144impl FnInversionWithDefaultStateFeature {
145 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 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}