1use std::sync::Arc;
12
13use num_enum::{IntoPrimitive, TryFromPrimitive};
14
15use crate::{
16 channel::{HidppChannel, MessageListenerGuard},
17 event::EventEmitter,
18 feature::{CreatableFeature, EmittingFeature, Feature, FeatureEndpoint, event_payload},
19 protocol::v20::Hidpp20Error,
20};
21
22const EFFECT_UNCHANGED: u8 = 0xff;
24
25const MODE_SHIFT: u16 = 3;
27const MODE_MASK: u16 = 0b11 << MODE_SHIFT;
29
30bitflags::bitflags! {
31 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
37 #[cfg_attr(feature = "serde", derive(serde::Serialize))]
38 pub struct BacklightOptions: u16 {
39 const WOW = 1 << 0;
41 const CROWN = 1 << 1;
43 const PWR_SAVE = 1 << 2;
45 const WOW_SUPPORTED = 1 << 8;
47 const CROWN_SUPPORTED = 1 << 9;
49 const PWR_SAVE_SUPPORTED = 1 << 10;
51 const AUTO_MODE_SUPPORTED = 1 << 11;
53 const TEMP_MANUAL_SUPPORTED = 1 << 12;
55 const PERM_MANUAL_SUPPORTED = 1 << 13;
57 }
58}
59
60bitflags::bitflags! {
61 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
64 #[cfg_attr(feature = "serde", derive(serde::Serialize))]
65 pub struct BacklightEffectList: u16 {
66 const STATIC = 1 << 0;
68 const NONE = 1 << 1;
70 const BREATHING = 1 << 2;
72 const CONTRAST = 1 << 3;
74 const REACTION = 1 << 4;
76 const RANDOM = 1 << 5;
78 const WAVES = 1 << 6;
80 }
81}
82
83#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
85#[cfg_attr(feature = "serde", derive(serde::Serialize))]
86#[non_exhaustive]
87#[repr(u8)]
88pub enum BacklightMode {
89 None = 0,
91 Automatic = 1,
93 TemporaryManual = 2,
96 PermanentManual = 3,
98}
99
100#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
102#[cfg_attr(feature = "serde", derive(serde::Serialize))]
103#[non_exhaustive]
104#[repr(u8)]
105pub enum BacklightEffect {
106 Static = 0,
108 None = 1,
110 Breathing = 2,
112 Contrast = 3,
114 Reaction = 4,
116 Random = 5,
118 Waves = 6,
120}
121
122#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
124#[cfg_attr(feature = "serde", derive(serde::Serialize))]
125#[non_exhaustive]
126#[repr(u8)]
127pub enum BacklightStatus {
128 DisabledBySoftware = 0,
130 DisabledByCriticalBattery = 1,
132 AlsAutomatic = 2,
134 AlsSaturated = 3,
136 TemporaryManual = 4,
138 PermanentManual = 5,
140}
141
142#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
144#[cfg_attr(feature = "serde", derive(serde::Serialize))]
145#[non_exhaustive]
146pub struct BacklightConfig {
147 pub enabled: bool,
149 pub options: BacklightOptions,
151 pub mode: BacklightMode,
153 pub effect_list: BacklightEffectList,
155 pub current_level: u8,
157 pub duration_hands_out: u16,
160 pub duration_hands_in: u16,
163 pub duration_powered: u16,
165}
166
167#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
170#[cfg_attr(feature = "serde", derive(serde::Serialize))]
171pub struct SetBacklightConfig {
172 pub enabled: bool,
174 pub options: BacklightOptions,
178 pub mode: BacklightMode,
181 pub effect: Option<BacklightEffect>,
183 pub current_level: u8,
185 pub duration_hands_out: u16,
188 pub duration_hands_in: u16,
191 pub duration_powered: u16,
193}
194
195#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
197#[cfg_attr(feature = "serde", derive(serde::Serialize))]
198#[non_exhaustive]
199pub struct BacklightInfo {
200 pub nb_levels: u8,
202 pub current_level: u8,
204 pub status: BacklightStatus,
206 pub effect: BacklightEffect,
208 pub oob_duration_hands_out: u16,
210 pub oob_duration_hands_in: u16,
212 pub oob_duration_powered: u16,
214}
215
216#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
218#[cfg_attr(feature = "serde", derive(serde::Serialize))]
219#[non_exhaustive]
220pub enum BacklightEvent {
221 InfoChanged(BacklightInfoUpdate),
223}
224
225#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
227#[cfg_attr(feature = "serde", derive(serde::Serialize))]
228#[non_exhaustive]
229pub struct BacklightInfoUpdate {
230 pub nb_levels: u8,
232 pub current_level: u8,
234 pub status: BacklightStatus,
236 pub effect: BacklightEffect,
238}
239
240pub struct BacklightFeature {
242 endpoint: FeatureEndpoint,
244
245 emitter: Arc<EventEmitter<BacklightEvent>>,
247
248 _msg_listener: MessageListenerGuard,
250}
251
252impl CreatableFeature for BacklightFeature {
253 const ID: u16 = 0x1982;
254 const STARTING_VERSION: u8 = 3;
255
256 fn new(chan: Arc<HidppChannel>, device_index: u8, feature_index: u8) -> Self {
257 let emitter = Arc::new(EventEmitter::new());
258
259 let listener = chan.add_msg_listener_guarded({
260 let emitter = Arc::clone(&emitter);
261
262 move |raw, matched| {
263 let Some((func, payload)) =
264 event_payload(raw, matched, device_index, feature_index)
265 else {
266 return;
267 };
268 if func.to_lo() != 0 {
270 return;
271 }
272 if let Ok(update) = BacklightInfoUpdate::from_payload(&payload) {
273 emitter.emit(BacklightEvent::InfoChanged(update));
274 }
275 }
276 });
277
278 Self {
279 endpoint: FeatureEndpoint::new(chan, device_index, feature_index),
280 emitter,
281 _msg_listener: listener,
282 }
283 }
284}
285
286impl Feature for BacklightFeature {}
287
288impl EmittingFeature<BacklightEvent> for BacklightFeature {
289 fn listen(&self) -> async_channel::Receiver<BacklightEvent> {
290 self.emitter.create_receiver()
291 }
292}
293
294impl BacklightFeature {
295 pub async fn get_backlight_config(&self) -> Result<BacklightConfig, Hidpp20Error> {
297 let payload = self.endpoint.call(0, [0; 3]).await?.extend_payload();
298 let raw_options = u16::from_le_bytes([payload[1], payload[2]]);
299 Ok(BacklightConfig {
300 enabled: payload[0] & 1 != 0,
301 options: BacklightOptions::from_bits_retain(raw_options & !MODE_MASK),
302 mode: BacklightMode::try_from(((raw_options & MODE_MASK) >> MODE_SHIFT) as u8)
303 .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
304 effect_list: BacklightEffectList::from_bits_retain(u16::from_le_bytes([
305 payload[3], payload[4],
306 ])),
307 current_level: payload[5],
308 duration_hands_out: u16::from_le_bytes([payload[6], payload[7]]),
309 duration_hands_in: u16::from_le_bytes([payload[8], payload[9]]),
310 duration_powered: u16::from_le_bytes([payload[10], payload[11]]),
311 })
312 }
313
314 pub async fn set_backlight_config(
316 &self,
317 config: SetBacklightConfig,
318 ) -> Result<(), Hidpp20Error> {
319 let options_byte = (config.options.bits()
322 & (BacklightOptions::WOW | BacklightOptions::CROWN | BacklightOptions::PWR_SAVE).bits())
323 as u8
324 | (u8::from(config.mode) << MODE_SHIFT);
325 let [out_lo, out_hi] = config.duration_hands_out.to_le_bytes();
326 let [in_lo, in_hi] = config.duration_hands_in.to_le_bytes();
327 let [pwr_lo, pwr_hi] = config.duration_powered.to_le_bytes();
328 let mut args = [0; 16];
329 args[..10].copy_from_slice(&[
330 u8::from(config.enabled),
331 options_byte,
332 config.effect.map_or(EFFECT_UNCHANGED, u8::from),
333 config.current_level,
334 out_lo,
335 out_hi,
336 in_lo,
337 in_hi,
338 pwr_lo,
339 pwr_hi,
340 ]);
341 self.endpoint.call_long(1, args).await?;
342 Ok(())
343 }
344
345 pub async fn get_backlight_info(&self) -> Result<BacklightInfo, Hidpp20Error> {
347 let payload = self.endpoint.call(2, [0; 3]).await?.extend_payload();
348 Ok(BacklightInfo {
349 nb_levels: payload[0],
350 current_level: payload[1],
351 status: BacklightStatus::try_from(payload[2])
352 .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
353 effect: BacklightEffect::try_from(payload[3])
354 .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
355 oob_duration_hands_out: u16::from_le_bytes([payload[4], payload[5]]),
356 oob_duration_hands_in: u16::from_le_bytes([payload[6], payload[7]]),
357 oob_duration_powered: u16::from_le_bytes([payload[8], payload[9]]),
358 })
359 }
360
361 pub async fn set_backlight_effect(&self, effect: BacklightEffect) -> Result<(), Hidpp20Error> {
363 self.endpoint.call(3, [effect.into(), 0, 0]).await?;
364 Ok(())
365 }
366}
367
368impl BacklightInfoUpdate {
369 fn from_payload(payload: &[u8; 16]) -> Result<Self, Hidpp20Error> {
370 Ok(Self {
371 nb_levels: payload[0],
372 current_level: payload[1],
373 status: BacklightStatus::try_from(payload[2])
374 .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
375 effect: BacklightEffect::try_from(payload[3])
376 .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
377 })
378 }
379}
380
381#[cfg(test)]
382mod tests {
383 use super::{
384 BacklightEffect, BacklightInfoUpdate, BacklightMode, BacklightOptions, BacklightStatus,
385 };
386
387 #[test]
388 fn decodes_options_and_mode_split() {
389 let raw = BacklightOptions::WOW.bits()
391 | (u16::from(u8::from(BacklightMode::PermanentManual)) << 3)
392 | BacklightOptions::AUTO_MODE_SUPPORTED.bits();
393 let mode = BacklightMode::try_from(((raw & (0b11 << 3)) >> 3) as u8).unwrap();
394 let options = BacklightOptions::from_bits_retain(raw & !(0b11 << 3));
395
396 assert_eq!(mode, BacklightMode::PermanentManual);
397 assert!(options.contains(BacklightOptions::WOW));
398 assert!(options.contains(BacklightOptions::AUTO_MODE_SUPPORTED));
399 assert!(!options.contains(BacklightOptions::PWR_SAVE));
401 assert!(!options.contains(BacklightOptions::CROWN));
402 }
403
404 #[test]
405 fn decodes_backlight_info_event() {
406 let mut payload = [0; 16];
407 payload[0] = 8;
408 payload[1] = 5;
409 payload[2] = 5;
410 payload[3] = 2;
411
412 let update = BacklightInfoUpdate::from_payload(&payload).unwrap();
413 assert_eq!(update.nb_levels, 8);
414 assert_eq!(update.current_level, 5);
415 assert_eq!(update.status, BacklightStatus::PermanentManual);
416 assert_eq!(update.effect, BacklightEffect::Breathing);
417 }
418
419 #[test]
420 fn maps_do_not_change_effect_sentinel() {
421 assert_eq!(None::<BacklightEffect>.map_or(0xff, u8::from), 0xff);
422 assert_eq!(Some(BacklightEffect::Waves).map_or(0xff, u8::from), 6);
423 }
424}