Skip to main content

hidpp/feature/
color_led_effects.rs

1//! Implements the `ColorLedEffects` feature (ID `0x8070`, version 7), the
2//! per-zone RGB effect engine used by Logitech keyboards and mice.
3//!
4//! Each device exposes one or more LED *zones*; each zone supports a set of
5//! *effects* (fixed color, breathing, color wave, …). An effect is applied with
6//! [`set_zone_effect`](ColorLedEffectsFeature::set_zone_effect), whose ten
7//! parameter bytes have effect-specific meaning, and can be stored volatilely or
8//! in EEPROM via [`Persistence`].
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::ColorLedEffectsEvent;
21pub use types::{
22    ColorLedInfo, CyclingDirection, EffectId, EffectSettings, ExtCapabilities, LedBinIndex,
23    LedBinInfo, LocationEffect, NvCapabilities, NvCapabilityState, NvConfig, Persistence,
24    PersistenceSource, PersistencyCapabilities, Rgb, SwControl, SwControlState,
25    ZONE_EFFECT_PARAM_COUNT, ZoneEffect, ZoneEffectInfo, ZoneInfo,
26};
27
28use self::types::be16;
29use crate::{
30    feature::{EventSource, FeatureEndpoint},
31    protocol::v20::{ErrorType, Hidpp20Error},
32};
33
34/// Implements the `ColorLedEffects` / `0x8070` feature.
35#[derive(Feature)]
36#[creatable(id = 0x8070, version = 0)]
37pub struct ColorLedEffectsFeature {
38    /// The endpoint this feature talks to.
39    endpoint: FeatureEndpoint,
40
41    /// Publishes decoded events to listeners.
42    events: EventSource<ColorLedEffectsEvent>,
43}
44
45impl ColorLedEffectsFeature {
46    /// Retrieves the zone count and capability bitmasks.
47    pub async fn get_info(&self) -> Result<ColorLedInfo, Hidpp20Error> {
48        let payload = self.endpoint.call(0, [0; 3]).await?.extend_payload();
49        Ok(ColorLedInfo::from_payload(&payload))
50    }
51
52    /// Retrieves information about `zone_index`.
53    pub async fn get_zone_info(&self, zone_index: u8) -> Result<ZoneInfo, Hidpp20Error> {
54        let payload = self
55            .endpoint
56            .call(1, [zone_index, 0, 0])
57            .await?
58            .extend_payload();
59        ZoneInfo::from_payload(&payload)
60    }
61
62    /// Retrieves information about effect `zone_effect_index` of `zone_index`.
63    pub async fn get_zone_effect_info(
64        &self,
65        zone_index: u8,
66        zone_effect_index: u8,
67    ) -> Result<ZoneEffectInfo, Hidpp20Error> {
68        let payload = self
69            .endpoint
70            .call(2, [zone_index, zone_effect_index, 0])
71            .await?
72            .extend_payload();
73        ZoneEffectInfo::from_payload(&payload)
74    }
75
76    /// Applies effect `zone_effect_index` to `zone_index` with effect-specific
77    /// `params`.
78    ///
79    /// The meaning of each parameter byte depends on the effect's
80    /// [`EffectId`] (discoverable with [`Self::get_zone_effect_info`]). For
81    /// example, the [`EffectId::FixedColor`] effect uses the first three
82    /// parameters as red, green and blue.
83    pub async fn set_zone_effect(
84        &self,
85        zone_index: u8,
86        zone_effect_index: u8,
87        params: [u8; ZONE_EFFECT_PARAM_COUNT],
88        persistence: Persistence,
89    ) -> Result<(), Hidpp20Error> {
90        let mut args = [0; 16];
91        args[0] = zone_index;
92        args[1] = zone_effect_index;
93        args[2..2 + ZONE_EFFECT_PARAM_COUNT].copy_from_slice(&params);
94        args[12] = persistence.into();
95        self.endpoint.call_long(3, args).await?;
96        Ok(())
97    }
98
99    /// Reads one non-volatile configuration `capability`.
100    ///
101    /// Exactly one [`NvCapabilities`] bit must be set.
102    pub async fn get_nv_config(
103        &self,
104        capability: NvCapabilities,
105    ) -> Result<NvConfig, Hidpp20Error> {
106        validate_single_nv_capability(capability)?;
107        let [cap_hi, cap_lo] = capability.bits().to_be_bytes();
108        let payload = self
109            .endpoint
110            .call(4, [cap_hi, cap_lo, 0])
111            .await?
112            .extend_payload();
113        Ok(NvConfig {
114            capability: NvCapabilities::from_bits_retain(be16(&payload, 0)),
115            state: NvCapabilityState::try_from(payload[2])
116                .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
117            param1: payload[3],
118            param2: payload[4],
119        })
120    }
121
122    /// Writes one non-volatile configuration entry (to EEPROM, so use sparingly).
123    pub async fn set_nv_config(
124        &self,
125        capability: NvCapabilities,
126        state: NvCapabilityState,
127        param1: u8,
128        param2: u8,
129    ) -> Result<(), Hidpp20Error> {
130        validate_single_nv_capability(capability)?;
131        let [cap_hi, cap_lo] = capability.bits().to_be_bytes();
132        let mut args = [0; 16];
133        args[..5].copy_from_slice(&[cap_hi, cap_lo, state.into(), param1, param2]);
134        self.endpoint.call_long(5, args).await?;
135        Ok(())
136    }
137
138    /// Reads manufacturing LED bin information.
139    pub async fn get_led_bin_info(
140        &self,
141        zone_index: u8,
142        led_bin_index: LedBinIndex,
143    ) -> Result<LedBinInfo, Hidpp20Error> {
144        let payload = self
145            .endpoint
146            .call(6, [zone_index, led_bin_index.into(), 0])
147            .await?
148            .extend_payload();
149        LedBinInfo::from_payload(&payload)
150    }
151
152    /// Retrieves whether firmware or software owns the LEDs.
153    pub async fn get_sw_control(&self) -> Result<SwControlState, Hidpp20Error> {
154        let payload = self.endpoint.call(7, [0; 3]).await?.extend_payload();
155        Ok(SwControlState {
156            control: SwControl::try_from(payload[0])
157                .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
158            sync_events: payload[1] != 0,
159        })
160    }
161
162    /// Takes or releases software control of the LEDs.
163    ///
164    /// `sync_events` enables the [`ColorLedEffectsEvent::SyncEffect`] event. This
165    /// is not stored in EEPROM.
166    pub async fn set_sw_control(
167        &self,
168        control: SwControl,
169        sync_events: bool,
170    ) -> Result<(), Hidpp20Error> {
171        self.endpoint
172            .call(8, [control.into(), u8::from(sync_events), 0])
173            .await?;
174        Ok(())
175    }
176
177    /// Reads the effect settings of `zone_index`.
178    ///
179    /// Not supported when [`ExtCapabilities::NO_GET_EFFECT_SETTINGS`] is set.
180    pub async fn get_effect_settings(
181        &self,
182        zone_index: u8,
183        source: PersistenceSource,
184    ) -> Result<EffectSettings, Hidpp20Error> {
185        let payload = self
186            .endpoint
187            .call(9, [zone_index, source.into(), 0])
188            .await?
189            .extend_payload();
190        Ok(EffectSettings::from_payload(&payload))
191    }
192
193    /// Clears the effect settings of `zone_index`, reverting it to the default
194    /// mode.
195    pub async fn clear_effect_settings(&self, zone_index: u8) -> Result<(), Hidpp20Error> {
196        self.endpoint.call(10, [zone_index, 0, 0]).await?;
197        Ok(())
198    }
199
200    /// Sets the color-cycling direction.
201    pub async fn set_cycling_direction(
202        &self,
203        direction: CyclingDirection,
204    ) -> Result<(), Hidpp20Error> {
205        self.endpoint.call(11, [direction.into(), 0, 0]).await?;
206        Ok(())
207    }
208
209    /// Retrieves the color currently displayed by `zone_index`.
210    pub async fn get_current_color(&self, zone_index: u8) -> Result<Rgb, Hidpp20Error> {
211        let payload = self
212            .endpoint
213            .call(12, [zone_index, 0, 0])
214            .await?
215            .extend_payload();
216        Ok(Rgb {
217            red: payload[1],
218            green: payload[2],
219            blue: payload[3],
220        })
221    }
222
223    /// Synchronizes effect timing across devices by applying a `drift_value`
224    /// correction (milliseconds).
225    ///
226    /// Valid only while sync events are enabled. A `zone_index` of `0xff` targets
227    /// all zones.
228    pub async fn synchronize_effect(
229        &self,
230        zone_index: u8,
231        drift_value: i16,
232    ) -> Result<(), Hidpp20Error> {
233        let [drift_hi, drift_lo] = drift_value.to_be_bytes();
234        let mut args = [0; 16];
235        args[..4].copy_from_slice(&[zone_index, 0, drift_hi, drift_lo]);
236        self.endpoint.call_long(13, args).await?;
237        Ok(())
238    }
239
240    /// Retrieves the currently configured effect of `zone_index`.
241    ///
242    /// Requires [`ExtCapabilities::GET_ZONE_EFFECT`].
243    pub async fn get_zone_effect(
244        &self,
245        zone_index: u8,
246        source: PersistenceSource,
247    ) -> Result<ZoneEffect, Hidpp20Error> {
248        let payload = self
249            .endpoint
250            .call(14, [zone_index, source.into(), 0])
251            .await?
252            .extend_payload();
253        Ok(ZoneEffect::from_payload(&payload))
254    }
255
256    /// Stores manufacturing LED bin information and returns the device's echo.
257    ///
258    /// Requires [`ExtCapabilities::SET_LED_BIN_INFO`].
259    pub async fn set_led_bin_info(&self, info: &LedBinInfo) -> Result<LedBinInfo, Hidpp20Error> {
260        let mut args = [0; 16];
261        args[0] = info.zone_index;
262        args[1] = info.led_bin_index.into();
263        args[2..4].copy_from_slice(&info.red.to_be_bytes());
264        args[4..6].copy_from_slice(&info.green.to_be_bytes());
265        args[6..8].copy_from_slice(&info.blue.to_be_bytes());
266        args[8..10].copy_from_slice(&info.white.to_be_bytes());
267        let payload = self.endpoint.call_long(15, args).await?.extend_payload();
268        LedBinInfo::from_payload(&payload)
269    }
270}
271
272fn validate_single_nv_capability(capability: NvCapabilities) -> Result<(), Hidpp20Error> {
273    if capability.bits().count_ones() != 1 {
274        return Err(Hidpp20Error::Feature(ErrorType::InvalidArgument));
275    }
276    Ok(())
277}