openlogi_device/write/lighting/
rgb.rs1use std::sync::Arc;
6
7use hidpp::channel::{ChannelError, HidppChannel};
8use hidpp::device::Device;
9use hidpp::feature::CreatableFeature;
10use hidpp::feature::device_information::DeviceInformationFeature;
11use hidpp::feature::rgb_effects::{
12 CLUSTER_EFFECT_PARAM_COUNT, EventsNotificationFlags, PowerModeTarget, RgbEffectsFeature,
13 RgbPersistence, SwControlFlags,
14};
15use hidpp::protocol::v20::Hidpp20Error;
16use openlogi_core::color::Rgb;
17use tracing::{debug, warn};
18
19use super::{
20 FRAME_GAP, HidppOperation, WriteError, WriteScope, classify_hidpp_error, open_feature,
21};
22
23const FEATURE: u16 = 0x8071;
24const STATIC_RGB: u16 = 1;
26
27#[cfg(test)]
28mod tests;
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43pub struct RgbControlLease {
44 control: SwControlFlags,
45 events: EventsNotificationFlags,
46 device: Option<[u8; 4]>,
47}
48
49impl RgbControlLease {
50 #[must_use]
56 pub fn continues(&self, earlier: &Self) -> bool {
57 self.device == earlier.device
58 }
59}
60
61pub(super) struct RgbEngine {
64 feature: Arc<RgbEffectsFeature>,
65 unit: Option<[u8; 4]>,
66 statics: Vec<(u8, u8)>,
67 complete: bool,
68}
69
70pub(super) async fn probe(
79 channel: &Arc<HidppChannel>,
80 index: u8,
81 scope: &WriteScope<impl Fn() -> bool, impl Fn() -> bool>,
82) -> Result<Option<RgbEngine>, WriteError> {
83 let opened = scope
84 .read(async {
85 let mut device = device(channel, index).await?;
86 let feature = open_feature::<RgbEffectsFeature>(&mut device).await?;
87 Ok((device, feature))
88 })
89 .await;
90 let (mut device, feature) = match opened {
91 Ok(opened) => opened,
92 Err(WriteError::FeatureUnsupported {
93 feature_hex: FEATURE,
94 }) => return Ok(None),
95 Err(error) => return Err(error),
96 };
97 let unit = scope.read(unit_id(&mut device)).await?;
98 let (statics, complete) = discover(&feature, scope).await?;
99 Ok(Some(RgbEngine {
100 feature,
101 unit,
102 statics,
103 complete,
104 }))
105}
106
107impl RgbEngine {
108 pub(super) fn is_complete(&self) -> bool {
111 self.complete
112 }
113
114 pub(super) async fn apply(
118 &self,
119 index: u8,
120 color: Rgb,
121 scope: &WriteScope<impl Fn() -> bool, impl Fn() -> bool>,
122 ) -> Result<RgbControlLease, WriteError> {
123 debug_assert!(self.complete, "a partial engine must not claim alone");
124 self.under_control(index, scope, async |_| {
125 self.paint_statics(color, scope).await
126 })
127 .await
128 }
129
130 pub(super) async fn apply_per_key(
137 &self,
138 index: u8,
139 color: Rgb,
140 scope: &WriteScope<impl Fn() -> bool, impl Fn() -> bool>,
141 paint: impl AsyncFn() -> Result<(), WriteError>,
142 ) -> Result<RgbControlLease, WriteError> {
143 self.under_control(index, scope, async |fresh| {
144 self.paint_statics(color, scope).await?;
145 paint().await?;
146 if fresh {
147 scope.check()?;
148 paint().await?;
149 }
150 Ok(())
151 })
152 .await
153 }
154
155 async fn paint_statics(
156 &self,
157 color: Rgb,
158 scope: &WriteScope<impl Fn() -> bool, impl Fn() -> bool>,
159 ) -> Result<(), WriteError> {
160 let (r, g, b) = color.components();
161 let mut params = [0; CLUSTER_EFFECT_PARAM_COUNT];
162 params[..3].copy_from_slice(&[r, g, b]);
163 for &(cluster, effect) in &self.statics {
164 scope.check()?;
165 self.feature
166 .set_rgb_cluster_effect(
167 cluster,
168 effect,
169 params,
170 RgbPersistence::VOLATILE,
171 PowerModeTarget::FullPower,
172 )
173 .await
174 .map_err(classify)?;
175 tokio::time::sleep(FRAME_GAP).await;
176 }
177 scope.check()
178 }
179
180 async fn under_control(
184 &self,
185 index: u8,
186 scope: &WriteScope<impl Fn() -> bool, impl Fn() -> bool>,
187 body: impl AsyncFnOnce(bool) -> Result<(), WriteError>,
188 ) -> Result<RgbControlLease, WriteError> {
189 let feature = &self.feature;
190 let previous = scope
191 .read(async { feature.get_sw_control().await.map_err(classify) })
192 .await?;
193 scope.check()?;
194 let fresh = !previous.control.contains(SwControlFlags::ALL_CLUSTERS);
195
196 let result = async {
200 feature
201 .set_sw_control(
202 previous.control | SwControlFlags::ALL_CLUSTERS,
203 previous.events,
204 )
205 .await
206 .map_err(classify)?;
207 body(fresh).await?;
208 scope.check()
209 }
210 .await;
211
212 if let Err(error) = result {
213 let restored = async {
216 scope.check_available()?;
217 feature
218 .set_sw_control(previous.control, previous.events)
219 .await
220 .map_err(classify)
221 }
222 .await;
223 if let Err(restore_error) = restored {
224 warn!(
225 index,
226 ?error,
227 ?restore_error,
228 "RGB control restoration incomplete"
229 );
230 return Err(restore_error);
231 }
232 return Err(error);
233 }
234 Ok(RgbControlLease {
236 control: previous.control,
237 events: previous.events,
238 device: self.unit,
239 })
240 }
241}
242
243pub(super) async fn release(
255 channel: &Arc<HidppChannel>,
256 index: u8,
257 lease: RgbControlLease,
258 scope: &WriteScope<impl Fn() -> bool, impl Fn() -> bool>,
259) -> Result<(), WriteError> {
260 let mut device = scope.read(device(channel, index)).await?;
261 if scope.read(unit_id(&mut device)).await? != lease.device {
262 debug!(
263 index,
264 "another device answers on the route; leaving its RGB control"
265 );
266 return Err(WriteError::DeviceNotFound);
267 }
268 scope.check()?;
269 let feature = scope
270 .read(open_feature::<RgbEffectsFeature>(&mut device))
271 .await?;
272 scope.check()?;
273 feature
274 .set_sw_control(
275 lease.control.difference(SwControlFlags::ALL_CLUSTERS),
276 lease.events,
277 )
278 .await
279 .map_err(classify)?;
280 debug!(index, "returned 0x8071 cluster control to the firmware");
281 Ok(())
282}
283
284async fn device(channel: &Arc<HidppChannel>, index: u8) -> Result<Device, WriteError> {
285 Device::new(Arc::clone(channel), index)
286 .await
287 .map_err(|_| WriteError::DeviceUnreachable { index })
288}
289
290async fn unit_id(device: &mut Device) -> Result<Option<[u8; 4]>, WriteError> {
294 let feature = match open_feature::<DeviceInformationFeature>(device).await {
295 Ok(feature) => feature,
296 Err(WriteError::FeatureUnsupported { .. }) => return Ok(None),
297 Err(error) => return Err(error),
298 };
299 let info = feature
300 .get_device_info()
301 .await
302 .map_err(|error| match error {
303 Hidpp20Error::Channel(ChannelError::Timeout) => WriteError::RequestTimedOut {
304 operation: HidppOperation::Lighting,
305 },
306 other => classify_hidpp_error(
307 other,
308 HidppOperation::Lighting,
309 DeviceInformationFeature::ID,
310 ),
311 })?;
312 Ok(Some(info.unit_id))
313}
314
315async fn discover(
318 feature: &RgbEffectsFeature,
319 scope: &WriteScope<impl Fn() -> bool, impl Fn() -> bool>,
320) -> Result<(Vec<(u8, u8)>, bool), WriteError> {
321 let info = scope
322 .read(async { feature.get_device_info().await.map_err(classify) })
323 .await?;
324 let mut statics = Vec::with_capacity(usize::from(info.cluster_count));
325 let mut complete = info.cluster_count > 0;
326 for cluster in 0..info.cluster_count {
327 let info = scope
328 .read(async { feature.get_cluster_info(cluster).await.map_err(classify) })
329 .await?;
330 let mut found = None;
331 for effect in 0..info.effects_number {
332 let info = scope
333 .read(async {
334 feature
335 .get_effect_info(cluster, effect)
336 .await
337 .map_err(classify)
338 })
339 .await?;
340 if info.effect_id == STATIC_RGB {
341 found = Some(effect);
342 break;
343 }
344 }
345 match found {
346 Some(effect) => statics.push((cluster, effect)),
347 None => complete = false,
348 }
349 }
350 Ok((statics, complete))
351}
352
353fn classify(error: Hidpp20Error) -> WriteError {
354 match error {
355 Hidpp20Error::Channel(ChannelError::Timeout) => WriteError::RequestTimedOut {
356 operation: HidppOperation::Lighting,
357 },
358 other => classify_hidpp_error(other, HidppOperation::Lighting, FEATURE),
359 }
360}