Skip to main content

hidpp/feature/
illumination.rs

1//! Implements the `Illumination` feature (ID `0x1990`) for devices with a
2//! controllable illumination light (brightness in Lumens and color temperature
3//! in Kelvin).
4//!
5//! Brightness and color temperature share the same control shape — an info
6//! query, a value get/set, and a level-list get/set — exposed as two parallel
7//! sets of methods. Feature version 1 adds the effective-maximum brightness
8//! query and its events.
9//!
10//! All multi-byte fields in this feature are big-endian.
11
12pub mod event;
13pub mod types;
14
15#[cfg(test)]
16mod tests;
17
18use openlogi_hidpp_derive::Feature;
19
20pub use event::IlluminationEvent;
21pub use types::{
22    BrightnessClampedSource, ControlCapabilities, ControlInfo, IlluminationState, LevelConfig,
23    SetLevels,
24};
25
26use self::types::{be16, illumination_state};
27use crate::{
28    feature::{EventSource, FeatureEndpoint},
29    protocol::v20::{ErrorType, Hidpp20Error},
30};
31
32// Function ids. Color-temperature functions mirror the brightness ones offset by
33// five, but they are spelled out for clarity.
34const FN_GET_ILLUMINATION: u8 = 0;
35const FN_SET_ILLUMINATION: u8 = 1;
36const FN_GET_BRIGHTNESS_INFO: u8 = 2;
37const FN_GET_BRIGHTNESS: u8 = 3;
38const FN_SET_BRIGHTNESS: u8 = 4;
39const FN_GET_BRIGHTNESS_LEVELS: u8 = 5;
40const FN_SET_BRIGHTNESS_LEVELS: u8 = 6;
41const FN_GET_COLOR_TEMPERATURE_INFO: u8 = 7;
42const FN_GET_COLOR_TEMPERATURE: u8 = 8;
43const FN_SET_COLOR_TEMPERATURE: u8 = 9;
44const FN_GET_COLOR_TEMPERATURE_LEVELS: u8 = 10;
45const FN_SET_COLOR_TEMPERATURE_LEVELS: u8 = 11;
46const FN_GET_BRIGHTNESS_EFFECTIVE_MAX: u8 = 12;
47
48/// Implements the `Illumination` / `0x1990` feature.
49#[derive(Feature)]
50#[creatable(id = 0x1990, version = 0)]
51pub struct IlluminationFeature {
52    /// The endpoint this feature talks to.
53    endpoint: FeatureEndpoint,
54
55    /// Publishes decoded events to listeners.
56    events: EventSource<IlluminationEvent>,
57}
58
59impl IlluminationFeature {
60    /// Retrieves whether the illumination is on.
61    pub async fn get_illumination(&self) -> Result<IlluminationState, Hidpp20Error> {
62        let payload = self
63            .endpoint
64            .call(FN_GET_ILLUMINATION, [0; 3])
65            .await?
66            .extend_payload();
67        illumination_state(payload[0])
68    }
69
70    /// Turns the illumination on or off.
71    pub async fn set_illumination(&self, state: IlluminationState) -> Result<(), Hidpp20Error> {
72        self.endpoint
73            .call(FN_SET_ILLUMINATION, [u8::from(state), 0, 0])
74            .await?;
75        Ok(())
76    }
77
78    /// Retrieves the brightness capabilities and range (in Lumens).
79    pub async fn get_brightness_info(&self) -> Result<ControlInfo, Hidpp20Error> {
80        self.read_info(FN_GET_BRIGHTNESS_INFO).await
81    }
82
83    /// Retrieves the current brightness (in Lumens).
84    pub async fn get_brightness(&self) -> Result<u16, Hidpp20Error> {
85        self.read_value(FN_GET_BRIGHTNESS).await
86    }
87
88    /// Sets the brightness (in Lumens).
89    ///
90    /// The value must be within `[min, max]` and on the resolution grid from
91    /// [`Self::get_brightness_info`]. On devices with a dynamic maximum a value
92    /// above the effective maximum is clamped (see
93    /// [`IlluminationEvent::BrightnessClamped`]).
94    pub async fn set_brightness(&self, brightness: u16) -> Result<(), Hidpp20Error> {
95        self.write_value(FN_SET_BRIGHTNESS, brightness).await
96    }
97
98    /// Retrieves the brightness level configuration starting at `start_index`
99    /// (ignored for linear levels).
100    pub async fn get_brightness_levels(
101        &self,
102        start_index: u8,
103    ) -> Result<LevelConfig, Hidpp20Error> {
104        self.read_levels(FN_GET_BRIGHTNESS_LEVELS, start_index)
105            .await
106    }
107
108    /// Writes the brightness level configuration.
109    pub async fn set_brightness_levels(&self, levels: &SetLevels) -> Result<(), Hidpp20Error> {
110        self.write_levels(FN_SET_BRIGHTNESS_LEVELS, levels).await
111    }
112
113    /// Retrieves the current effective maximum brightness (in Lumens), or `0`
114    /// when none is in effect. Requires feature version 1.
115    pub async fn get_brightness_effective_max(&self) -> Result<u16, Hidpp20Error> {
116        self.read_value(FN_GET_BRIGHTNESS_EFFECTIVE_MAX).await
117    }
118
119    /// Retrieves the color-temperature capabilities and range (in Kelvin).
120    pub async fn get_color_temperature_info(&self) -> Result<ControlInfo, Hidpp20Error> {
121        self.read_info(FN_GET_COLOR_TEMPERATURE_INFO).await
122    }
123
124    /// Retrieves the current color temperature (in Kelvin).
125    pub async fn get_color_temperature(&self) -> Result<u16, Hidpp20Error> {
126        self.read_value(FN_GET_COLOR_TEMPERATURE).await
127    }
128
129    /// Sets the color temperature (in Kelvin).
130    ///
131    /// The value must be within `[min, max]` and on the resolution grid from
132    /// [`Self::get_color_temperature_info`].
133    pub async fn set_color_temperature(&self, color_temperature: u16) -> Result<(), Hidpp20Error> {
134        self.write_value(FN_SET_COLOR_TEMPERATURE, color_temperature)
135            .await
136    }
137
138    /// Retrieves the color-temperature level configuration starting at
139    /// `start_index` (ignored for linear levels).
140    pub async fn get_color_temperature_levels(
141        &self,
142        start_index: u8,
143    ) -> Result<LevelConfig, Hidpp20Error> {
144        self.read_levels(FN_GET_COLOR_TEMPERATURE_LEVELS, start_index)
145            .await
146    }
147
148    /// Writes the color-temperature level configuration.
149    pub async fn set_color_temperature_levels(
150        &self,
151        levels: &SetLevels,
152    ) -> Result<(), Hidpp20Error> {
153        self.write_levels(FN_SET_COLOR_TEMPERATURE_LEVELS, levels)
154            .await
155    }
156
157    /// Shared `get<Control>Info` reader.
158    async fn read_info(&self, function: u8) -> Result<ControlInfo, Hidpp20Error> {
159        let payload = self.endpoint.call(function, [0; 3]).await?.extend_payload();
160        Ok(ControlInfo::from_payload(&payload))
161    }
162
163    /// Shared `get<Control>` / effective-max reader for a big-endian `u16`.
164    async fn read_value(&self, function: u8) -> Result<u16, Hidpp20Error> {
165        let payload = self.endpoint.call(function, [0; 3]).await?.extend_payload();
166        Ok(be16(&payload, 0))
167    }
168
169    /// Shared `set<Control>` writer for a big-endian `u16`.
170    async fn write_value(&self, function: u8, value: u16) -> Result<(), Hidpp20Error> {
171        let [hi, lo] = value.to_be_bytes();
172        self.endpoint.call(function, [hi, lo, 0]).await?;
173        Ok(())
174    }
175
176    /// Shared `get<Control>Levels` reader.
177    async fn read_levels(
178        &self,
179        function: u8,
180        start_index: u8,
181    ) -> Result<LevelConfig, Hidpp20Error> {
182        if start_index > 0x0f {
183            return Err(Hidpp20Error::Feature(ErrorType::InvalidArgument));
184        }
185        // The request carries the start index in the high nibble of byte 0.
186        let payload = self
187            .endpoint
188            .call(function, [start_index << 4, 0, 0])
189            .await?
190            .extend_payload();
191        Ok(LevelConfig::from_payload(&payload))
192    }
193
194    /// Shared `set<Control>Levels` writer.
195    async fn write_levels(&self, function: u8, levels: &SetLevels) -> Result<(), Hidpp20Error> {
196        self.endpoint
197            .call_long(function, levels.to_payload()?)
198            .await?;
199        Ok(())
200    }
201}