Skip to main content

hidpp/feature/rgb_effects/
mod.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 std::sync::Arc;
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    channel::{HidppChannel, MessageListenerGuard},
38    event::EventEmitter,
39    feature::{CreatableFeature, EmittingFeature, Feature, FeatureEndpoint, event_payload},
40    protocol::v20::Hidpp20Error,
41};
42
43/// `typeOfInfo` value selecting general info in `getInfo`.
44const TYPE_GENERAL_INFO: u8 = 0x00;
45/// `typeOfInfo` value selecting onboard-stored effect info in `getInfo`.
46const TYPE_ONBOARD_EFFECT: u8 = 0x01;
47/// `getOrSet` value requesting a backup read in `manageRgbLedBinInfo`.
48const GET_BACKUP: u8 = 0x02;
49/// Bit offset of the power-mode target in the `setRgbClusterEffect` flags byte.
50const POWER_TARGET_SHIFT: u8 = 2;
51
52/// Implements the `RgbEffects` / `0x8071` feature.
53pub struct RgbEffectsFeature {
54    /// The endpoint this feature talks to.
55    endpoint: FeatureEndpoint,
56
57    /// The emitter used to publish decoded events.
58    emitter: Arc<EventEmitter<RgbEffectsEvent>>,
59
60    /// Removes the message listener when the feature is dropped.
61    _msg_listener: MessageListenerGuard,
62}
63
64impl CreatableFeature for RgbEffectsFeature {
65    const ID: u16 = 0x8071;
66    const STARTING_VERSION: u8 = 0;
67
68    fn new(chan: Arc<HidppChannel>, device_index: u8, feature_index: u8) -> Self {
69        let emitter = Arc::new(EventEmitter::new());
70
71        let listener = chan.add_msg_listener_guarded({
72            let emitter = Arc::clone(&emitter);
73
74            move |raw, matched| {
75                let Some((func, payload)) =
76                    event_payload(raw, matched, device_index, feature_index)
77                else {
78                    return;
79                };
80                if let Some(event) = event::decode_event(func.to_lo(), &payload) {
81                    emitter.emit(event);
82                }
83            }
84        });
85
86        Self {
87            endpoint: FeatureEndpoint::new(chan, device_index, feature_index),
88            emitter,
89            _msg_listener: listener,
90        }
91    }
92}
93
94impl Feature for RgbEffectsFeature {}
95
96impl EmittingFeature<RgbEffectsEvent> for RgbEffectsFeature {
97    fn listen(&self) -> async_channel::Receiver<RgbEffectsEvent> {
98        self.emitter.create_receiver()
99    }
100}
101
102impl RgbEffectsFeature {
103    /// Retrieves device-level RGB information (`getInfo` device mode).
104    pub async fn get_device_info(&self) -> Result<RgbDeviceInfo, Hidpp20Error> {
105        let payload = self
106            .endpoint
107            .call(0, [ALL_CLUSTERS, ALL_EFFECTS, TYPE_GENERAL_INFO])
108            .await?
109            .extend_payload();
110        Ok(RgbDeviceInfo::from_payload(&payload))
111    }
112
113    /// Retrieves cluster-level information for `cluster_index` (`getInfo` cluster
114    /// mode).
115    pub async fn get_cluster_info(
116        &self,
117        cluster_index: u8,
118    ) -> Result<RgbClusterInfo, Hidpp20Error> {
119        let payload = self
120            .endpoint
121            .call(0, [cluster_index, ALL_EFFECTS, TYPE_GENERAL_INFO])
122            .await?
123            .extend_payload();
124        Ok(RgbClusterInfo::from_payload(&payload))
125    }
126
127    /// Retrieves effect-level information for an effect of a cluster (`getInfo`
128    /// effect mode).
129    pub async fn get_effect_info(
130        &self,
131        cluster_index: u8,
132        cluster_effect_index: u8,
133    ) -> Result<RgbEffectInfo, Hidpp20Error> {
134        let payload = self
135            .endpoint
136            .call(0, [cluster_index, cluster_effect_index, TYPE_GENERAL_INFO])
137            .await?
138            .extend_payload();
139        Ok(RgbEffectInfo::from_payload(&payload))
140    }
141
142    /// Retrieves raw information about an onboard-stored effect slot.
143    ///
144    /// The returned parameters' meaning depends on `slot_info_type` (see the
145    /// feature spec): e.g. slot state, defaults, UUID bytes, or effect-name
146    /// characters.
147    pub async fn get_onboard_effect_info(
148        &self,
149        cluster_index: u8,
150        cluster_effect_index: u8,
151        slot: u8,
152        slot_info_type: SlotInfoType,
153    ) -> Result<[u8; ONBOARD_INFO_PARAM_COUNT], Hidpp20Error> {
154        let mut args = [0; 16];
155        args[..5].copy_from_slice(&[
156            cluster_index,
157            cluster_effect_index,
158            TYPE_ONBOARD_EFFECT,
159            slot,
160            slot_info_type.into(),
161        ]);
162        let payload = self.endpoint.call_long(0, args).await?.extend_payload();
163        let mut params = [0; ONBOARD_INFO_PARAM_COUNT];
164        params.copy_from_slice(&payload[3..3 + ONBOARD_INFO_PARAM_COUNT]);
165        Ok(params)
166    }
167
168    /// Applies effect `cluster_effect_index` to `cluster_index`.
169    ///
170    /// `params` are effect-specific (discoverable via [`Self::get_effect_info`]).
171    /// `persistence` controls volatile/non-volatile storage and `power_mode`
172    /// selects which power mode the effect applies to. Requires software control
173    /// (see [`Self::set_sw_control`]).
174    pub async fn set_rgb_cluster_effect(
175        &self,
176        cluster_index: u8,
177        cluster_effect_index: u8,
178        params: [u8; CLUSTER_EFFECT_PARAM_COUNT],
179        persistence: RgbPersistence,
180        power_mode: PowerModeTarget,
181    ) -> Result<(), Hidpp20Error> {
182        let mut args = [0; 16];
183        args[0] = cluster_index;
184        args[1] = cluster_effect_index;
185        args[2..2 + CLUSTER_EFFECT_PARAM_COUNT].copy_from_slice(&params);
186        args[12] = persistence.bits() | (u8::from(power_mode) << POWER_TARGET_SHIFT);
187        self.endpoint.call_long(1, args).await?;
188        Ok(())
189    }
190
191    /// Sets the multi-LED pattern of `cluster_index`.
192    pub async fn set_multi_led_cluster_pattern(
193        &self,
194        cluster_index: u8,
195        pattern: u8,
196    ) -> Result<(), Hidpp20Error> {
197        self.endpoint.call(2, [cluster_index, pattern, 0]).await?;
198        Ok(())
199    }
200
201    /// Reads one non-volatile configuration `capability`.
202    pub async fn get_nv_config(
203        &self,
204        capability: RgbNvCapabilities,
205    ) -> Result<RgbNvConfig, Hidpp20Error> {
206        let [cap_hi, cap_lo] = capability.bits().to_be_bytes();
207        let payload = self
208            .endpoint
209            .call(3, [GetOrSet::Get.into(), cap_hi, cap_lo])
210            .await?
211            .extend_payload();
212        Ok(RgbNvConfig {
213            capability: RgbNvCapabilities::from_bits_retain(be16(&payload, 1)),
214            state: payload[3],
215            param1: payload[4],
216            param2: payload[5],
217        })
218    }
219
220    /// Writes one non-volatile configuration entry (to EEPROM).
221    pub async fn set_nv_config(
222        &self,
223        capability: RgbNvCapabilities,
224        state: u8,
225        param1: u8,
226        param2: u8,
227    ) -> Result<(), Hidpp20Error> {
228        let [cap_hi, cap_lo] = capability.bits().to_be_bytes();
229        let mut args = [0; 16];
230        args[..6].copy_from_slice(&[GetOrSet::Set.into(), cap_hi, cap_lo, state, param1, param2]);
231        self.endpoint.call_long(3, args).await?;
232        Ok(())
233    }
234
235    /// Reads raw manufacturing LED bin parameters.
236    ///
237    /// `backup` reads the backup copy instead of the active one.
238    pub async fn get_led_bin_info(
239        &self,
240        cluster_index: u8,
241        led_bin_index: LedBinIndex,
242        backup: bool,
243    ) -> Result<[u8; LED_BIN_PARAM_COUNT], Hidpp20Error> {
244        let get_or_set = if backup {
245            GET_BACKUP
246        } else {
247            GetOrSet::Get.into()
248        };
249        let payload = self
250            .endpoint
251            .call(4, [get_or_set, cluster_index, led_bin_index.into()])
252            .await?
253            .extend_payload();
254        let mut params = [0; LED_BIN_PARAM_COUNT];
255        params.copy_from_slice(&payload[3..3 + LED_BIN_PARAM_COUNT]);
256        Ok(params)
257    }
258
259    /// Stores raw manufacturing LED bin parameters.
260    pub async fn set_led_bin_info(
261        &self,
262        cluster_index: u8,
263        led_bin_index: LedBinIndex,
264        params: [u8; LED_BIN_PARAM_COUNT],
265    ) -> Result<(), Hidpp20Error> {
266        let mut args = [0; 16];
267        args[0] = GetOrSet::Set.into();
268        args[1] = cluster_index;
269        args[2] = led_bin_index.into();
270        args[3..3 + LED_BIN_PARAM_COUNT].copy_from_slice(&params);
271        self.endpoint.call_long(4, args).await?;
272        Ok(())
273    }
274
275    /// Retrieves the software-control and event-notification flags.
276    pub async fn get_sw_control(&self) -> Result<RgbSwControl, Hidpp20Error> {
277        let payload = self
278            .endpoint
279            .call(5, [GetOrSet::Get.into(), 0, 0])
280            .await?
281            .extend_payload();
282        Ok(RgbSwControl {
283            control: SwControlFlags::from_bits_retain(payload[1]),
284            events: EventsNotificationFlags::from_bits_retain(payload[2]),
285        })
286    }
287
288    /// Sets the software-control and event-notification flags.
289    pub async fn set_sw_control(
290        &self,
291        control: SwControlFlags,
292        events: EventsNotificationFlags,
293    ) -> Result<(), Hidpp20Error> {
294        self.endpoint
295            .call(5, [GetOrSet::Set.into(), control.bits(), events.bits()])
296            .await?;
297        Ok(())
298    }
299
300    /// Applies an effect-sync `drift_value` (milliseconds) correction.
301    ///
302    /// A `cluster_index` of `0xff` targets all clusters.
303    pub async fn set_effect_sync_correction(
304        &self,
305        cluster_index: u8,
306        drift_value: i16,
307    ) -> Result<(), Hidpp20Error> {
308        let [drift_hi, drift_lo] = drift_value.to_be_bytes();
309        let mut args = [0; 16];
310        args[..4].copy_from_slice(&[cluster_index, 0, drift_hi, drift_lo]);
311        self.endpoint.call_long(6, args).await?;
312        Ok(())
313    }
314
315    /// Retrieves the RGB power-mode configuration.
316    pub async fn get_power_mode_config(&self) -> Result<RgbPowerModeConfig, Hidpp20Error> {
317        let payload = self
318            .endpoint
319            .call(7, [GetOrSet::Get.into(), 0, 0])
320            .await?
321            .extend_payload();
322        Ok(RgbPowerModeConfig::from_payload(&payload))
323    }
324
325    /// Writes the RGB power-mode configuration.
326    pub async fn set_power_mode_config(
327        &self,
328        config: RgbPowerModeConfig,
329    ) -> Result<(), Hidpp20Error> {
330        let [flags_hi, flags_lo] = config.flags.to_be_bytes();
331        let [psave_hi, psave_lo] = config.no_activity_timeout_to_power_save.to_be_bytes();
332        let [off_hi, off_lo] = config.no_activity_timeout_to_off.to_be_bytes();
333        let mut args = [0; 16];
334        args[..7].copy_from_slice(&[
335            GetOrSet::Set.into(),
336            flags_hi,
337            flags_lo,
338            psave_hi,
339            psave_lo,
340            off_hi,
341            off_lo,
342        ]);
343        self.endpoint.call_long(7, args).await?;
344        Ok(())
345    }
346
347    /// Retrieves the current RGB power mode.
348    pub async fn get_power_mode(&self) -> Result<RgbPowerMode, Hidpp20Error> {
349        let payload = self
350            .endpoint
351            .call(8, [GetOrSet::Get.into(), 0, 0])
352            .await?
353            .extend_payload();
354        RgbPowerMode::try_from(payload[1]).map_err(|_| Hidpp20Error::UnsupportedResponse)
355    }
356
357    /// Sets the RGB power mode. Requires software control of power modes (see
358    /// [`Self::set_sw_control`]).
359    pub async fn set_power_mode(&self, mode: RgbPowerMode) -> Result<(), Hidpp20Error> {
360        self.endpoint
361            .call(8, [GetOrSet::Set.into(), mode.into(), 0])
362            .await?;
363        Ok(())
364    }
365
366    /// Shuts down the RGB system.
367    ///
368    /// Requires [`RgbExtCapabilities::SHUTDOWN`].
369    pub async fn shutdown(&self) -> Result<(), Hidpp20Error> {
370        self.endpoint.call(9, [0; 3]).await?;
371        Ok(())
372    }
373}