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::SharedChannel;
19use crate::backend::HidBackend;
20use crate::channel::route::DeviceRoute;
21
22use super::{HidppOperation, WriteError, classify_hidpp_error, open_feature, with_route};
23
24const PER_KEY_LIGHTING_FEATURE: u16 = 0x8080;
27const COLOR_LED_EFFECTS_FEATURE: u16 = 0x8070;
32
33const REPORT_SET_KEYS: u8 = 0x12;
37const REPORT_LONG: u8 = 0x11;
38const SW_ID: u8 = 0x0a;
41const FN_SET_KEY_RANGE: u8 = 0x3;
42const FN_FRAME_END: u8 = 0x5;
43const SET_RANGE_MODE: u8 = 0x01;
46const KEYS_PER_FRAME: u8 = 0x0e;
47
48const EFFECT_FIXED: u8 = 0x01;
54const MAX_COLOR_LED_EFFECT_ZONES: u8 = 4;
59const FRAME_GAP: Duration = Duration::from_millis(8);
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum LightingMethod {
68 Auto,
72 Effects,
74 PerKey,
76 PerKeyV2,
79}
80
81pub async fn set_keyboard_color(
86 backend: &dyn HidBackend,
87 route: &DeviceRoute,
88 r: u8,
89 g: u8,
90 b: u8,
91) -> Result<(), WriteError> {
92 set_keyboard_color_with(backend, route, LightingMethod::Auto, r, g, b).await
93}
94
95pub async fn set_keyboard_color_with(
99 backend: &dyn HidBackend,
100 route: &DeviceRoute,
101 method: LightingMethod,
102 r: u8,
103 g: u8,
104 b: u8,
105) -> Result<(), WriteError> {
106 let device_index = route.device_index();
107 with_route(backend, route, move |channel| async move {
108 set_keyboard_color_with_on_channel(&channel, device_index, method, r, g, b).await
109 })
110 .await
111}
112
113pub(super) async fn set_keyboard_color_with_on_channel(
114 channel: &Arc<HidppChannel>,
115 device_index: u8,
116 method: LightingMethod,
117 r: u8,
118 g: u8,
119 b: u8,
120) -> Result<(), WriteError> {
121 match method {
122 LightingMethod::PerKey => set_color_per_key(channel, device_index, r, g, b).await,
123 LightingMethod::PerKeyV2 => set_color_per_key_v2(channel, device_index, r, g, b).await,
124 LightingMethod::Effects => set_color_effects(channel, device_index, r, g, b).await,
125 LightingMethod::Auto => match set_color_effects(channel, device_index, r, g, b).await {
126 Err(WriteError::FeatureUnsupported { feature_hex })
127 if feature_hex == COLOR_LED_EFFECTS_FEATURE =>
128 {
129 debug!("no 0x8070 effect engine — trying the per-key paths");
130 match set_color_per_key_v2(channel, device_index, r, g, b).await {
135 Err(WriteError::FeatureUnsupported { feature_hex })
136 if feature_hex == PerKeyLightingFeature::ID =>
137 {
138 debug!("no 0x8081 per-key zones — falling back to 0x8080 per-key");
139 set_color_per_key(channel, device_index, r, g, b).await
140 }
141 other => other,
142 }
143 }
144 other => other,
145 },
146 }
147}
148
149async fn resolve_feature_index(
153 channel: &Arc<HidppChannel>,
154 device_index: u8,
155 feature_id: u16,
156) -> Result<Option<u8>, WriteError> {
157 let device = Device::new(Arc::clone(channel), device_index)
158 .await
159 .map_err(|_| WriteError::DeviceUnreachable {
160 index: device_index,
161 })?;
162 let info = device
163 .root()
164 .get_feature(feature_id)
165 .await
166 .map_err(|e| classify_hidpp_error(e, HidppOperation::ResolveFeature, feature_id))?;
167 Ok(info.map(|i| i.index))
168}
169
170async fn set_color_effects(
179 channel: &Arc<HidppChannel>,
180 index: u8,
181 r: u8,
182 g: u8,
183 b: u8,
184) -> Result<(), WriteError> {
185 let mut device = Device::new(Arc::clone(channel), index)
186 .await
187 .map_err(|_| WriteError::DeviceUnreachable { index })?;
188 let feature = open_feature::<ColorLedEffectsFeature>(&mut device).await?;
189 let zone_count = feature
190 .get_info()
191 .await
192 .map_err(classify_lighting_error)?
193 .zone_count;
194
195 let mut params = [0u8; ZONE_EFFECT_PARAM_COUNT];
196 params[0] = r;
197 params[1] = g;
198 params[2] = b;
199 let zones_to_write = if zone_count == 0 {
200 debug!(
201 index,
202 "0x8070 reported zero zones; applying legacy 4-zone fallback"
203 );
204 MAX_COLOR_LED_EFFECT_ZONES
205 } else {
206 zone_count.min(MAX_COLOR_LED_EFFECT_ZONES)
207 };
208 if zone_count > MAX_COLOR_LED_EFFECT_ZONES {
209 debug!(
210 index,
211 zone_count,
212 capped_zone_count = MAX_COLOR_LED_EFFECT_ZONES,
213 "0x8070 zone count capped to legacy write limit"
214 );
215 }
216 for zone in 0..zones_to_write {
217 feature
218 .set_zone_effect(zone, EFFECT_FIXED, params, Persistence::Volatile)
219 .await
220 .map_err(classify_lighting_error)?;
221 tokio::time::sleep(FRAME_GAP).await;
222 }
223 debug!(
224 index,
225 zone_count, zones_to_write, r, g, b, "set keyboard colour via typed 0x8070"
226 );
227 Ok(())
228}
229
230fn classify_lighting_error(error: hidpp::protocol::v20::Hidpp20Error) -> WriteError {
232 classify_hidpp_error(error, HidppOperation::Lighting, ColorLedEffectsFeature::ID)
233}
234
235async fn set_color_per_key_v2(
249 channel: &Arc<HidppChannel>,
250 index: u8,
251 r: u8,
252 g: u8,
253 b: u8,
254) -> Result<(), WriteError> {
255 let mut device = Device::new(Arc::clone(channel), index)
256 .await
257 .map_err(|_| WriteError::DeviceUnreachable { index })?;
258 let feature = open_feature::<PerKeyLightingFeature>(&mut device).await?;
259
260 let zones = present_zones(&feature).await?;
261 if zones.is_empty() {
262 debug!(index, "0x8081 reported no present zones");
266 return Err(WriteError::FeatureUnsupported {
267 feature_hex: PerKeyLightingFeature::ID,
268 });
269 }
270
271 let color = Rgb {
272 red: r,
273 green: g,
274 blue: b,
275 };
276 for chunk in zones.chunks(MAX_SINGLE_VALUE_ZONES) {
279 feature
280 .set_rgb_zones_single_value(color, chunk)
281 .await
282 .map_err(classify_per_key_v2_error)?;
283 }
284 feature
285 .frame_end(FramePersistence::Volatile, 0, 0)
286 .await
287 .map_err(classify_per_key_v2_error)?;
288
289 debug!(
290 index,
291 zone_count = zones.len(),
292 r,
293 g,
294 b,
295 "set keyboard colour via typed 0x8081"
296 );
297 Ok(())
298}
299
300async fn present_zones(feature: &PerKeyLightingFeature) -> Result<Vec<u8>, WriteError> {
306 let mut zones = Vec::new();
307 for (page, base) in [
308 (ZonePresencePage::Zones0To111, 0u16),
309 (ZonePresencePage::Zones112To223, 112),
310 (ZonePresencePage::Zones224To255, 224),
311 ] {
312 let bitfield = feature
313 .get_rgb_zone_presence(page)
314 .await
315 .map_err(classify_per_key_v2_error)?;
316 collect_present_zones(base, &bitfield, &mut zones);
317 }
318 Ok(zones)
319}
320
321pub(super) fn collect_present_zones(
328 base: u16,
329 bitfield: &[u8; ZONE_PRESENCE_PAGE_LEN],
330 zones: &mut Vec<u8>,
331) {
332 for (byte_index, byte) in bitfield.iter().enumerate() {
333 for bit in 0..8u16 {
334 if byte & (1 << bit) == 0 {
335 continue;
336 }
337 let Ok(offset) = u16::try_from(byte_index * 8) else {
338 continue;
339 };
340 let Ok(zone_id) = u8::try_from(base + offset + bit) else {
341 continue;
342 };
343 if !matches!(zone_id, 0 | 0xff) {
344 zones.push(zone_id);
345 }
346 }
347 }
348}
349
350fn classify_per_key_v2_error(error: hidpp::protocol::v20::Hidpp20Error) -> WriteError {
352 classify_hidpp_error(error, HidppOperation::Lighting, PerKeyLightingFeature::ID)
353}
354
355async fn set_color_per_key(
359 channel: &Arc<HidppChannel>,
360 device_index: u8,
361 r: u8,
362 g: u8,
363 b: u8,
364) -> Result<(), WriteError> {
365 let feature_index = resolve_feature_index(channel, device_index, PER_KEY_LIGHTING_FEATURE)
366 .await?
367 .ok_or(WriteError::FeatureUnsupported {
368 feature_hex: PER_KEY_LIGHTING_FEATURE,
369 })?;
370
371 for report in per_key_reports(device_index, feature_index, r, g, b) {
372 let written = channel
373 .write_raw_report(&report)
374 .await
375 .map_err(classify_raw_lighting_error)?;
376 if written != report.len() {
377 return Err(WriteError::Hidpp(format!(
378 "raw lighting report wrote {written} of {} bytes",
379 report.len()
380 )));
381 }
382 }
383 debug!(
384 device_index,
385 feature_index, r, g, b, "set keyboard colour via 0x8080"
386 );
387 Ok(())
388}
389
390pub(super) fn per_key_reports(
391 device_index: u8,
392 feature_index: u8,
393 r: u8,
394 g: u8,
395 b: u8,
396) -> Vec<Vec<u8>> {
397 let mut reports = Vec::new();
398 let key_ids: Vec<u8> = (0x00u8..=0xe8).collect();
403 for chunk in key_ids.chunks(KEYS_PER_FRAME as usize) {
404 let mut rep = vec![0u8; 64];
405 rep[0] = REPORT_SET_KEYS;
406 rep[1] = device_index;
407 rep[2] = feature_index;
408 rep[3] = (FN_SET_KEY_RANGE << 4) | SW_ID;
409 rep[5] = SET_RANGE_MODE;
410 rep[7] = KEYS_PER_FRAME;
411 for (i, &key) in chunk.iter().enumerate() {
412 let off = 8 + i * 4;
413 rep[off] = key;
414 rep[off + 1] = r;
415 rep[off + 2] = g;
416 rep[off + 3] = b;
417 }
418 reports.push(rep);
419 }
420 let mut commit = vec![0u8; 20];
421 commit[0] = REPORT_LONG;
422 commit[1] = device_index;
423 commit[2] = feature_index;
424 commit[3] = (FN_FRAME_END << 4) | SW_ID;
425 reports.push(commit);
426 reports
427}
428
429fn classify_raw_lighting_error(error: ChannelError) -> WriteError {
430 match error {
431 ChannelError::Timeout => WriteError::RequestTimedOut {
432 operation: HidppOperation::Lighting,
433 },
434 other => WriteError::Hidpp(format!("{other:?}")),
435 }
436}
437
438pub async fn set_keyboard_color_on(
441 shared: &SharedChannel,
442 r: u8,
443 g: u8,
444 b: u8,
445) -> Result<(), WriteError> {
446 set_keyboard_color_with_on(shared, LightingMethod::Auto, r, g, b).await
447}
448
449pub async fn set_keyboard_color_with_on(
452 shared: &SharedChannel,
453 method: LightingMethod,
454 r: u8,
455 g: u8,
456 b: u8,
457) -> Result<(), WriteError> {
458 set_keyboard_color_with_on_channel(shared.channel(), shared.device_index(), method, r, g, b)
459 .await
460}