use std::sync::Arc;
use hidpp::channel::{ChannelError, HidppChannel};
use hidpp::device::Device;
use hidpp::feature::CreatableFeature;
use hidpp::feature::device_information::DeviceInformationFeature;
use hidpp::feature::rgb_effects::{
CLUSTER_EFFECT_PARAM_COUNT, EventsNotificationFlags, PowerModeTarget, RgbEffectsFeature,
RgbPersistence, SwControlFlags,
};
use hidpp::protocol::v20::Hidpp20Error;
use openlogi_core::color::Rgb;
use tracing::{debug, warn};
use super::{
FRAME_GAP, HidppOperation, WriteError, WriteScope, classify_hidpp_error, open_feature,
};
const FEATURE: u16 = 0x8071;
const STATIC_RGB: u16 = 1;
#[cfg(test)]
mod tests;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RgbControlLease {
control: SwControlFlags,
events: EventsNotificationFlags,
device: Option<[u8; 4]>,
}
impl RgbControlLease {
#[must_use]
pub fn continues(&self, earlier: &Self) -> bool {
self.device == earlier.device
}
}
pub(super) struct RgbEngine {
feature: Arc<RgbEffectsFeature>,
unit: Option<[u8; 4]>,
statics: Vec<(u8, u8)>,
complete: bool,
}
pub(super) async fn probe(
channel: &Arc<HidppChannel>,
index: u8,
scope: &WriteScope<impl Fn() -> bool, impl Fn() -> bool>,
) -> Result<Option<RgbEngine>, WriteError> {
let opened = scope
.read(async {
let mut device = device(channel, index).await?;
let feature = open_feature::<RgbEffectsFeature>(&mut device).await?;
Ok((device, feature))
})
.await;
let (mut device, feature) = match opened {
Ok(opened) => opened,
Err(WriteError::FeatureUnsupported {
feature_hex: FEATURE,
}) => return Ok(None),
Err(error) => return Err(error),
};
let unit = scope.read(unit_id(&mut device)).await?;
let (statics, complete) = discover(&feature, scope).await?;
Ok(Some(RgbEngine {
feature,
unit,
statics,
complete,
}))
}
impl RgbEngine {
pub(super) fn is_complete(&self) -> bool {
self.complete
}
pub(super) async fn apply(
&self,
index: u8,
color: Rgb,
scope: &WriteScope<impl Fn() -> bool, impl Fn() -> bool>,
) -> Result<RgbControlLease, WriteError> {
debug_assert!(self.complete, "a partial engine must not claim alone");
self.under_control(index, scope, async |_| {
self.paint_statics(color, scope).await
})
.await
}
pub(super) async fn apply_per_key(
&self,
index: u8,
color: Rgb,
scope: &WriteScope<impl Fn() -> bool, impl Fn() -> bool>,
paint: impl AsyncFn() -> Result<(), WriteError>,
) -> Result<RgbControlLease, WriteError> {
self.under_control(index, scope, async |fresh| {
self.paint_statics(color, scope).await?;
paint().await?;
if fresh {
scope.check()?;
paint().await?;
}
Ok(())
})
.await
}
async fn paint_statics(
&self,
color: Rgb,
scope: &WriteScope<impl Fn() -> bool, impl Fn() -> bool>,
) -> Result<(), WriteError> {
let (r, g, b) = color.components();
let mut params = [0; CLUSTER_EFFECT_PARAM_COUNT];
params[..3].copy_from_slice(&[r, g, b]);
for &(cluster, effect) in &self.statics {
scope.check()?;
self.feature
.set_rgb_cluster_effect(
cluster,
effect,
params,
RgbPersistence::VOLATILE,
PowerModeTarget::FullPower,
)
.await
.map_err(classify)?;
tokio::time::sleep(FRAME_GAP).await;
}
scope.check()
}
async fn under_control(
&self,
index: u8,
scope: &WriteScope<impl Fn() -> bool, impl Fn() -> bool>,
body: impl AsyncFnOnce(bool) -> Result<(), WriteError>,
) -> Result<RgbControlLease, WriteError> {
let feature = &self.feature;
let previous = scope
.read(async { feature.get_sw_control().await.map_err(classify) })
.await?;
scope.check()?;
let fresh = !previous.control.contains(SwControlFlags::ALL_CLUSTERS);
let result = async {
feature
.set_sw_control(
previous.control | SwControlFlags::ALL_CLUSTERS,
previous.events,
)
.await
.map_err(classify)?;
body(fresh).await?;
scope.check()
}
.await;
if let Err(error) = result {
let restored = async {
scope.check_available()?;
feature
.set_sw_control(previous.control, previous.events)
.await
.map_err(classify)
}
.await;
if let Err(restore_error) = restored {
warn!(
index,
?error,
?restore_error,
"RGB control restoration incomplete"
);
return Err(restore_error);
}
return Err(error);
}
Ok(RgbControlLease {
control: previous.control,
events: previous.events,
device: self.unit,
})
}
}
pub(super) async fn release(
channel: &Arc<HidppChannel>,
index: u8,
lease: RgbControlLease,
scope: &WriteScope<impl Fn() -> bool, impl Fn() -> bool>,
) -> Result<(), WriteError> {
let mut device = scope.read(device(channel, index)).await?;
if scope.read(unit_id(&mut device)).await? != lease.device {
debug!(
index,
"another device answers on the route; leaving its RGB control"
);
return Err(WriteError::DeviceNotFound);
}
scope.check()?;
let feature = scope
.read(open_feature::<RgbEffectsFeature>(&mut device))
.await?;
scope.check()?;
feature
.set_sw_control(
lease.control.difference(SwControlFlags::ALL_CLUSTERS),
lease.events,
)
.await
.map_err(classify)?;
debug!(index, "returned 0x8071 cluster control to the firmware");
Ok(())
}
async fn device(channel: &Arc<HidppChannel>, index: u8) -> Result<Device, WriteError> {
Device::new(Arc::clone(channel), index)
.await
.map_err(|_| WriteError::DeviceUnreachable { index })
}
async fn unit_id(device: &mut Device) -> Result<Option<[u8; 4]>, WriteError> {
let feature = match open_feature::<DeviceInformationFeature>(device).await {
Ok(feature) => feature,
Err(WriteError::FeatureUnsupported { .. }) => return Ok(None),
Err(error) => return Err(error),
};
let info = feature
.get_device_info()
.await
.map_err(|error| match error {
Hidpp20Error::Channel(ChannelError::Timeout) => WriteError::RequestTimedOut {
operation: HidppOperation::Lighting,
},
other => classify_hidpp_error(
other,
HidppOperation::Lighting,
DeviceInformationFeature::ID,
),
})?;
Ok(Some(info.unit_id))
}
async fn discover(
feature: &RgbEffectsFeature,
scope: &WriteScope<impl Fn() -> bool, impl Fn() -> bool>,
) -> Result<(Vec<(u8, u8)>, bool), WriteError> {
let info = scope
.read(async { feature.get_device_info().await.map_err(classify) })
.await?;
let mut statics = Vec::with_capacity(usize::from(info.cluster_count));
let mut complete = info.cluster_count > 0;
for cluster in 0..info.cluster_count {
let info = scope
.read(async { feature.get_cluster_info(cluster).await.map_err(classify) })
.await?;
let mut found = None;
for effect in 0..info.effects_number {
let info = scope
.read(async {
feature
.get_effect_info(cluster, effect)
.await
.map_err(classify)
})
.await?;
if info.effect_id == STATIC_RGB {
found = Some(effect);
break;
}
}
match found {
Some(effect) => statics.push((cluster, effect)),
None => complete = false,
}
}
Ok((statics, complete))
}
fn classify(error: Hidpp20Error) -> WriteError {
match error {
Hidpp20Error::Channel(ChannelError::Timeout) => WriteError::RequestTimedOut {
operation: HidppOperation::Lighting,
},
other => classify_hidpp_error(other, HidppOperation::Lighting, FEATURE),
}
}