hidpp/feature/
haptic_feedback.rs1use 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
27#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
29#[repr(u8)]
30pub enum HapticWaveform {
31 DampStateChange = 1,
33 SubtleCollision = 4,
35}
36
37#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
39pub struct HapticIntensity(u8);
40
41impl HapticIntensity {
42 pub const MAX: u8 = 100;
44
45 #[must_use]
47 pub const fn new(value: u8) -> Option<Self> {
48 if value <= Self::MAX {
49 Some(Self(value))
50 } else {
51 None
52 }
53 }
54
55 #[must_use]
57 pub const fn get(self) -> u8 {
58 self.0
59 }
60}
61
62#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
64pub struct HapticConfiguration {
65 pub enabled: bool,
67 pub intensity: HapticIntensity,
69 pub level_count: u8,
71 pub level_step: u8,
73}
74
75#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
77pub struct HapticCapabilities {
78 pub unknown_prefix: [u8; 4],
80 pub waveforms: SupportedWaveforms,
82}
83
84#[derive(Clone, Feature)]
86#[creatable(id = 0x19b0, version = 0)]
87pub struct HapticFeedbackFeature {
88 endpoint: FeatureEndpoint,
89}
90
91impl HapticFeedbackFeature {
92 pub async fn get_capabilities(&self) -> Result<HapticCapabilities, Hidpp20Error> {
94 let payload = self.endpoint.call(0, [0; 3]).await?.extend_payload();
95 Ok(HapticCapabilities {
96 unknown_prefix: payload[0..4]
97 .try_into()
98 .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
99 waveforms: SupportedWaveforms::from_bits_retain(u32::from_be_bytes(
100 payload[4..8]
101 .try_into()
102 .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
103 )),
104 })
105 }
106
107 pub async fn get_configuration(&self) -> Result<HapticConfiguration, Hidpp20Error> {
109 let payload = self.endpoint.call(1, [0; 3]).await?.extend_payload();
110 let enabled = match payload[0] {
111 0 => false,
112 1 => true,
113 _ => return Err(Hidpp20Error::UnsupportedResponse),
114 };
115 let Some(intensity) = HapticIntensity::new(payload[1]) else {
116 return Err(Hidpp20Error::UnsupportedResponse);
117 };
118 Ok(HapticConfiguration {
119 enabled,
120 intensity,
121 level_step: payload[2] >> 4,
122 level_count: payload[2] & 0x0f,
123 })
124 }
125
126 pub async fn set_configuration(
128 &self,
129 enabled: bool,
130 intensity: HapticIntensity,
131 ) -> Result<(), Hidpp20Error> {
132 self.endpoint
133 .call(2, [u8::from(enabled), intensity.get(), 0])
134 .await?;
135 Ok(())
136 }
137
138 pub async fn play(&self, waveform: HapticWaveform) -> Result<(), Hidpp20Error> {
140 self.endpoint.call(4, [waveform.into(), 0, 0]).await?;
141 Ok(())
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn intensity_rejects_values_above_one_hundred() {
151 assert_eq!(
152 HapticIntensity::new(100).map(HapticIntensity::get),
153 Some(100)
154 );
155 assert_eq!(HapticIntensity::new(101), None);
156 }
157
158 #[test]
159 fn waveform_mask_retains_unknown_bits() {
160 let mask = SupportedWaveforms::from_bits_retain((1 << 4) | (1 << 31));
161 assert!(mask.contains(SupportedWaveforms::SUBTLE_COLLISION));
162 assert_eq!(mask.bits(), (1 << 4) | (1 << 31));
163 }
164}