Skip to main content

hidpp/feature/
rgb_effects.rs

1//! Implements the `RgbEffects` feature (ID `0x8071`, version 4), the modern
2//! per-cluster RGB effect engine (successor to
3//! [`ColorLedEffects`](super::color_led_effects), `0x8070`).
4//!
5//! A device groups its LEDs into *clusters*, each supporting a set of *effects*.
6//! [`get_device_info`](RgbEffectsFeature::get_device_info),
7//! [`get_cluster_info`](RgbEffectsFeature::get_cluster_info) and
8//! [`get_effect_info`](RgbEffectsFeature::get_effect_info) decode the three
9//! general-info modes of the polymorphic `getInfo` function; effects are applied
10//! with [`set_rgb_cluster_effect`](RgbEffectsFeature::set_rgb_cluster_effect).
11//!
12//! Software must first take control with
13//! [`set_sw_control`](RgbEffectsFeature::set_sw_control) before applying effects
14//! or power modes, or those calls return a "not allowed" error.
15//!
16//! All multi-byte fields in this feature are big-endian.
17
18pub mod event;
19pub mod types;
20
21#[cfg(test)]
22mod tests;
23
24use openlogi_hidpp_derive::Feature;
25
26pub use event::RgbEffectsEvent;
27pub use types::{
28    ActivityEventType, CLUSTER_EFFECT_PARAM_COUNT, DisplayPersistencyCapabilities,
29    EventsNotificationFlags, LED_BIN_PARAM_COUNT, LedBinIndex, ONBOARD_INFO_PARAM_COUNT,
30    PowerModeTarget, RgbClusterInfo, RgbDeviceInfo, RgbEffectInfo, RgbExtCapabilities,
31    RgbNvCapabilities, RgbNvConfig, RgbPersistence, RgbPowerMode, RgbPowerModeConfig, RgbSwControl,
32    SlotInfoType, SwControlFlags,
33};
34
35use self::types::{ALL_CLUSTERS, ALL_EFFECTS, GetOrSet, be16};
36use crate::{
37    feature::{EventSource, FeatureEndpoint},
38    protocol::v20::Hidpp20Error,
39};
40
41/// `typeOfInfo` value selecting general info in `getInfo`.
42const TYPE_GENERAL_INFO: u8 = 0x00;
43/// `typeOfInfo` value selecting onboard-stored effect info in `getInfo`.
44const TYPE_ONBOARD_EFFECT: u8 = 0x01;
45/// `getOrSet` value requesting a backup read in `manageRgbLedBinInfo`.
46const GET_BACKUP: u8 = 0x02;
47/// Bit offset of the power-mode target in the `setRgbClusterEffect` flags byte.
48const POWER_TARGET_SHIFT: u8 = 2;
49
50/// Implements the `RgbEffects` / `0x8071` feature.
51#[derive(Feature)]
52#[creatable(id = 0x8071, version = 0)]
53pub struct RgbEffectsFeature {
54    /// The endpoint this feature talks to.
55    endpoint: FeatureEndpoint,
56
57    /// Publishes decoded events to listeners.
58    events: EventSource<RgbEffectsEvent>,
59}
60
61impl RgbEffectsFeature {
62    /// Retrieves device-level RGB information (`getInfo` device mode).
63    pub async fn get_device_info(&self) -> Result<RgbDeviceInfo, Hidpp20Error> {
64        let payload = self
65            .endpoint
66            .call(0, [ALL_CLUSTERS, ALL_EFFECTS, TYPE_GENERAL_INFO])
67            .await?
68            .extend_payload();
69        Ok(RgbDeviceInfo::from_payload(&payload))
70    }
71
72    /// Retrieves cluster-level information for `cluster_index` (`getInfo` cluster
73    /// mode).
74    pub async fn get_cluster_info(
75        &self,
76        cluster_index: u8,
77    ) -> Result<RgbClusterInfo, Hidpp20Error> {
78        let payload = self
79            .endpoint
80            .call(0, [cluster_index, ALL_EFFECTS, TYPE_GENERAL_INFO])
81            .await?
82            .extend_payload();
83        Ok(RgbClusterInfo::from_payload(&payload))
84    }
85
86    /// Retrieves effect-level information for an effect of a cluster (`getInfo`
87    /// effect mode).
88    pub async fn get_effect_info(
89        &self,
90        cluster_index: u8,
91        cluster_effect_index: u8,
92    ) -> Result<RgbEffectInfo, Hidpp20Error> {
93        let payload = self
94            .endpoint
95            .call(0, [cluster_index, cluster_effect_index, TYPE_GENERAL_INFO])
96            .await?
97            .extend_payload();
98        Ok(RgbEffectInfo::from_payload(&payload))
99    }
100
101    /// Retrieves raw information about an onboard-stored effect slot.
102    ///
103    /// The returned parameters' meaning depends on `slot_info_type` (see the
104    /// feature spec): e.g. slot state, defaults, UUID bytes, or effect-name
105    /// characters.
106    pub async fn get_onboard_effect_info(
107        &self,
108        cluster_index: u8,
109        cluster_effect_index: u8,
110        slot: u8,
111        slot_info_type: SlotInfoType,
112    ) -> Result<[u8; ONBOARD_INFO_PARAM_COUNT], Hidpp20Error> {
113        let mut args = [0; 16];
114        args[..5].copy_from_slice(&[
115            cluster_index,
116            cluster_effect_index,
117            TYPE_ONBOARD_EFFECT,
118            slot,
119            slot_info_type.into(),
120        ]);
121        let payload = self.endpoint.call_long(0, args).await?.extend_payload();
122        let mut params = [0; ONBOARD_INFO_PARAM_COUNT];
123        params.copy_from_slice(&payload[3..3 + ONBOARD_INFO_PARAM_COUNT]);
124        Ok(params)
125    }
126
127    /// Applies effect `cluster_effect_index` to `cluster_index`.
128    ///
129    /// `params` are effect-specific (discoverable via [`Self::get_effect_info`]).
130    /// `persistence` controls volatile/non-volatile storage and `power_mode`
131    /// selects which power mode the effect applies to. Requires software control
132    /// (see [`Self::set_sw_control`]).
133    pub async fn set_rgb_cluster_effect(
134        &self,
135        cluster_index: u8,
136        cluster_effect_index: u8,
137        params: [u8; CLUSTER_EFFECT_PARAM_COUNT],
138        persistence: RgbPersistence,
139        power_mode: PowerModeTarget,
140    ) -> Result<(), Hidpp20Error> {
141        let mut args = [0; 16];
142        args[0] = cluster_index;
143        args[1] = cluster_effect_index;
144        args[2..2 + CLUSTER_EFFECT_PARAM_COUNT].copy_from_slice(&params);
145        args[12] = persistence.bits() | (u8::from(power_mode) << POWER_TARGET_SHIFT);
146        self.endpoint.call_long(1, args).await?;
147        Ok(())
148    }
149
150    /// Sets the multi-LED pattern of `cluster_index`.
151    pub async fn set_multi_led_cluster_pattern(
152        &self,
153        cluster_index: u8,
154        pattern: u8,
155    ) -> Result<(), Hidpp20Error> {
156        self.endpoint.call(2, [cluster_index, pattern, 0]).await?;
157        Ok(())
158    }
159
160    /// Reads one non-volatile configuration `capability`.
161    pub async fn get_nv_config(
162        &self,
163        capability: RgbNvCapabilities,
164    ) -> Result<RgbNvConfig, Hidpp20Error> {
165        let [cap_hi, cap_lo] = capability.bits().to_be_bytes();
166        let payload = self
167            .endpoint
168            .call(3, [GetOrSet::Get.into(), cap_hi, cap_lo])
169            .await?
170            .extend_payload();
171        Ok(RgbNvConfig {
172            capability: RgbNvCapabilities::from_bits_retain(be16(&payload, 1)),
173            state: payload[3],
174            param1: payload[4],
175            param2: payload[5],
176        })
177    }
178
179    /// Writes one non-volatile configuration entry (to EEPROM).
180    pub async fn set_nv_config(
181        &self,
182        capability: RgbNvCapabilities,
183        state: u8,
184        param1: u8,
185        param2: u8,
186    ) -> Result<(), Hidpp20Error> {
187        let [cap_hi, cap_lo] = capability.bits().to_be_bytes();
188        let mut args = [0; 16];
189        args[..6].copy_from_slice(&[GetOrSet::Set.into(), cap_hi, cap_lo, state, param1, param2]);
190        self.endpoint.call_long(3, args).await?;
191        Ok(())
192    }
193
194    /// Reads raw manufacturing LED bin parameters.
195    ///
196    /// `backup` reads the backup copy instead of the active one.
197    pub async fn get_led_bin_info(
198        &self,
199        cluster_index: u8,
200        led_bin_index: LedBinIndex,
201        backup: bool,
202    ) -> Result<[u8; LED_BIN_PARAM_COUNT], Hidpp20Error> {
203        let get_or_set = if backup {
204            GET_BACKUP
205        } else {
206            GetOrSet::Get.into()
207        };
208        let payload = self
209            .endpoint
210            .call(4, [get_or_set, cluster_index, led_bin_index.into()])
211            .await?
212            .extend_payload();
213        let mut params = [0; LED_BIN_PARAM_COUNT];
214        params.copy_from_slice(&payload[3..3 + LED_BIN_PARAM_COUNT]);
215        Ok(params)
216    }
217
218    /// Stores raw manufacturing LED bin parameters.
219    pub async fn set_led_bin_info(
220        &self,
221        cluster_index: u8,
222        led_bin_index: LedBinIndex,
223        params: [u8; LED_BIN_PARAM_COUNT],
224    ) -> Result<(), Hidpp20Error> {
225        let mut args = [0; 16];
226        args[0] = GetOrSet::Set.into();
227        args[1] = cluster_index;
228        args[2] = led_bin_index.into();
229        args[3..3 + LED_BIN_PARAM_COUNT].copy_from_slice(&params);
230        self.endpoint.call_long(4, args).await?;
231        Ok(())
232    }
233
234    /// Retrieves the software-control and event-notification flags.
235    pub async fn get_sw_control(&self) -> Result<RgbSwControl, Hidpp20Error> {
236        let payload = self
237            .endpoint
238            .call(5, [GetOrSet::Get.into(), 0, 0])
239            .await?
240            .extend_payload();
241        Ok(RgbSwControl {
242            control: SwControlFlags::from_bits_retain(payload[1]),
243            events: EventsNotificationFlags::from_bits_retain(payload[2]),
244        })
245    }
246
247    /// Sets the software-control and event-notification flags.
248    pub async fn set_sw_control(
249        &self,
250        control: SwControlFlags,
251        events: EventsNotificationFlags,
252    ) -> Result<(), Hidpp20Error> {
253        self.endpoint
254            .call(5, [GetOrSet::Set.into(), control.bits(), events.bits()])
255            .await?;
256        Ok(())
257    }
258
259    /// Applies an effect-sync `drift_value` (milliseconds) correction.
260    ///
261    /// A `cluster_index` of `0xff` targets all clusters.
262    pub async fn set_effect_sync_correction(
263        &self,
264        cluster_index: u8,
265        drift_value: i16,
266    ) -> Result<(), Hidpp20Error> {
267        let [drift_hi, drift_lo] = drift_value.to_be_bytes();
268        let mut args = [0; 16];
269        args[..4].copy_from_slice(&[cluster_index, 0, drift_hi, drift_lo]);
270        self.endpoint.call_long(6, args).await?;
271        Ok(())
272    }
273
274    /// Retrieves the RGB power-mode configuration.
275    pub async fn get_power_mode_config(&self) -> Result<RgbPowerModeConfig, Hidpp20Error> {
276        let payload = self
277            .endpoint
278            .call(7, [GetOrSet::Get.into(), 0, 0])
279            .await?
280            .extend_payload();
281        Ok(RgbPowerModeConfig::from_payload(&payload))
282    }
283
284    /// Writes the RGB power-mode configuration.
285    pub async fn set_power_mode_config(
286        &self,
287        config: RgbPowerModeConfig,
288    ) -> Result<(), Hidpp20Error> {
289        let [flags_hi, flags_lo] = config.flags.to_be_bytes();
290        let [psave_hi, psave_lo] = config.no_activity_timeout_to_power_save.to_be_bytes();
291        let [off_hi, off_lo] = config.no_activity_timeout_to_off.to_be_bytes();
292        let mut args = [0; 16];
293        args[..7].copy_from_slice(&[
294            GetOrSet::Set.into(),
295            flags_hi,
296            flags_lo,
297            psave_hi,
298            psave_lo,
299            off_hi,
300            off_lo,
301        ]);
302        self.endpoint.call_long(7, args).await?;
303        Ok(())
304    }
305
306    /// Retrieves the current RGB power mode.
307    pub async fn get_power_mode(&self) -> Result<RgbPowerMode, Hidpp20Error> {
308        let payload = self
309            .endpoint
310            .call(8, [GetOrSet::Get.into(), 0, 0])
311            .await?
312            .extend_payload();
313        RgbPowerMode::try_from(payload[1]).map_err(|_| Hidpp20Error::UnsupportedResponse)
314    }
315
316    /// Sets the RGB power mode. Requires software control of power modes (see
317    /// [`Self::set_sw_control`]).
318    pub async fn set_power_mode(&self, mode: RgbPowerMode) -> Result<(), Hidpp20Error> {
319        self.endpoint
320            .call(8, [GetOrSet::Set.into(), mode.into(), 0])
321            .await?;
322        Ok(())
323    }
324
325    /// Shuts down the RGB system.
326    ///
327    /// Requires [`RgbExtCapabilities::SHUTDOWN`].
328    pub async fn shutdown(&self) -> Result<(), Hidpp20Error> {
329        self.endpoint.call(9, [0; 3]).await?;
330        Ok(())
331    }
332}