Skip to main content

hidpp/feature/illumination/
types.rs

1//! Domain types for the `Illumination` feature (`0x1990`).
2
3use num_enum::{FromPrimitive, IntoPrimitive, TryFromPrimitive};
4
5use crate::protocol::v20::{ErrorType, Hidpp20Error};
6
7/// Reads a big-endian `u16` at `offset` of a payload.
8pub(super) fn be16(payload: &[u8; 16], offset: usize) -> u16 {
9    u16::from_be_bytes([payload[offset], payload[offset + 1]])
10}
11
12bitflags::bitflags! {
13    /// Capabilities of an illumination control (brightness or color
14    /// temperature), from `getBrightnessInfo` / `getColorTemperatureInfo`.
15    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
16    #[cfg_attr(feature = "serde", derive(serde::Serialize))]
17    pub struct ControlCapabilities: u8 {
18        /// The control emits change events.
19        const HAS_EVENTS = 1 << 0;
20        /// The control supports linear (min/max/step) levels.
21        const HAS_LINEAR_LEVELS = 1 << 1;
22        /// The control supports an explicit list of non-linear levels.
23        const HAS_NON_LINEAR_LEVELS = 1 << 2;
24        /// The control has a dynamic effective maximum (brightness only).
25        const HAS_DYNAMIC_MAXIMUM = 1 << 3;
26    }
27}
28
29/// Capabilities and range of an illumination control.
30///
31/// Values are in Lumens for brightness and Kelvin for color temperature.
32#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
33#[cfg_attr(feature = "serde", derive(serde::Serialize))]
34#[non_exhaustive]
35pub struct ControlInfo {
36    /// Control capabilities.
37    pub capabilities: ControlCapabilities,
38    /// Minimum value. When `min == max` only one setting exists and the
39    /// corresponding setter is unsupported.
40    pub min: u16,
41    /// Maximum value.
42    pub max: u16,
43    /// Resolution: valid values satisfy `(value - min) % resolution == 0`.
44    pub resolution: u16,
45    /// Maximum number of non-linear levels (`0` if non-linear levels are
46    /// unsupported).
47    pub max_levels: u8,
48}
49
50impl ControlInfo {
51    pub(super) fn from_payload(payload: &[u8; 16]) -> Self {
52        Self {
53            capabilities: ControlCapabilities::from_bits_retain(payload[0]),
54            min: be16(payload, 1),
55            max: be16(payload, 3),
56            resolution: be16(payload, 5),
57            max_levels: payload[7] & 0x0f,
58        }
59    }
60}
61
62/// The level configuration of an illumination control.
63///
64/// A control exposes its selectable levels either as a linear `min/max/step`
65/// range or as an explicit list of non-linear values.
66#[derive(Clone, Debug, PartialEq, Eq, Hash)]
67#[cfg_attr(feature = "serde", derive(serde::Serialize))]
68pub enum LevelConfig {
69    /// Evenly spaced levels from `min` to `max` (inclusive) in steps of `step`.
70    Linear {
71        /// Lowest level value.
72        min: u16,
73        /// Highest level value.
74        max: u16,
75        /// Spacing between adjacent levels.
76        step: u16,
77    },
78    /// An explicit list of level values.
79    NonLinear {
80        /// Zero-based index of the first returned value within the full list.
81        start_index: u8,
82        /// Total number of available levels.
83        level_count: u8,
84        /// The values in this page (`1..=7` of them).
85        values: Vec<u16>,
86    },
87}
88
89impl LevelConfig {
90    pub(super) fn from_payload(payload: &[u8; 16]) -> Self {
91        let flags = payload[0];
92        if flags & 1 != 0 {
93            LevelConfig::Linear {
94                min: be16(payload, 2),
95                max: be16(payload, 4),
96                step: be16(payload, 6),
97            }
98        } else {
99            let valid_count = usize::from((flags >> 5) & 0x07);
100            let values = (0..valid_count).map(|i| be16(payload, 2 + 2 * i)).collect();
101            LevelConfig::NonLinear {
102                start_index: payload[1] >> 4,
103                level_count: payload[1] & 0x0f,
104                values,
105            }
106        }
107    }
108}
109
110/// A level configuration to write with `setBrightnessLevels` /
111/// `setColorTemperatureLevels`.
112#[derive(Clone, Debug, PartialEq, Eq, Hash)]
113#[cfg_attr(feature = "serde", derive(serde::Serialize))]
114pub enum SetLevels {
115    /// Reset the level configuration to the factory defaults.
116    Reset,
117    /// Configure evenly spaced linear levels.
118    Linear {
119        /// Lowest level value.
120        min: u16,
121        /// Highest level value.
122        max: u16,
123        /// Spacing between adjacent levels.
124        step: u16,
125    },
126    /// Configure an explicit list of non-linear levels.
127    NonLinear {
128        /// Zero-based index at which `values` are written.
129        start_index: u8,
130        /// Total number of available levels (`0` resets the count to the factory
131        /// default).
132        level_count: u8,
133        /// The monotonically increasing values to write (`1..=7` of them).
134        values: Vec<u16>,
135    },
136}
137
138impl SetLevels {
139    /// Encodes this configuration into a request payload.
140    pub(super) fn to_payload(&self) -> Result<[u8; 16], Hidpp20Error> {
141        let mut args = [0u8; 16];
142        match self {
143            SetLevels::Reset => {
144                // bit1 = reset; every other field is ignored by the device.
145                args[0] = 1 << 1;
146            }
147            SetLevels::Linear { min, max, step } => {
148                args[0] = 1; // bit0 = linear
149                args[2..4].copy_from_slice(&min.to_be_bytes());
150                args[4..6].copy_from_slice(&max.to_be_bytes());
151                args[6..8].copy_from_slice(&step.to_be_bytes());
152            }
153            SetLevels::NonLinear {
154                start_index,
155                level_count,
156                values,
157            } => {
158                if !(1..=7).contains(&values.len()) || *start_index > 0x0f || *level_count > 0x0f {
159                    return Err(Hidpp20Error::Feature(ErrorType::InvalidArgument));
160                }
161                let valid_count = (values.len() as u8) & 0x07;
162                args[0] = valid_count << 5; // linear = 0, reset = 0
163                args[1] = (start_index << 4) | (level_count & 0x0f);
164                for (i, value) in values.iter().take(7).enumerate() {
165                    args[2 + 2 * i..4 + 2 * i].copy_from_slice(&value.to_be_bytes());
166                }
167            }
168        }
169        Ok(args)
170    }
171}
172
173/// On/off state of the illumination.
174#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
175#[cfg_attr(feature = "serde", derive(serde::Serialize))]
176#[non_exhaustive]
177#[repr(u8)]
178pub enum IlluminationState {
179    /// Illumination is off.
180    Off = 0,
181    /// Illumination is on.
182    On = 1,
183}
184
185impl From<bool> for IlluminationState {
186    fn from(value: bool) -> Self {
187        if value { Self::On } else { Self::Off }
188    }
189}
190
191/// What caused a [`brightness clamp`](super::event::IlluminationEvent::BrightnessClamped).
192#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, FromPrimitive)]
193#[cfg_attr(feature = "serde", derive(serde::Serialize))]
194#[non_exhaustive]
195#[repr(u8)]
196pub enum BrightnessClampedSource {
197    /// The source is unknown.
198    Unknown = 0,
199    /// A HID++ `setBrightness` request triggered the clamp.
200    HidPlusPlus = 1,
201    /// A hardware button triggered the clamp.
202    Button = 2,
203    /// A source this crate does not model; carries the raw byte.
204    #[num_enum(catch_all)]
205    Other(u8),
206}
207
208/// Decodes the on/off state bit shared by `getIllumination` and its event.
209pub(super) fn illumination_state(byte: u8) -> Result<IlluminationState, Hidpp20Error> {
210    IlluminationState::try_from(byte & 1).map_err(|_| Hidpp20Error::UnsupportedResponse)
211}