use std::time::Duration;
use async_hid::AsyncHidWrite;
use hidpp::{
device::Device,
feature::{
CreatableFeature,
color_led_effects::{ColorLedEffectsFeature, Persistence, ZONE_EFFECT_PARAM_COUNT},
},
};
use tracing::debug;
use crate::route::DeviceRoute;
use super::{HidppOperation, WriteError, classify_hidpp_error, open_feature, with_route};
const PER_KEY_LIGHTING_FEATURE: u16 = 0x8080;
const COLOR_LED_EFFECTS_FEATURE: u16 = 0x8070;
const REPORT_SET_KEYS: u8 = 0x12;
const REPORT_LONG: u8 = 0x11;
const SW_ID: u8 = 0x0a;
const FN_SET_KEY_RANGE: u8 = 0x3;
const FN_FRAME_END: u8 = 0x5;
const SET_RANGE_MODE: u8 = 0x01;
const KEYS_PER_FRAME: u8 = 0x0e;
const EFFECT_FIXED: u8 = 0x01;
const MAX_COLOR_LED_EFFECT_ZONES: u8 = 4;
const FRAME_GAP: Duration = Duration::from_millis(8);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LightingMethod {
Auto,
Effects,
PerKey,
}
pub async fn set_keyboard_color(
route: &DeviceRoute,
r: u8,
g: u8,
b: u8,
) -> Result<(), WriteError> {
set_keyboard_color_with(route, LightingMethod::Auto, r, g, b).await
}
pub async fn set_keyboard_color_with(
route: &DeviceRoute,
method: LightingMethod,
r: u8,
g: u8,
b: u8,
) -> Result<(), WriteError> {
match method {
LightingMethod::PerKey => set_color_per_key(route, r, g, b).await,
LightingMethod::Effects => set_color_effects(route, r, g, b).await,
LightingMethod::Auto => match set_color_effects(route, r, g, b).await {
Err(WriteError::FeatureUnsupported { feature_hex })
if feature_hex == COLOR_LED_EFFECTS_FEATURE =>
{
debug!("no 0x8070 effect engine — falling back to 0x8080 per-key");
set_color_per_key(route, r, g, b).await
}
other => other,
},
}
}
async fn resolve_feature_index(
route: &DeviceRoute,
feature_id: u16,
) -> Result<Option<u8>, WriteError> {
let device_index = route.device_index();
with_route(route, move |channel| async move {
let device = Device::new(std::sync::Arc::clone(&channel), device_index)
.await
.map_err(|_| WriteError::DeviceUnreachable {
index: device_index,
})?;
let info = device
.root()
.get_feature(feature_id)
.await
.map_err(|e| classify_hidpp_error(e, HidppOperation::ResolveFeature, feature_id))?;
Ok(info.map(|i| i.index))
})
.await
}
async fn set_color_effects(route: &DeviceRoute, r: u8, g: u8, b: u8) -> Result<(), WriteError> {
let index = route.device_index();
with_route(route, move |channel| async move {
let mut device = Device::new(std::sync::Arc::clone(&channel), index)
.await
.map_err(|_| WriteError::DeviceUnreachable { index })?;
let feature = open_feature::<ColorLedEffectsFeature>(&mut device).await?;
let zone_count = feature
.get_info()
.await
.map_err(classify_lighting_error)?
.zone_count;
let mut params = [0u8; ZONE_EFFECT_PARAM_COUNT];
params[0] = r;
params[1] = g;
params[2] = b;
let zones_to_write = if zone_count == 0 {
debug!(
index,
"0x8070 reported zero zones; applying legacy 4-zone fallback"
);
MAX_COLOR_LED_EFFECT_ZONES
} else {
zone_count.min(MAX_COLOR_LED_EFFECT_ZONES)
};
if zone_count > MAX_COLOR_LED_EFFECT_ZONES {
debug!(
index,
zone_count,
capped_zone_count = MAX_COLOR_LED_EFFECT_ZONES,
"0x8070 zone count capped to legacy write limit"
);
}
for zone in 0..zones_to_write {
feature
.set_zone_effect(zone, EFFECT_FIXED, params, Persistence::Volatile)
.await
.map_err(classify_lighting_error)?;
tokio::time::sleep(FRAME_GAP).await;
}
debug!(
index,
zone_count, zones_to_write, r, g, b, "set keyboard colour via typed 0x8070"
);
Ok(())
})
.await
}
fn classify_lighting_error(error: hidpp::protocol::v20::Hidpp20Error) -> WriteError {
classify_hidpp_error(error, HidppOperation::Lighting, ColorLedEffectsFeature::ID)
}
async fn set_color_per_key(route: &DeviceRoute, r: u8, g: u8, b: u8) -> Result<(), WriteError> {
let device_index = route.device_index();
let feature_index = resolve_feature_index(route, PER_KEY_LIGHTING_FEATURE)
.await?
.ok_or(WriteError::FeatureUnsupported {
feature_hex: PER_KEY_LIGHTING_FEATURE,
})?;
let Some(mut writer) = crate::transport::open_route_writer(route).await? else {
return Err(WriteError::DeviceNotFound);
};
let key_ids: Vec<u8> = (0x00u8..=0xe8).collect();
for chunk in key_ids.chunks(KEYS_PER_FRAME as usize) {
let mut rep = vec![0u8; 64];
rep[0] = REPORT_SET_KEYS;
rep[1] = device_index;
rep[2] = feature_index;
rep[3] = (FN_SET_KEY_RANGE << 4) | SW_ID;
rep[5] = SET_RANGE_MODE;
rep[7] = KEYS_PER_FRAME;
for (i, &key) in chunk.iter().enumerate() {
let off = 8 + i * 4;
rep[off] = key;
rep[off + 1] = r;
rep[off + 2] = g;
rep[off + 3] = b;
}
writer
.write_output_report(&rep)
.await
.map_err(WriteError::from)?;
}
let mut commit = vec![0u8; 20];
commit[0] = REPORT_LONG;
commit[1] = device_index;
commit[2] = feature_index;
commit[3] = (FN_FRAME_END << 4) | SW_ID;
writer
.write_output_report(&commit)
.await
.map_err(WriteError::from)?;
debug!(
device_index,
feature_index, r, g, b, "set keyboard colour via 0x8080"
);
Ok(())
}