1use std::time::Duration;
2
3use async_hid::AsyncHidWrite;
4use hidpp::{
5 device::Device,
6 feature::{
7 CreatableFeature,
8 color_led_effects::{ColorLedEffectsFeature, Persistence, ZONE_EFFECT_PARAM_COUNT},
9 },
10};
11use tracing::debug;
12
13use crate::route::DeviceRoute;
14
15use super::{HidppOperation, WriteError, classify_hidpp_error, open_feature, with_route};
16
17const PER_KEY_LIGHTING_FEATURE: u16 = 0x8080;
20const COLOR_LED_EFFECTS_FEATURE: u16 = 0x8070;
25
26const REPORT_SET_KEYS: u8 = 0x12;
30const REPORT_LONG: u8 = 0x11;
31const SW_ID: u8 = 0x0a;
34const FN_SET_KEY_RANGE: u8 = 0x3;
35const FN_FRAME_END: u8 = 0x5;
36const SET_RANGE_MODE: u8 = 0x01;
39const KEYS_PER_FRAME: u8 = 0x0e;
40
41const EFFECT_FIXED: u8 = 0x01;
47const MAX_COLOR_LED_EFFECT_ZONES: u8 = 4;
52const FRAME_GAP: Duration = Duration::from_millis(8);
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum LightingMethod {
61 Auto,
64 Effects,
66 PerKey,
68}
69
70pub async fn set_keyboard_color(
75 route: &DeviceRoute,
76 r: u8,
77 g: u8,
78 b: u8,
79) -> Result<(), WriteError> {
80 set_keyboard_color_with(route, LightingMethod::Auto, r, g, b).await
81}
82
83pub async fn set_keyboard_color_with(
87 route: &DeviceRoute,
88 method: LightingMethod,
89 r: u8,
90 g: u8,
91 b: u8,
92) -> Result<(), WriteError> {
93 match method {
94 LightingMethod::PerKey => set_color_per_key(route, r, g, b).await,
95 LightingMethod::Effects => set_color_effects(route, r, g, b).await,
96 LightingMethod::Auto => match set_color_effects(route, r, g, b).await {
97 Err(WriteError::FeatureUnsupported { feature_hex })
98 if feature_hex == COLOR_LED_EFFECTS_FEATURE =>
99 {
100 debug!("no 0x8070 effect engine — falling back to 0x8080 per-key");
101 set_color_per_key(route, r, g, b).await
102 }
103 other => other,
104 },
105 }
106}
107
108async fn resolve_feature_index(
112 route: &DeviceRoute,
113 feature_id: u16,
114) -> Result<Option<u8>, WriteError> {
115 let device_index = route.device_index();
116 with_route(route, move |channel| async move {
117 let device = Device::new(std::sync::Arc::clone(&channel), device_index)
118 .await
119 .map_err(|_| WriteError::DeviceUnreachable {
120 index: device_index,
121 })?;
122 let info = device
123 .root()
124 .get_feature(feature_id)
125 .await
126 .map_err(|e| classify_hidpp_error(e, HidppOperation::ResolveFeature, feature_id))?;
127 Ok(info.map(|i| i.index))
128 })
129 .await
130}
131
132async fn set_color_effects(route: &DeviceRoute, r: u8, g: u8, b: u8) -> Result<(), WriteError> {
141 let index = route.device_index();
142 with_route(route, move |channel| async move {
143 let mut device = Device::new(std::sync::Arc::clone(&channel), index)
144 .await
145 .map_err(|_| WriteError::DeviceUnreachable { index })?;
146 let feature = open_feature::<ColorLedEffectsFeature>(&mut device).await?;
147 let zone_count = feature
148 .get_info()
149 .await
150 .map_err(classify_lighting_error)?
151 .zone_count;
152
153 let mut params = [0u8; ZONE_EFFECT_PARAM_COUNT];
154 params[0] = r;
155 params[1] = g;
156 params[2] = b;
157 let zones_to_write = if zone_count == 0 {
158 debug!(
159 index,
160 "0x8070 reported zero zones; applying legacy 4-zone fallback"
161 );
162 MAX_COLOR_LED_EFFECT_ZONES
163 } else {
164 zone_count.min(MAX_COLOR_LED_EFFECT_ZONES)
165 };
166 if zone_count > MAX_COLOR_LED_EFFECT_ZONES {
167 debug!(
168 index,
169 zone_count,
170 capped_zone_count = MAX_COLOR_LED_EFFECT_ZONES,
171 "0x8070 zone count capped to legacy write limit"
172 );
173 }
174 for zone in 0..zones_to_write {
175 feature
176 .set_zone_effect(zone, EFFECT_FIXED, params, Persistence::Volatile)
177 .await
178 .map_err(classify_lighting_error)?;
179 tokio::time::sleep(FRAME_GAP).await;
180 }
181 debug!(
182 index,
183 zone_count, zones_to_write, r, g, b, "set keyboard colour via typed 0x8070"
184 );
185 Ok(())
186 })
187 .await
188}
189
190fn classify_lighting_error(error: hidpp::protocol::v20::Hidpp20Error) -> WriteError {
192 classify_hidpp_error(error, HidppOperation::Lighting, ColorLedEffectsFeature::ID)
193}
194
195async fn set_color_per_key(route: &DeviceRoute, r: u8, g: u8, b: u8) -> Result<(), WriteError> {
199 let device_index = route.device_index();
200 let feature_index = resolve_feature_index(route, PER_KEY_LIGHTING_FEATURE)
201 .await?
202 .ok_or(WriteError::FeatureUnsupported {
203 feature_hex: PER_KEY_LIGHTING_FEATURE,
204 })?;
205
206 let Some(mut writer) = crate::transport::open_route_writer(route).await? else {
207 return Err(WriteError::DeviceNotFound);
208 };
209 let key_ids: Vec<u8> = (0x00u8..=0xe8).collect();
214 for chunk in key_ids.chunks(KEYS_PER_FRAME as usize) {
215 let mut rep = vec![0u8; 64];
216 rep[0] = REPORT_SET_KEYS;
217 rep[1] = device_index;
218 rep[2] = feature_index;
219 rep[3] = (FN_SET_KEY_RANGE << 4) | SW_ID;
220 rep[5] = SET_RANGE_MODE;
221 rep[7] = KEYS_PER_FRAME;
222 for (i, &key) in chunk.iter().enumerate() {
223 let off = 8 + i * 4;
224 rep[off] = key;
225 rep[off + 1] = r;
226 rep[off + 2] = g;
227 rep[off + 3] = b;
228 }
229 writer
230 .write_output_report(&rep)
231 .await
232 .map_err(WriteError::from)?;
233 }
234 let mut commit = vec![0u8; 20];
235 commit[0] = REPORT_LONG;
236 commit[1] = device_index;
237 commit[2] = feature_index;
238 commit[3] = (FN_FRAME_END << 4) | SW_ID;
239 writer
240 .write_output_report(&commit)
241 .await
242 .map_err(WriteError::from)?;
243 debug!(
244 device_index,
245 feature_index, r, g, b, "set keyboard colour via 0x8080"
246 );
247 Ok(())
248}