use std::sync::Arc;
use num_enum::{IntoPrimitive, TryFromPrimitive};
use crate::{
channel::{HidppChannel, MessageListenerGuard},
event::EventEmitter,
feature::{CreatableFeature, EmittingFeature, Feature, FeatureEndpoint, event_payload},
protocol::v20::Hidpp20Error,
};
const EFFECT_UNCHANGED: u8 = 0xff;
const MODE_SHIFT: u16 = 3;
const MODE_MASK: u16 = 0b11 << MODE_SHIFT;
bitflags::bitflags! {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct BacklightOptions: u16 {
const WOW = 1 << 0;
const CROWN = 1 << 1;
const PWR_SAVE = 1 << 2;
const WOW_SUPPORTED = 1 << 8;
const CROWN_SUPPORTED = 1 << 9;
const PWR_SAVE_SUPPORTED = 1 << 10;
const AUTO_MODE_SUPPORTED = 1 << 11;
const TEMP_MANUAL_SUPPORTED = 1 << 12;
const PERM_MANUAL_SUPPORTED = 1 << 13;
}
}
bitflags::bitflags! {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct BacklightEffectList: u16 {
const STATIC = 1 << 0;
const NONE = 1 << 1;
const BREATHING = 1 << 2;
const CONTRAST = 1 << 3;
const REACTION = 1 << 4;
const RANDOM = 1 << 5;
const WAVES = 1 << 6;
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
#[repr(u8)]
pub enum BacklightMode {
None = 0,
Automatic = 1,
TemporaryManual = 2,
PermanentManual = 3,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
#[repr(u8)]
pub enum BacklightEffect {
Static = 0,
None = 1,
Breathing = 2,
Contrast = 3,
Reaction = 4,
Random = 5,
Waves = 6,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
#[repr(u8)]
pub enum BacklightStatus {
DisabledBySoftware = 0,
DisabledByCriticalBattery = 1,
AlsAutomatic = 2,
AlsSaturated = 3,
TemporaryManual = 4,
PermanentManual = 5,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct BacklightConfig {
pub enabled: bool,
pub options: BacklightOptions,
pub mode: BacklightMode,
pub effect_list: BacklightEffectList,
pub current_level: u8,
pub duration_hands_out: u16,
pub duration_hands_in: u16,
pub duration_powered: u16,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct SetBacklightConfig {
pub enabled: bool,
pub options: BacklightOptions,
pub mode: BacklightMode,
pub effect: Option<BacklightEffect>,
pub current_level: u8,
pub duration_hands_out: u16,
pub duration_hands_in: u16,
pub duration_powered: u16,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct BacklightInfo {
pub nb_levels: u8,
pub current_level: u8,
pub status: BacklightStatus,
pub effect: BacklightEffect,
pub oob_duration_hands_out: u16,
pub oob_duration_hands_in: u16,
pub oob_duration_powered: u16,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum BacklightEvent {
InfoChanged(BacklightInfoUpdate),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct BacklightInfoUpdate {
pub nb_levels: u8,
pub current_level: u8,
pub status: BacklightStatus,
pub effect: BacklightEffect,
}
pub struct BacklightFeature {
endpoint: FeatureEndpoint,
emitter: Arc<EventEmitter<BacklightEvent>>,
_msg_listener: MessageListenerGuard,
}
impl CreatableFeature for BacklightFeature {
const ID: u16 = 0x1982;
const STARTING_VERSION: u8 = 3;
fn new(chan: Arc<HidppChannel>, device_index: u8, feature_index: u8) -> Self {
let emitter = Arc::new(EventEmitter::new());
let listener = chan.add_msg_listener_guarded({
let emitter = Arc::clone(&emitter);
move |raw, matched| {
let Some((func, payload)) =
event_payload(raw, matched, device_index, feature_index)
else {
return;
};
if func.to_lo() != 0 {
return;
}
if let Ok(update) = BacklightInfoUpdate::from_payload(&payload) {
emitter.emit(BacklightEvent::InfoChanged(update));
}
}
});
Self {
endpoint: FeatureEndpoint::new(chan, device_index, feature_index),
emitter,
_msg_listener: listener,
}
}
}
impl Feature for BacklightFeature {}
impl EmittingFeature<BacklightEvent> for BacklightFeature {
fn listen(&self) -> async_channel::Receiver<BacklightEvent> {
self.emitter.create_receiver()
}
}
impl BacklightFeature {
pub async fn get_backlight_config(&self) -> Result<BacklightConfig, Hidpp20Error> {
let payload = self.endpoint.call(0, [0; 3]).await?.extend_payload();
let raw_options = u16::from_le_bytes([payload[1], payload[2]]);
Ok(BacklightConfig {
enabled: payload[0] & 1 != 0,
options: BacklightOptions::from_bits_retain(raw_options & !MODE_MASK),
mode: BacklightMode::try_from(((raw_options & MODE_MASK) >> MODE_SHIFT) as u8)
.map_err(|_| Hidpp20Error::UnsupportedResponse)?,
effect_list: BacklightEffectList::from_bits_retain(u16::from_le_bytes([
payload[3], payload[4],
])),
current_level: payload[5],
duration_hands_out: u16::from_le_bytes([payload[6], payload[7]]),
duration_hands_in: u16::from_le_bytes([payload[8], payload[9]]),
duration_powered: u16::from_le_bytes([payload[10], payload[11]]),
})
}
pub async fn set_backlight_config(
&self,
config: SetBacklightConfig,
) -> Result<(), Hidpp20Error> {
let options_byte = (config.options.bits()
& (BacklightOptions::WOW | BacklightOptions::CROWN | BacklightOptions::PWR_SAVE).bits())
as u8
| (u8::from(config.mode) << MODE_SHIFT);
let [out_lo, out_hi] = config.duration_hands_out.to_le_bytes();
let [in_lo, in_hi] = config.duration_hands_in.to_le_bytes();
let [pwr_lo, pwr_hi] = config.duration_powered.to_le_bytes();
let mut args = [0; 16];
args[..10].copy_from_slice(&[
u8::from(config.enabled),
options_byte,
config.effect.map_or(EFFECT_UNCHANGED, u8::from),
config.current_level,
out_lo,
out_hi,
in_lo,
in_hi,
pwr_lo,
pwr_hi,
]);
self.endpoint.call_long(1, args).await?;
Ok(())
}
pub async fn get_backlight_info(&self) -> Result<BacklightInfo, Hidpp20Error> {
let payload = self.endpoint.call(2, [0; 3]).await?.extend_payload();
Ok(BacklightInfo {
nb_levels: payload[0],
current_level: payload[1],
status: BacklightStatus::try_from(payload[2])
.map_err(|_| Hidpp20Error::UnsupportedResponse)?,
effect: BacklightEffect::try_from(payload[3])
.map_err(|_| Hidpp20Error::UnsupportedResponse)?,
oob_duration_hands_out: u16::from_le_bytes([payload[4], payload[5]]),
oob_duration_hands_in: u16::from_le_bytes([payload[6], payload[7]]),
oob_duration_powered: u16::from_le_bytes([payload[8], payload[9]]),
})
}
pub async fn set_backlight_effect(&self, effect: BacklightEffect) -> Result<(), Hidpp20Error> {
self.endpoint.call(3, [effect.into(), 0, 0]).await?;
Ok(())
}
}
impl BacklightInfoUpdate {
fn from_payload(payload: &[u8; 16]) -> Result<Self, Hidpp20Error> {
Ok(Self {
nb_levels: payload[0],
current_level: payload[1],
status: BacklightStatus::try_from(payload[2])
.map_err(|_| Hidpp20Error::UnsupportedResponse)?,
effect: BacklightEffect::try_from(payload[3])
.map_err(|_| Hidpp20Error::UnsupportedResponse)?,
})
}
}
#[cfg(test)]
mod tests {
use super::{
BacklightEffect, BacklightInfoUpdate, BacklightMode, BacklightOptions, BacklightStatus,
};
#[test]
fn decodes_options_and_mode_split() {
let raw = BacklightOptions::WOW.bits()
| (u16::from(u8::from(BacklightMode::PermanentManual)) << 3)
| BacklightOptions::AUTO_MODE_SUPPORTED.bits();
let mode = BacklightMode::try_from(((raw & (0b11 << 3)) >> 3) as u8).unwrap();
let options = BacklightOptions::from_bits_retain(raw & !(0b11 << 3));
assert_eq!(mode, BacklightMode::PermanentManual);
assert!(options.contains(BacklightOptions::WOW));
assert!(options.contains(BacklightOptions::AUTO_MODE_SUPPORTED));
assert!(!options.contains(BacklightOptions::PWR_SAVE));
assert!(!options.contains(BacklightOptions::CROWN));
}
#[test]
fn decodes_backlight_info_event() {
let mut payload = [0; 16];
payload[0] = 8;
payload[1] = 5;
payload[2] = 5;
payload[3] = 2;
let update = BacklightInfoUpdate::from_payload(&payload).unwrap();
assert_eq!(update.nb_levels, 8);
assert_eq!(update.current_level, 5);
assert_eq!(update.status, BacklightStatus::PermanentManual);
assert_eq!(update.effect, BacklightEffect::Breathing);
}
#[test]
fn maps_do_not_change_effect_sentinel() {
assert_eq!(None::<BacklightEffect>.map_or(0xff, u8::from), 0xff);
assert_eq!(Some(BacklightEffect::Waves).map_or(0xff, u8::from), 6);
}
}