Skip to main content

hidpp/feature/illumination/
mod.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 std::sync::Arc;
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    channel::{HidppChannel, MessageListenerGuard},
29    event::EventEmitter,
30    feature::{CreatableFeature, EmittingFeature, Feature, FeatureEndpoint, event_payload},
31    protocol::v20::{ErrorType, Hidpp20Error},
32};
33
34// Function ids. Color-temperature functions mirror the brightness ones offset by
35// five, but they are spelled out for clarity.
36const FN_GET_ILLUMINATION: u8 = 0;
37const FN_SET_ILLUMINATION: u8 = 1;
38const FN_GET_BRIGHTNESS_INFO: u8 = 2;
39const FN_GET_BRIGHTNESS: u8 = 3;
40const FN_SET_BRIGHTNESS: u8 = 4;
41const FN_GET_BRIGHTNESS_LEVELS: u8 = 5;
42const FN_SET_BRIGHTNESS_LEVELS: u8 = 6;
43const FN_GET_COLOR_TEMPERATURE_INFO: u8 = 7;
44const FN_GET_COLOR_TEMPERATURE: u8 = 8;
45const FN_SET_COLOR_TEMPERATURE: u8 = 9;
46const FN_GET_COLOR_TEMPERATURE_LEVELS: u8 = 10;
47const FN_SET_COLOR_TEMPERATURE_LEVELS: u8 = 11;
48const FN_GET_BRIGHTNESS_EFFECTIVE_MAX: u8 = 12;
49
50/// Implements the `Illumination` / `0x1990` feature.
51pub struct IlluminationFeature {
52    /// The endpoint this feature talks to.
53    endpoint: FeatureEndpoint,
54
55    /// The emitter used to publish decoded events.
56    emitter: Arc<EventEmitter<IlluminationEvent>>,
57
58    /// Removes the message listener when the feature is dropped.
59    _msg_listener: MessageListenerGuard,
60}
61
62impl CreatableFeature for IlluminationFeature {
63    const ID: u16 = 0x1990;
64    const STARTING_VERSION: u8 = 0;
65
66    fn new(chan: Arc<HidppChannel>, device_index: u8, feature_index: u8) -> Self {
67        let emitter = Arc::new(EventEmitter::new());
68
69        let listener = chan.add_msg_listener_guarded({
70            let emitter = Arc::clone(&emitter);
71
72            move |raw, matched| {
73                let Some((func, payload)) =
74                    event_payload(raw, matched, device_index, feature_index)
75                else {
76                    return;
77                };
78                if let Some(event) = event::decode_event(func.to_lo(), &payload) {
79                    emitter.emit(event);
80                }
81            }
82        });
83
84        Self {
85            endpoint: FeatureEndpoint::new(chan, device_index, feature_index),
86            emitter,
87            _msg_listener: listener,
88        }
89    }
90}
91
92impl Feature for IlluminationFeature {}
93
94impl EmittingFeature<IlluminationEvent> for IlluminationFeature {
95    fn listen(&self) -> async_channel::Receiver<IlluminationEvent> {
96        self.emitter.create_receiver()
97    }
98}
99
100impl IlluminationFeature {
101    /// Retrieves whether the illumination is on.
102    pub async fn get_illumination(&self) -> Result<IlluminationState, Hidpp20Error> {
103        let payload = self
104            .endpoint
105            .call(FN_GET_ILLUMINATION, [0; 3])
106            .await?
107            .extend_payload();
108        illumination_state(payload[0])
109    }
110
111    /// Turns the illumination on or off.
112    pub async fn set_illumination(&self, state: IlluminationState) -> Result<(), Hidpp20Error> {
113        self.endpoint
114            .call(FN_SET_ILLUMINATION, [u8::from(state), 0, 0])
115            .await?;
116        Ok(())
117    }
118
119    /// Retrieves the brightness capabilities and range (in Lumens).
120    pub async fn get_brightness_info(&self) -> Result<ControlInfo, Hidpp20Error> {
121        self.read_info(FN_GET_BRIGHTNESS_INFO).await
122    }
123
124    /// Retrieves the current brightness (in Lumens).
125    pub async fn get_brightness(&self) -> Result<u16, Hidpp20Error> {
126        self.read_value(FN_GET_BRIGHTNESS).await
127    }
128
129    /// Sets the brightness (in Lumens).
130    ///
131    /// The value must be within `[min, max]` and on the resolution grid from
132    /// [`Self::get_brightness_info`]. On devices with a dynamic maximum a value
133    /// above the effective maximum is clamped (see
134    /// [`IlluminationEvent::BrightnessClamped`]).
135    pub async fn set_brightness(&self, brightness: u16) -> Result<(), Hidpp20Error> {
136        self.write_value(FN_SET_BRIGHTNESS, brightness).await
137    }
138
139    /// Retrieves the brightness level configuration starting at `start_index`
140    /// (ignored for linear levels).
141    pub async fn get_brightness_levels(
142        &self,
143        start_index: u8,
144    ) -> Result<LevelConfig, Hidpp20Error> {
145        self.read_levels(FN_GET_BRIGHTNESS_LEVELS, start_index)
146            .await
147    }
148
149    /// Writes the brightness level configuration.
150    pub async fn set_brightness_levels(&self, levels: &SetLevels) -> Result<(), Hidpp20Error> {
151        self.write_levels(FN_SET_BRIGHTNESS_LEVELS, levels).await
152    }
153
154    /// Retrieves the current effective maximum brightness (in Lumens), or `0`
155    /// when none is in effect. Requires feature version 1.
156    pub async fn get_brightness_effective_max(&self) -> Result<u16, Hidpp20Error> {
157        self.read_value(FN_GET_BRIGHTNESS_EFFECTIVE_MAX).await
158    }
159
160    /// Retrieves the color-temperature capabilities and range (in Kelvin).
161    pub async fn get_color_temperature_info(&self) -> Result<ControlInfo, Hidpp20Error> {
162        self.read_info(FN_GET_COLOR_TEMPERATURE_INFO).await
163    }
164
165    /// Retrieves the current color temperature (in Kelvin).
166    pub async fn get_color_temperature(&self) -> Result<u16, Hidpp20Error> {
167        self.read_value(FN_GET_COLOR_TEMPERATURE).await
168    }
169
170    /// Sets the color temperature (in Kelvin).
171    ///
172    /// The value must be within `[min, max]` and on the resolution grid from
173    /// [`Self::get_color_temperature_info`].
174    pub async fn set_color_temperature(&self, color_temperature: u16) -> Result<(), Hidpp20Error> {
175        self.write_value(FN_SET_COLOR_TEMPERATURE, color_temperature)
176            .await
177    }
178
179    /// Retrieves the color-temperature level configuration starting at
180    /// `start_index` (ignored for linear levels).
181    pub async fn get_color_temperature_levels(
182        &self,
183        start_index: u8,
184    ) -> Result<LevelConfig, Hidpp20Error> {
185        self.read_levels(FN_GET_COLOR_TEMPERATURE_LEVELS, start_index)
186            .await
187    }
188
189    /// Writes the color-temperature level configuration.
190    pub async fn set_color_temperature_levels(
191        &self,
192        levels: &SetLevels,
193    ) -> Result<(), Hidpp20Error> {
194        self.write_levels(FN_SET_COLOR_TEMPERATURE_LEVELS, levels)
195            .await
196    }
197
198    /// Shared `get<Control>Info` reader.
199    async fn read_info(&self, function: u8) -> Result<ControlInfo, Hidpp20Error> {
200        let payload = self.endpoint.call(function, [0; 3]).await?.extend_payload();
201        Ok(ControlInfo::from_payload(&payload))
202    }
203
204    /// Shared `get<Control>` / effective-max reader for a big-endian `u16`.
205    async fn read_value(&self, function: u8) -> Result<u16, Hidpp20Error> {
206        let payload = self.endpoint.call(function, [0; 3]).await?.extend_payload();
207        Ok(be16(&payload, 0))
208    }
209
210    /// Shared `set<Control>` writer for a big-endian `u16`.
211    async fn write_value(&self, function: u8, value: u16) -> Result<(), Hidpp20Error> {
212        let [hi, lo] = value.to_be_bytes();
213        self.endpoint.call(function, [hi, lo, 0]).await?;
214        Ok(())
215    }
216
217    /// Shared `get<Control>Levels` reader.
218    async fn read_levels(
219        &self,
220        function: u8,
221        start_index: u8,
222    ) -> Result<LevelConfig, Hidpp20Error> {
223        if start_index > 0x0f {
224            return Err(Hidpp20Error::Feature(ErrorType::InvalidArgument));
225        }
226        // The request carries the start index in the high nibble of byte 0.
227        let payload = self
228            .endpoint
229            .call(function, [start_index << 4, 0, 0])
230            .await?
231            .extend_payload();
232        Ok(LevelConfig::from_payload(&payload))
233    }
234
235    /// Shared `set<Control>Levels` writer.
236    async fn write_levels(&self, function: u8, levels: &SetLevels) -> Result<(), Hidpp20Error> {
237        self.endpoint
238            .call_long(function, levels.to_payload()?)
239            .await?;
240        Ok(())
241    }
242}