hidpp/feature/
haptic_feedback.rs1use std::sync::Arc;
8
9use num_enum::{IntoPrimitive, TryFromPrimitive};
10
11use crate::{
12 channel::HidppChannel,
13 feature::{CreatableFeature, Feature, FeatureEndpoint},
14 protocol::v20::Hidpp20Error,
15};
16
17bitflags::bitflags! {
18 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
23 #[cfg_attr(feature = "serde", derive(serde::Serialize))]
24 pub struct SupportedWaveforms: u32 {
25 const DAMP_STATE_CHANGE = 1 << 1;
27 const SUBTLE_COLLISION = 1 << 4;
29 }
30}
31
32#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
34#[repr(u8)]
35pub enum HapticWaveform {
36 DampStateChange = 1,
38 SubtleCollision = 4,
40}
41
42#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
44pub struct HapticIntensity(u8);
45
46impl HapticIntensity {
47 pub const MAX: u8 = 100;
49
50 #[must_use]
52 pub const fn new(value: u8) -> Option<Self> {
53 if value <= Self::MAX {
54 Some(Self(value))
55 } else {
56 None
57 }
58 }
59
60 #[must_use]
62 pub const fn get(self) -> u8 {
63 self.0
64 }
65}
66
67#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
69pub struct HapticConfiguration {
70 pub enabled: bool,
72 pub intensity: HapticIntensity,
74 pub level_count: u8,
76 pub level_step: u8,
78}
79
80#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
82pub struct HapticCapabilities {
83 pub unknown_prefix: [u8; 4],
85 pub waveforms: SupportedWaveforms,
87}
88
89#[derive(Clone)]
91pub struct HapticFeedbackFeature {
92 endpoint: FeatureEndpoint,
93}
94
95impl CreatableFeature for HapticFeedbackFeature {
96 const ID: u16 = 0x19b0;
97 const STARTING_VERSION: u8 = 0;
98
99 fn new(chan: Arc<HidppChannel>, device_index: u8, feature_index: u8) -> Self {
100 Self {
101 endpoint: FeatureEndpoint::new(chan, device_index, feature_index),
102 }
103 }
104}
105
106impl Feature for HapticFeedbackFeature {}
107
108impl HapticFeedbackFeature {
109 pub async fn get_capabilities(&self) -> Result<HapticCapabilities, Hidpp20Error> {
111 let payload = self.endpoint.call(0, [0; 3]).await?.extend_payload();
112 Ok(HapticCapabilities {
113 unknown_prefix: payload[0..4]
114 .try_into()
115 .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
116 waveforms: SupportedWaveforms::from_bits_retain(u32::from_be_bytes(
117 payload[4..8]
118 .try_into()
119 .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
120 )),
121 })
122 }
123
124 pub async fn get_configuration(&self) -> Result<HapticConfiguration, Hidpp20Error> {
126 let payload = self.endpoint.call(1, [0; 3]).await?.extend_payload();
127 let enabled = match payload[0] {
128 0 => false,
129 1 => true,
130 _ => return Err(Hidpp20Error::UnsupportedResponse),
131 };
132 let Some(intensity) = HapticIntensity::new(payload[1]) else {
133 return Err(Hidpp20Error::UnsupportedResponse);
134 };
135 Ok(HapticConfiguration {
136 enabled,
137 intensity,
138 level_step: payload[2] >> 4,
139 level_count: payload[2] & 0x0f,
140 })
141 }
142
143 pub async fn set_configuration(
145 &self,
146 enabled: bool,
147 intensity: HapticIntensity,
148 ) -> Result<(), Hidpp20Error> {
149 self.endpoint
150 .call(2, [u8::from(enabled), intensity.get(), 0])
151 .await?;
152 Ok(())
153 }
154
155 pub async fn play(&self, waveform: HapticWaveform) -> Result<(), Hidpp20Error> {
157 self.endpoint.call(4, [waveform.into(), 0, 0]).await?;
158 Ok(())
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn intensity_rejects_values_above_one_hundred() {
168 assert_eq!(
169 HapticIntensity::new(100).map(HapticIntensity::get),
170 Some(100)
171 );
172 assert_eq!(HapticIntensity::new(101), None);
173 }
174
175 #[test]
176 fn waveform_mask_retains_unknown_bits() {
177 let mask = SupportedWaveforms::from_bits_retain((1 << 4) | (1 << 31));
178 assert!(mask.contains(SupportedWaveforms::SUBTLE_COLLISION));
179 assert_eq!(mask.bits(), (1 << 4) | (1 << 31));
180 }
181}