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    ///
134    /// Drive this future to completion even if its requester cancels: it waits
135    /// for native write completion before bounding the response wait. See
136    /// [`crate::channel::HidppChannel::send_write_through`].
137    pub async fn set_rgb_cluster_effect(
138        &self,
139        cluster_index: u8,
140        cluster_effect_index: u8,
141        params: [u8; CLUSTER_EFFECT_PARAM_COUNT],
142        persistence: RgbPersistence,
143        power_mode: PowerModeTarget,
144    ) -> Result<(), Hidpp20Error> {
145        let mut args = [0; 16];
146        args[0] = cluster_index;
147        args[1] = cluster_effect_index;
148        args[2..2 + CLUSTER_EFFECT_PARAM_COUNT].copy_from_slice(&params);
149        args[12] = persistence.bits() | (u8::from(power_mode) << POWER_TARGET_SHIFT);
150        self.endpoint.call_long_write_through(1, args).await?;
151        Ok(())
152    }
153
154    /// Sets the multi-LED pattern of `cluster_index`.
155    pub async fn set_multi_led_cluster_pattern(
156        &self,
157        cluster_index: u8,
158        pattern: u8,
159    ) -> Result<(), Hidpp20Error> {
160        self.endpoint.call(2, [cluster_index, pattern, 0]).await?;
161        Ok(())
162    }
163
164    /// Reads one non-volatile configuration `capability`.
165    pub async fn get_nv_config(
166        &self,
167        capability: RgbNvCapabilities,
168    ) -> Result<RgbNvConfig, Hidpp20Error> {
169        let [cap_hi, cap_lo] = capability.bits().to_be_bytes();
170        let payload = self
171            .endpoint
172            .call(3, [GetOrSet::Get.into(), cap_hi, cap_lo])
173            .await?
174            .extend_payload();
175        Ok(RgbNvConfig {
176            capability: RgbNvCapabilities::from_bits_retain(be16(&payload, 1)),
177            state: payload[3],
178            param1: payload[4],
179            param2: payload[5],
180        })
181    }
182
183    /// Writes one non-volatile configuration entry (to EEPROM).
184    pub async fn set_nv_config(
185        &self,
186        capability: RgbNvCapabilities,
187        state: u8,
188        param1: u8,
189        param2: u8,
190    ) -> Result<(), Hidpp20Error> {
191        let [cap_hi, cap_lo] = capability.bits().to_be_bytes();
192        let mut args = [0; 16];
193        args[..6].copy_from_slice(&[GetOrSet::Set.into(), cap_hi, cap_lo, state, param1, param2]);
194        self.endpoint.call_long(3, args).await?;
195        Ok(())
196    }
197
198    /// Reads raw manufacturing LED bin parameters.
199    ///
200    /// `backup` reads the backup copy instead of the active one.
201    pub async fn get_led_bin_info(
202        &self,
203        cluster_index: u8,
204        led_bin_index: LedBinIndex,
205        backup: bool,
206    ) -> Result<[u8; LED_BIN_PARAM_COUNT], Hidpp20Error> {
207        let get_or_set = if backup {
208            GET_BACKUP
209        } else {
210            GetOrSet::Get.into()
211        };
212        let payload = self
213            .endpoint
214            .call(4, [get_or_set, cluster_index, led_bin_index.into()])
215            .await?
216            .extend_payload();
217        let mut params = [0; LED_BIN_PARAM_COUNT];
218        params.copy_from_slice(&payload[3..3 + LED_BIN_PARAM_COUNT]);
219        Ok(params)
220    }
221
222    /// Stores raw manufacturing LED bin parameters.
223    pub async fn set_led_bin_info(
224        &self,
225        cluster_index: u8,
226        led_bin_index: LedBinIndex,
227        params: [u8; LED_BIN_PARAM_COUNT],
228    ) -> Result<(), Hidpp20Error> {
229        let mut args = [0; 16];
230        args[0] = GetOrSet::Set.into();
231        args[1] = cluster_index;
232        args[2] = led_bin_index.into();
233        args[3..3 + LED_BIN_PARAM_COUNT].copy_from_slice(&params);
234        self.endpoint.call_long(4, args).await?;
235        Ok(())
236    }
237
238    /// Retrieves the software-control and event-notification flags.
239    pub async fn get_sw_control(&self) -> Result<RgbSwControl, Hidpp20Error> {
240        let payload = self
241            .endpoint
242            .call(5, [GetOrSet::Get.into(), 0, 0])
243            .await?
244            .extend_payload();
245        Ok(RgbSwControl {
246            control: SwControlFlags::from_bits_retain(payload[1]),
247            events: EventsNotificationFlags::from_bits_retain(payload[2]),
248        })
249    }
250
251    /// Sets the software-control and event-notification flags.
252    ///
253    /// Drive to completion, as for [`Self::set_rgb_cluster_effect`]. The
254    /// [v4 spec, pp. 26–28](https://drive.google.com/file/d/1lecrJQgAC7wnXlo8PEnwVSb3L0GioALG/view)
255    /// returns all three request fields; matching them keeps a late claim ACK
256    /// with different flags from satisfying a restore request.
257    pub async fn set_sw_control(
258        &self,
259        control: SwControlFlags,
260        events: EventsNotificationFlags,
261    ) -> Result<(), Hidpp20Error> {
262        self.endpoint
263            .call_echoed_write_through(5, [GetOrSet::Set.into(), control.bits(), events.bits()])
264            .await?;
265        Ok(())
266    }
267
268    /// Applies an effect-sync `drift_value` (milliseconds) correction.
269    ///
270    /// A `cluster_index` of `0xff` targets all clusters.
271    pub async fn set_effect_sync_correction(
272        &self,
273        cluster_index: u8,
274        drift_value: i16,
275    ) -> Result<(), Hidpp20Error> {
276        let [drift_hi, drift_lo] = drift_value.to_be_bytes();
277        let mut args = [0; 16];
278        args[..4].copy_from_slice(&[cluster_index, 0, drift_hi, drift_lo]);
279        self.endpoint.call_long(6, args).await?;
280        Ok(())
281    }
282
283    /// Retrieves the RGB power-mode configuration.
284    pub async fn get_power_mode_config(&self) -> Result<RgbPowerModeConfig, Hidpp20Error> {
285        let payload = self
286            .endpoint
287            .call(7, [GetOrSet::Get.into(), 0, 0])
288            .await?
289            .extend_payload();
290        Ok(RgbPowerModeConfig::from_payload(&payload))
291    }
292
293    /// Writes the RGB power-mode configuration.
294    pub async fn set_power_mode_config(
295        &self,
296        config: RgbPowerModeConfig,
297    ) -> Result<(), Hidpp20Error> {
298        let [flags_hi, flags_lo] = config.flags.to_be_bytes();
299        let [psave_hi, psave_lo] = config.no_activity_timeout_to_power_save.to_be_bytes();
300        let [off_hi, off_lo] = config.no_activity_timeout_to_off.to_be_bytes();
301        let mut args = [0; 16];
302        args[..7].copy_from_slice(&[
303            GetOrSet::Set.into(),
304            flags_hi,
305            flags_lo,
306            psave_hi,
307            psave_lo,
308            off_hi,
309            off_lo,
310        ]);
311        self.endpoint.call_long(7, args).await?;
312        Ok(())
313    }
314
315    /// Retrieves the current RGB power mode.
316    pub async fn get_power_mode(&self) -> Result<RgbPowerMode, Hidpp20Error> {
317        let payload = self
318            .endpoint
319            .call(8, [GetOrSet::Get.into(), 0, 0])
320            .await?
321            .extend_payload();
322        RgbPowerMode::try_from(payload[1]).map_err(|_| Hidpp20Error::UnsupportedResponse)
323    }
324
325    /// Sets the RGB power mode. Requires software control of power modes (see
326    /// [`Self::set_sw_control`]).
327    pub async fn set_power_mode(&self, mode: RgbPowerMode) -> Result<(), Hidpp20Error> {
328        self.endpoint
329            .call(8, [GetOrSet::Set.into(), mode.into(), 0])
330            .await?;
331        Ok(())
332    }
333
334    /// Shuts down the RGB system.
335    ///
336    /// Requires [`RgbExtCapabilities::SHUTDOWN`].
337    pub async fn shutdown(&self) -> Result<(), Hidpp20Error> {
338        self.endpoint.call(9, [0; 3]).await?;
339        Ok(())
340    }
341}