1use std::sync::Arc;
2use std::time::Duration;
3
4use hidpp::{
5 channel::{ChannelError, HidppChannel},
6 device::Device,
7 feature::{
8 CreatableFeature,
9 color_led_effects::{ColorLedEffectsFeature, Persistence, ZONE_EFFECT_PARAM_COUNT},
10 per_key_lighting::{
11 FramePersistence, MAX_SINGLE_VALUE_ZONES, PerKeyLightingFeature, Rgb,
12 ZONE_PRESENCE_PAGE_LEN, ZonePresencePage,
13 },
14 },
15};
16use tracing::debug;
17
18use crate::route::DeviceRoute;
19
20use super::{HidppOperation, WriteError, classify_hidpp_error, open_feature, with_route};
21
22const PER_KEY_LIGHTING_FEATURE: u16 = 0x8080;
25const COLOR_LED_EFFECTS_FEATURE: u16 = 0x8070;
30
31const REPORT_SET_KEYS: u8 = 0x12;
35const REPORT_LONG: u8 = 0x11;
36const SW_ID: u8 = 0x0a;
39const FN_SET_KEY_RANGE: u8 = 0x3;
40const FN_FRAME_END: u8 = 0x5;
41const SET_RANGE_MODE: u8 = 0x01;
44const KEYS_PER_FRAME: u8 = 0x0e;
45
46const EFFECT_FIXED: u8 = 0x01;
52const MAX_COLOR_LED_EFFECT_ZONES: u8 = 4;
57const FRAME_GAP: Duration = Duration::from_millis(8);
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum LightingMethod {
66 Auto,
70 Effects,
72 PerKey,
74 PerKeyV2,
77}
78
79pub async fn set_keyboard_color(
84 route: &DeviceRoute,
85 r: u8,
86 g: u8,
87 b: u8,
88) -> Result<(), WriteError> {
89 set_keyboard_color_with(route, LightingMethod::Auto, r, g, b).await
90}
91
92pub async fn set_keyboard_color_with(
96 route: &DeviceRoute,
97 method: LightingMethod,
98 r: u8,
99 g: u8,
100 b: u8,
101) -> Result<(), WriteError> {
102 let device_index = route.device_index();
103 with_route(route, move |channel| async move {
104 set_keyboard_color_with_on_channel(&channel, device_index, method, r, g, b).await
105 })
106 .await
107}
108
109pub(super) async fn set_keyboard_color_with_on_channel(
110 channel: &Arc<HidppChannel>,
111 device_index: u8,
112 method: LightingMethod,
113 r: u8,
114 g: u8,
115 b: u8,
116) -> Result<(), WriteError> {
117 match method {
118 LightingMethod::PerKey => set_color_per_key(channel, device_index, r, g, b).await,
119 LightingMethod::PerKeyV2 => set_color_per_key_v2(channel, device_index, r, g, b).await,
120 LightingMethod::Effects => set_color_effects(channel, device_index, r, g, b).await,
121 LightingMethod::Auto => match set_color_effects(channel, device_index, r, g, b).await {
122 Err(WriteError::FeatureUnsupported { feature_hex })
123 if feature_hex == COLOR_LED_EFFECTS_FEATURE =>
124 {
125 debug!("no 0x8070 effect engine — trying the per-key paths");
126 match set_color_per_key_v2(channel, device_index, r, g, b).await {
131 Err(WriteError::FeatureUnsupported { feature_hex })
132 if feature_hex == PerKeyLightingFeature::ID =>
133 {
134 debug!("no 0x8081 per-key zones — falling back to 0x8080 per-key");
135 set_color_per_key(channel, device_index, r, g, b).await
136 }
137 other => other,
138 }
139 }
140 other => other,
141 },
142 }
143}
144
145async fn resolve_feature_index(
149 channel: &Arc<HidppChannel>,
150 device_index: u8,
151 feature_id: u16,
152) -> Result<Option<u8>, WriteError> {
153 let device = Device::new(Arc::clone(channel), device_index)
154 .await
155 .map_err(|_| WriteError::DeviceUnreachable {
156 index: device_index,
157 })?;
158 let info = device
159 .root()
160 .get_feature(feature_id)
161 .await
162 .map_err(|e| classify_hidpp_error(e, HidppOperation::ResolveFeature, feature_id))?;
163 Ok(info.map(|i| i.index))
164}
165
166async fn set_color_effects(
175 channel: &Arc<HidppChannel>,
176 index: u8,
177 r: u8,
178 g: u8,
179 b: u8,
180) -> Result<(), WriteError> {
181 let mut device = Device::new(Arc::clone(channel), index)
182 .await
183 .map_err(|_| WriteError::DeviceUnreachable { index })?;
184 let feature = open_feature::<ColorLedEffectsFeature>(&mut device).await?;
185 let zone_count = feature
186 .get_info()
187 .await
188 .map_err(classify_lighting_error)?
189 .zone_count;
190
191 let mut params = [0u8; ZONE_EFFECT_PARAM_COUNT];
192 params[0] = r;
193 params[1] = g;
194 params[2] = b;
195 let zones_to_write = if zone_count == 0 {
196 debug!(
197 index,
198 "0x8070 reported zero zones; applying legacy 4-zone fallback"
199 );
200 MAX_COLOR_LED_EFFECT_ZONES
201 } else {
202 zone_count.min(MAX_COLOR_LED_EFFECT_ZONES)
203 };
204 if zone_count > MAX_COLOR_LED_EFFECT_ZONES {
205 debug!(
206 index,
207 zone_count,
208 capped_zone_count = MAX_COLOR_LED_EFFECT_ZONES,
209 "0x8070 zone count capped to legacy write limit"
210 );
211 }
212 for zone in 0..zones_to_write {
213 feature
214 .set_zone_effect(zone, EFFECT_FIXED, params, Persistence::Volatile)
215 .await
216 .map_err(classify_lighting_error)?;
217 tokio::time::sleep(FRAME_GAP).await;
218 }
219 debug!(
220 index,
221 zone_count, zones_to_write, r, g, b, "set keyboard colour via typed 0x8070"
222 );
223 Ok(())
224}
225
226fn classify_lighting_error(error: hidpp::protocol::v20::Hidpp20Error) -> WriteError {
228 classify_hidpp_error(error, HidppOperation::Lighting, ColorLedEffectsFeature::ID)
229}
230
231async fn set_color_per_key_v2(
245 channel: &Arc<HidppChannel>,
246 index: u8,
247 r: u8,
248 g: u8,
249 b: u8,
250) -> Result<(), WriteError> {
251 let mut device = Device::new(Arc::clone(channel), index)
252 .await
253 .map_err(|_| WriteError::DeviceUnreachable { index })?;
254 let feature = open_feature::<PerKeyLightingFeature>(&mut device).await?;
255
256 let zones = present_zones(&feature).await?;
257 if zones.is_empty() {
258 debug!(index, "0x8081 reported no present zones");
262 return Err(WriteError::FeatureUnsupported {
263 feature_hex: PerKeyLightingFeature::ID,
264 });
265 }
266
267 let color = Rgb {
268 red: r,
269 green: g,
270 blue: b,
271 };
272 for chunk in zones.chunks(MAX_SINGLE_VALUE_ZONES) {
275 feature
276 .set_rgb_zones_single_value(color, chunk)
277 .await
278 .map_err(classify_per_key_v2_error)?;
279 }
280 feature
281 .frame_end(FramePersistence::Volatile, 0, 0)
282 .await
283 .map_err(classify_per_key_v2_error)?;
284
285 debug!(
286 index,
287 zone_count = zones.len(),
288 r,
289 g,
290 b,
291 "set keyboard colour via typed 0x8081"
292 );
293 Ok(())
294}
295
296async fn present_zones(feature: &PerKeyLightingFeature) -> Result<Vec<u8>, WriteError> {
302 let mut zones = Vec::new();
303 for (page, base) in [
304 (ZonePresencePage::Zones0To111, 0u16),
305 (ZonePresencePage::Zones112To223, 112),
306 (ZonePresencePage::Zones224To255, 224),
307 ] {
308 let bitfield = feature
309 .get_rgb_zone_presence(page)
310 .await
311 .map_err(classify_per_key_v2_error)?;
312 collect_present_zones(base, &bitfield, &mut zones);
313 }
314 Ok(zones)
315}
316
317pub(super) fn collect_present_zones(
324 base: u16,
325 bitfield: &[u8; ZONE_PRESENCE_PAGE_LEN],
326 zones: &mut Vec<u8>,
327) {
328 for (byte_index, byte) in bitfield.iter().enumerate() {
329 for bit in 0..8u16 {
330 if byte & (1 << bit) == 0 {
331 continue;
332 }
333 let Ok(offset) = u16::try_from(byte_index * 8) else {
334 continue;
335 };
336 let Ok(zone_id) = u8::try_from(base + offset + bit) else {
337 continue;
338 };
339 if !matches!(zone_id, 0 | 0xff) {
340 zones.push(zone_id);
341 }
342 }
343 }
344}
345
346fn classify_per_key_v2_error(error: hidpp::protocol::v20::Hidpp20Error) -> WriteError {
348 classify_hidpp_error(error, HidppOperation::Lighting, PerKeyLightingFeature::ID)
349}
350
351async fn set_color_per_key(
355 channel: &Arc<HidppChannel>,
356 device_index: u8,
357 r: u8,
358 g: u8,
359 b: u8,
360) -> Result<(), WriteError> {
361 let feature_index = resolve_feature_index(channel, device_index, PER_KEY_LIGHTING_FEATURE)
362 .await?
363 .ok_or(WriteError::FeatureUnsupported {
364 feature_hex: PER_KEY_LIGHTING_FEATURE,
365 })?;
366
367 for report in per_key_reports(device_index, feature_index, r, g, b) {
368 let written = channel
369 .write_raw_report(&report)
370 .await
371 .map_err(classify_raw_lighting_error)?;
372 if written != report.len() {
373 return Err(WriteError::Hidpp(format!(
374 "raw lighting report wrote {written} of {} bytes",
375 report.len()
376 )));
377 }
378 }
379 debug!(
380 device_index,
381 feature_index, r, g, b, "set keyboard colour via 0x8080"
382 );
383 Ok(())
384}
385
386pub(super) fn per_key_reports(
387 device_index: u8,
388 feature_index: u8,
389 r: u8,
390 g: u8,
391 b: u8,
392) -> Vec<Vec<u8>> {
393 let mut reports = Vec::new();
394 let key_ids: Vec<u8> = (0x00u8..=0xe8).collect();
399 for chunk in key_ids.chunks(KEYS_PER_FRAME as usize) {
400 let mut rep = vec![0u8; 64];
401 rep[0] = REPORT_SET_KEYS;
402 rep[1] = device_index;
403 rep[2] = feature_index;
404 rep[3] = (FN_SET_KEY_RANGE << 4) | SW_ID;
405 rep[5] = SET_RANGE_MODE;
406 rep[7] = KEYS_PER_FRAME;
407 for (i, &key) in chunk.iter().enumerate() {
408 let off = 8 + i * 4;
409 rep[off] = key;
410 rep[off + 1] = r;
411 rep[off + 2] = g;
412 rep[off + 3] = b;
413 }
414 reports.push(rep);
415 }
416 let mut commit = vec![0u8; 20];
417 commit[0] = REPORT_LONG;
418 commit[1] = device_index;
419 commit[2] = feature_index;
420 commit[3] = (FN_FRAME_END << 4) | SW_ID;
421 reports.push(commit);
422 reports
423}
424
425fn classify_raw_lighting_error(error: ChannelError) -> WriteError {
426 match error {
427 ChannelError::Timeout => WriteError::RequestTimedOut {
428 operation: HidppOperation::Lighting,
429 },
430 other => WriteError::Hidpp(format!("{other:?}")),
431 }
432}