1use num_enum::{IntoPrimitive, TryFromPrimitive};
8use openlogi_hidpp_derive::Feature;
9
10use crate::{feature::FeatureEndpoint, protocol::v20::Hidpp20Error};
11
12bitflags::bitflags! {
13 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
18 #[cfg_attr(feature = "serde", derive(serde::Serialize))]
19 pub struct SupportedWaveforms: u32 {
20 const DAMP_STATE_CHANGE = 1 << 1;
22 const SUBTLE_COLLISION = 1 << 4;
24 }
25}
26
27impl SupportedWaveforms {
28 #[must_use]
31 pub fn supports(self, waveform: HapticWaveform) -> bool {
32 1u32.checked_shl(u32::from(u8::from(waveform)))
33 .is_some_and(|bit| self.bits() & bit != 0)
34 }
35}
36
37bitflags::bitflags! {
38 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
43 #[cfg_attr(feature = "serde", derive(serde::Serialize))]
44 pub struct HapticConfigFlags: u8 {
45 const ENABLED = 1 << 0;
47 const POWER_SAVE = 1 << 1;
50 }
51}
52
53#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
55#[repr(u8)]
56pub enum HapticWaveform {
57 DampStateChange = 1,
59 SubtleCollision = 4,
61}
62
63#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
65pub struct HapticIntensity(u8);
66
67impl HapticIntensity {
68 pub const MAX: u8 = 100;
70
71 #[must_use]
73 pub const fn new(value: u8) -> Option<Self> {
74 if value <= Self::MAX {
75 Some(Self(value))
76 } else {
77 None
78 }
79 }
80
81 #[must_use]
83 pub const fn get(self) -> u8 {
84 self.0
85 }
86}
87
88#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
90pub struct HapticConfiguration {
91 pub flags: HapticConfigFlags,
93 pub intensity: HapticIntensity,
95 pub level_count: u8,
97 pub level_step: u8,
99}
100
101impl HapticConfiguration {
102 fn from_payload(payload: &[u8]) -> Result<Self, Hidpp20Error> {
103 let [flags, intensity, levels, ..] = *payload else {
104 return Err(Hidpp20Error::UnsupportedResponse);
105 };
106 Ok(Self {
107 flags: HapticConfigFlags::from_bits_retain(flags),
108 intensity: HapticIntensity::new(intensity).ok_or(Hidpp20Error::UnsupportedResponse)?,
109 level_step: levels >> 4,
110 level_count: levels & 0x0f,
111 })
112 }
113}
114
115#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
117pub struct HapticCapabilities {
118 pub unknown_prefix: [u8; 4],
120 pub waveforms: SupportedWaveforms,
122}
123
124#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
131#[cfg_attr(feature = "serde", derive(serde::Serialize))]
132pub struct HapticStatus(u8);
133
134impl HapticStatus {
135 #[must_use]
137 pub const fn from_raw(raw: u8) -> Self {
138 Self(raw)
139 }
140
141 #[must_use]
143 pub const fn raw(self) -> u8 {
144 self.0
145 }
146
147 #[must_use]
150 pub const fn is_ready(self) -> bool {
151 self.0 == 0
152 }
153}
154
155#[derive(Clone, Feature)]
157#[creatable(id = 0x19b0, version = 0)]
158pub struct HapticFeedbackFeature {
159 endpoint: FeatureEndpoint,
160}
161
162impl HapticFeedbackFeature {
163 pub async fn get_capabilities(&self) -> Result<HapticCapabilities, Hidpp20Error> {
168 let payload = self.endpoint.call(0, [0; 3]).await?.extend_payload();
169 Ok(HapticCapabilities {
170 unknown_prefix: payload[0..4]
171 .try_into()
172 .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
173 waveforms: SupportedWaveforms::from_bits_retain(u32::from_be_bytes(
174 payload[4..8]
175 .try_into()
176 .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
177 )),
178 })
179 }
180
181 pub async fn get_configuration(&self) -> Result<HapticConfiguration, Hidpp20Error> {
183 let payload = self.endpoint.call(1, [0; 3]).await?.extend_payload();
184 HapticConfiguration::from_payload(&payload)
185 }
186
187 pub async fn set_configuration(
200 &self,
201 flags: HapticConfigFlags,
202 intensity: HapticIntensity,
203 ) -> Result<HapticConfiguration, Hidpp20Error> {
204 let payload = self
205 .endpoint
206 .call(2, [flags.bits(), intensity.get(), 0])
207 .await?
208 .extend_payload();
209 HapticConfiguration::from_payload(&payload)
210 }
211
212 pub async fn get_haptic_status(&self) -> Result<HapticStatus, Hidpp20Error> {
220 let payload = self.endpoint.call(3, [0; 3]).await?.extend_payload();
221 Ok(HapticStatus(payload[0]))
222 }
223
224 pub async fn play(&self, waveform: HapticWaveform) -> Result<(), Hidpp20Error> {
226 self.endpoint.call(4, [waveform.into(), 0, 0]).await?;
227 Ok(())
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 #[test]
236 fn intensity_rejects_values_above_one_hundred() {
237 assert_eq!(
238 HapticIntensity::new(100).map(HapticIntensity::get),
239 Some(100)
240 );
241 assert_eq!(HapticIntensity::new(101), None);
242 }
243
244 #[test]
245 fn waveform_mask_retains_unknown_bits() {
246 let mask = SupportedWaveforms::from_bits_retain((1 << 4) | (1 << 31));
247 assert!(mask.contains(SupportedWaveforms::SUBTLE_COLLISION));
248 assert_eq!(mask.bits(), (1 << 4) | (1 << 31));
249 }
250
251 #[test]
252 fn a_waveform_is_supported_exactly_when_its_id_bit_is_set() {
253 let only_collision = SupportedWaveforms::from_bits_retain(1 << 4);
254 assert!(only_collision.supports(HapticWaveform::SubtleCollision));
255 assert!(!only_collision.supports(HapticWaveform::DampStateChange));
256
257 let only_damp = SupportedWaveforms::from_bits_retain(1 << 1);
258 assert!(only_damp.supports(HapticWaveform::DampStateChange));
259 assert!(!only_damp.supports(HapticWaveform::SubtleCollision));
260
261 let others = SupportedWaveforms::from_bits_retain(!((1 << 1) | (1 << 4)));
263 assert!(!others.supports(HapticWaveform::DampStateChange));
264 assert!(!others.supports(HapticWaveform::SubtleCollision));
265 assert!(!SupportedWaveforms::empty().supports(HapticWaveform::SubtleCollision));
266 }
267
268 #[test]
269 fn configuration_keeps_power_save_and_unnamed_flag_bits() {
270 let config = HapticConfiguration::from_payload(&[0x82, 60, 0x34]).unwrap();
272 assert!(!config.flags.contains(HapticConfigFlags::ENABLED));
273 assert!(config.flags.contains(HapticConfigFlags::POWER_SAVE));
274 assert_eq!(config.flags.bits(), 0x82);
275 assert_eq!(
276 (config.flags | HapticConfigFlags::ENABLED).bits(),
277 0x83,
278 "enabling must leave power save and the unnamed bit in place"
279 );
280 assert_eq!(config.intensity.get(), 60);
281 assert_eq!((config.level_step, config.level_count), (3, 4));
282 }
283
284 fn set_with_reply(reply: [u8; 3]) -> Result<HapticConfiguration, Hidpp20Error> {
287 use std::sync::Arc;
288
289 use crate::channel::mock::{MockRawHidChannel, channel_with_reader};
290 use crate::feature::CreatableFeature;
291 use crate::nibble::U4;
292 use crate::protocol::v20::{Message, MessageHeader};
293
294 futures::executor::block_on(async {
295 let (raw, handle) = MockRawHidChannel::new();
296 let channel = Arc::new(channel_with_reader(raw).await);
297 let feature = HapticFeedbackFeature::new(channel, 3, 0x0b);
298 let intensity = HapticIntensity::new(63).unwrap();
299 let mut write = Box::pin(
300 feature.set_configuration(HapticConfigFlags::from_bits_retain(0x83), intensity),
301 );
302 assert!(futures::poll!(write.as_mut()).is_pending());
303 let header = MessageHeader {
304 device_index: 3,
305 feature_index: 0x0b,
306 function_id: U4::from_lo(2),
307 software_id: U4::from_lo(1),
308 };
309 handle
310 .send_incoming(Message::Short(header, reply).into())
311 .await;
312 let result = write.await;
313 let written = handle.written_reports();
314 let request = written.last().expect("setHapticConfig was sent");
315 assert_eq!(&request[4..7], &[0x83, 63, 0]);
316 result
317 })
318 }
319
320 #[test]
321 fn set_configuration_returns_the_state_the_device_reports() {
322 let applied = set_with_reply([0x82, 60, 0x34]).unwrap();
324 assert!(!applied.flags.contains(HapticConfigFlags::ENABLED));
325 assert_eq!(applied.flags.bits(), 0x82);
326 assert_eq!(applied.intensity.get(), 60);
327 assert_eq!((applied.level_step, applied.level_count), (3, 4));
328 assert!(matches!(
329 set_with_reply([0x83, 101, 0]),
330 Err(Hidpp20Error::UnsupportedResponse)
331 ));
332 }
333
334 #[test]
335 fn configuration_rejects_out_of_range_intensity() {
336 assert!(matches!(
337 HapticConfiguration::from_payload(&[0x01, 101, 0]),
338 Err(Hidpp20Error::UnsupportedResponse)
339 ));
340 }
341
342 fn status_with_reply(reply: [u8; 3]) -> HapticStatus {
344 use std::sync::Arc;
345
346 use crate::channel::mock::{MockRawHidChannel, channel_with_reader};
347 use crate::feature::CreatableFeature;
348 use crate::nibble::U4;
349 use crate::protocol::v20::{Message, MessageHeader};
350
351 futures::executor::block_on(async {
352 let (raw, handle) = MockRawHidChannel::new();
353 let channel = Arc::new(channel_with_reader(raw).await);
354 let feature = HapticFeedbackFeature::new(channel, 3, 0x0b);
355 let mut read = Box::pin(feature.get_haptic_status());
356 assert!(futures::poll!(read.as_mut()).is_pending());
357 let header = MessageHeader {
358 device_index: 3,
359 feature_index: 0x0b,
360 function_id: U4::from_lo(3),
361 software_id: U4::from_lo(1),
362 };
363 handle
364 .send_incoming(Message::Short(header, reply).into())
365 .await;
366 let status = read.await.expect("the device answered getHapticInfo");
367 let written = handle.written_reports();
368 let request = written.last().expect("getHapticInfo was sent");
369 assert_eq!(request[3] >> 4, 3, "getHapticInfo is function 3");
370 assert_eq!(&request[4..7], &[0, 0, 0], "with an empty request");
371 status
372 })
373 }
374
375 #[test]
376 fn haptic_status_is_reply_byte_zero_and_only_zero_is_ready() {
377 let ready = status_with_reply([0, 0xff, 0xff]);
378 assert!(ready.is_ready(), "later bytes do not decide readiness");
379 assert_eq!(ready.raw(), 0);
380 for raw in [1, 2, 0x80, 0xff] {
381 let status = status_with_reply([raw, 0, 0]);
382 assert!(!status.is_ready(), "status {raw} refuses play");
383 assert_eq!(status.raw(), raw, "the raw byte is kept");
384 }
385 }
386}