1use std::collections::HashSet;
8use std::fs::OpenOptions;
9use std::os::unix::io::AsRawFd;
10
11use crate::descriptor::{
12 DeviceInfo, DeviceKind, LampArrayReports, LedRgbChannelInfo, ReportInfo, ReportType,
13};
14use crate::error::{Error, Result};
15
16fn hidiocgfeature(size: usize) -> libc::c_ulong {
21 0xC000_4807 | ((size as libc::c_ulong) << 16)
22}
23
24fn hidiocsfeature(size: usize) -> libc::c_ulong {
25 0xC000_4806 | ((size as libc::c_ulong) << 16)
26}
27
28fn scale_u8(value: u8, logical_max: u32) -> u8 {
34 if logical_max == 0 {
35 return 0;
36 }
37 if logical_max >= 255 {
38 return value;
39 }
40 ((value as u32 * logical_max + 127) / 255) as u8
41}
42
43pub fn lamp_array_kind_name(kind: u32) -> &'static str {
45 match kind {
46 0 => "Undefined",
47 1 => "Keyboard",
48 2 => "Mouse",
49 3 => "GameController",
50 4 => "Peripheral",
51 5 => "Scene",
52 6 => "Notification",
53 7 => "Chassis",
54 8 => "Wearable",
55 9 => "Furniture",
56 10 => "Art",
57 11 => "Headset",
58 0x100.. => "Vendor-defined",
59 _ => "Reserved",
60 }
61}
62
63#[derive(Debug, Clone)]
65pub struct LampAttributes {
66 pub lamp_id: u16,
67 pub position_x_um: u32,
69 pub position_y_um: u32,
70 pub position_z_um: u32,
71 pub update_latency_us: u32,
73 pub lamp_purposes: u32,
74 pub red_level_count: u8,
75 pub green_level_count: u8,
76 pub blue_level_count: u8,
77 pub intensity_level_count: u8,
78 pub is_programmable: bool,
79 pub input_binding: u8,
80}
81
82#[derive(Debug, Clone, Copy)]
84pub struct LampColor {
85 pub lamp_id: u16,
86 pub red: u8,
87 pub green: u8,
88 pub blue: u8,
89 pub intensity: u8,
90}
91
92#[derive(Debug, Clone)]
94pub struct LampArrayAttributes {
95 pub lamp_count: u16,
96 pub width_um: u32,
98 pub height_um: u32,
99 pub depth_um: u32,
100 pub kind: u32,
101 pub kind_name: &'static str,
102 pub min_update_interval_us: u32,
103}
104
105#[derive(Debug, Clone)]
107pub struct LedRgbAttributes<'a> {
108 pub name: &'a str,
109 pub path: &'a str,
110 pub protocol: &'static str,
111 pub report_id: u8,
112 pub channel_size: u32,
113 pub has_intensity: bool,
114}
115
116struct HidrawFd {
120 fd: std::fs::File,
121}
122
123impl HidrawFd {
124 fn open(path: &str) -> Result<Self> {
125 let fd = OpenOptions::new()
126 .read(true)
127 .write(true)
128 .open(path)
129 .map_err(|e| match e.kind() {
130 std::io::ErrorKind::PermissionDenied => Error::PermissionDenied {
131 path: path.to_string(),
132 },
133 std::io::ErrorKind::NotFound => Error::DeviceNotFound {
134 path: path.to_string(),
135 },
136 _ => Error::Io(e),
137 })?;
138 Ok(Self { fd })
139 }
140
141 fn feat_get(&self, report_id: u8, size: usize) -> Result<Vec<u8>> {
143 let buf_len = size + 1; let mut buf = vec![0u8; buf_len];
145 buf[0] = report_id;
146 let ret = unsafe {
147 libc::ioctl(
148 self.fd.as_raw_fd(),
149 hidiocgfeature(buf_len) as _,
150 buf.as_mut_ptr(),
151 )
152 };
153 if ret < 0 {
154 return Err(std::io::Error::last_os_error().into());
155 }
156 buf.truncate(ret as usize);
162 Ok(buf)
163 }
164
165 fn feat_set(&self, buf: &[u8]) -> Result<()> {
167 let ret = unsafe {
168 libc::ioctl(
169 self.fd.as_raw_fd(),
170 hidiocsfeature(buf.len()) as _,
171 buf.as_ptr(),
172 )
173 };
174 if ret < 0 {
175 return Err(std::io::Error::last_os_error().into());
176 }
177 Ok(())
178 }
179
180 fn output_set(&self, buf: &[u8]) -> Result<()> {
185 use std::io::Write;
186 (&self.fd).write_all(buf)?;
187 Ok(())
188 }
189}
190
191fn require_report<'a>(report: &'a Option<ReportInfo>, name: &str) -> Result<&'a ReportInfo> {
194 report.as_ref().ok_or_else(|| Error::MissingReport {
195 report_name: name.to_string(),
196 })
197}
198
199fn parse_lamp_response(buf: &[u8]) -> Result<LampAttributes> {
209 if buf.len() < 29 {
213 return Err(Error::TruncatedReport {
214 report_name: "LampAttributesResponse",
215 expected: 29,
216 got: buf.len(),
217 });
218 }
219 let lamp_id = u16::from_le_bytes([buf[1], buf[2]]);
220 let pos_x = u32::from_le_bytes([buf[3], buf[4], buf[5], buf[6]]);
221 let pos_y = u32::from_le_bytes([buf[7], buf[8], buf[9], buf[10]]);
222 let pos_z = u32::from_le_bytes([buf[11], buf[12], buf[13], buf[14]]);
223 let latency = u32::from_le_bytes([buf[15], buf[16], buf[17], buf[18]]);
224 let purposes = u32::from_le_bytes([buf[19], buf[20], buf[21], buf[22]]);
225
226 Ok(LampAttributes {
227 lamp_id,
228 position_x_um: pos_x,
229 position_y_um: pos_y,
230 position_z_um: pos_z,
231 update_latency_us: latency,
232 lamp_purposes: purposes,
233 red_level_count: buf[23],
234 green_level_count: buf[24],
235 blue_level_count: buf[25],
236 intensity_level_count: buf[26],
237 is_programmable: buf[27] != 0,
238 input_binding: buf[28],
239 })
240}
241
242pub struct LampArrayDevice<'a> {
251 info: &'a DeviceInfo,
252}
253
254impl<'a> LampArrayDevice<'a> {
255 pub fn new(info: &'a DeviceInfo) -> Self {
256 debug_assert!(matches!(info.kind, DeviceKind::LampArray(_)));
257 Self { info }
258 }
259
260 pub fn path(&self) -> &str {
262 &self.info.hidraw_path
263 }
264
265 pub fn name(&self) -> &str {
267 &self.info.name
268 }
269
270 fn reports(&self) -> &LampArrayReports {
271 match &self.info.kind {
272 DeviceKind::LampArray(r) => r,
273 _ => unreachable!(),
274 }
275 }
276
277 pub fn get_attributes(&self) -> Result<LampArrayAttributes> {
282 let fd = HidrawFd::open(&self.info.hidraw_path)?;
283 self.get_attributes_with_fd(&fd)
284 }
285
286 fn get_attributes_with_fd(&self, fd: &HidrawFd) -> Result<LampArrayAttributes> {
287 let rinfo = require_report(&self.reports().attributes, "attributes")?;
288 let buf = fd.feat_get(rinfo.report_id, rinfo.size)?;
289
290 if buf.len() < 23 {
293 return Err(Error::TruncatedReport {
294 report_name: "LampArrayAttributes",
295 expected: 23,
296 got: buf.len(),
297 });
298 }
299
300 let lamp_count = u16::from_le_bytes([buf[1], buf[2]]);
303 let width = u32::from_le_bytes([buf[3], buf[4], buf[5], buf[6]]);
304 let height = u32::from_le_bytes([buf[7], buf[8], buf[9], buf[10]]);
305 let depth = u32::from_le_bytes([buf[11], buf[12], buf[13], buf[14]]);
306 let kind = u32::from_le_bytes([buf[15], buf[16], buf[17], buf[18]]);
307 let interval = u32::from_le_bytes([buf[19], buf[20], buf[21], buf[22]]);
308
309 Ok(LampArrayAttributes {
310 lamp_count,
311 width_um: width,
312 height_um: height,
313 depth_um: depth,
314 kind,
315 kind_name: lamp_array_kind_name(kind),
316 min_update_interval_us: interval,
317 })
318 }
319
320 pub fn get_lamp(&self, index: u16) -> Result<LampAttributes> {
325 let fd = HidrawFd::open(&self.info.hidraw_path)?;
326 self.get_lamp_with_fd(&fd, index)
327 }
328
329 fn get_lamp_with_fd(&self, fd: &HidrawFd, index: u16) -> Result<LampAttributes> {
330 let req_info = require_report(&self.reports().attr_request, "attr_request")?;
331 let mut req_buf = vec![0u8; req_info.size + 1];
332 req_buf[0] = req_info.report_id;
333 req_buf[1..3].copy_from_slice(&index.to_le_bytes());
334 fd.feat_set(&req_buf)?;
335
336 let resp_info = require_report(&self.reports().attr_response, "attr_response")?;
337 let buf = fd.feat_get(resp_info.report_id, resp_info.size)?;
338 let lamp = parse_lamp_response(&buf)?;
339
340 if lamp.lamp_id != index {
343 return Err(Error::LampIdMismatch {
344 expected: index,
345 got: lamp.lamp_id,
346 });
347 }
348
349 Ok(lamp)
350 }
351
352 fn read_all_lamps_with_fd(
361 &self,
362 fd: &HidrawFd,
363 lamp_count: u16,
364 ) -> Result<Vec<LampAttributes>> {
365 if lamp_count == 0 {
366 return Ok(Vec::new());
367 }
368
369 let req_info = require_report(&self.reports().attr_request, "attr_request")?;
371 let mut req_buf = vec![0u8; req_info.size + 1];
372 req_buf[0] = req_info.report_id;
373 fd.feat_set(&req_buf)?;
375
376 let resp_info = require_report(&self.reports().attr_response, "attr_response")?;
378 let mut lamps = Vec::with_capacity(lamp_count as usize);
379
380 for expected_id in 0..lamp_count {
381 let buf = fd.feat_get(resp_info.report_id, resp_info.size)?;
382 let lamp = parse_lamp_response(&buf)?;
383 if lamp.lamp_id != expected_id {
384 return Err(Error::LampIdMismatch {
385 expected: expected_id,
386 got: lamp.lamp_id,
387 });
388 }
389 lamps.push(lamp);
390 }
391
392 Ok(lamps)
393 }
394
395 pub fn get_attributes_and_lamps(&self) -> Result<(LampArrayAttributes, Vec<LampAttributes>)> {
401 let fd = HidrawFd::open(&self.info.hidraw_path)?;
402 let attrs = self.get_attributes_with_fd(&fd)?;
403 let lamps = self.read_all_lamps_with_fd(&fd, attrs.lamp_count)?;
404 Ok((attrs, lamps))
405 }
406
407 pub fn set_autonomous(&self, enabled: bool) -> Result<()> {
413 let fd = HidrawFd::open(&self.info.hidraw_path)?;
414 self.set_autonomous_with_fd(&fd, enabled)
415 }
416
417 pub fn get_autonomous(&self) -> Result<bool> {
425 let ctrl_info = require_report(&self.reports().control, "control")?;
426 let fd = HidrawFd::open(&self.info.hidraw_path)?;
427 let buf = fd.feat_get(ctrl_info.report_id, ctrl_info.size)?;
428 Ok(buf.get(1).copied().unwrap_or(0) != 0)
432 }
433
434 fn set_autonomous_with_fd(&self, fd: &HidrawFd, enabled: bool) -> Result<()> {
435 require_report(&self.reports().control, "control")?;
436 self.try_set_autonomous_with_fd(fd, enabled)
437 }
438
439 fn try_set_autonomous_with_fd(&self, fd: &HidrawFd, enabled: bool) -> Result<()> {
443 let ctrl_info = match &self.reports().control {
444 Some(info) => info,
445 None => return Ok(()),
446 };
447 let mut buf = vec![0u8; ctrl_info.size + 1];
448 buf[0] = ctrl_info.report_id;
449 buf[1] = if enabled { 0x01 } else { 0x00 };
450 fd.feat_set(&buf)
451 }
452
453 pub fn set_color(&self, r: u8, g: u8, b: u8, intensity: u8) -> Result<()> {
471 let range_info = require_report(&self.reports().range_update, "range_update")?;
472 let range_report_id = range_info.report_id;
473 let range_size = range_info.size;
474
475 let fd = HidrawFd::open(&self.info.hidraw_path)?;
476 self.try_set_autonomous_with_fd(&fd, false)?;
477
478 let attrs = self.get_attributes_with_fd(&fd)?;
479 if attrs.lamp_count == 0 {
480 return Ok(());
481 }
482 let lamp_end = attrs.lamp_count - 1;
483
484 let lamps = self.read_all_lamps_with_fd(&fd, attrs.lamp_count)?;
486
487 let (max_r, max_g, max_b) = lamps.iter().filter(|l| l.is_programmable).fold(
492 (255u32, 255u32, 255u32),
493 |(mr, mg, mb), l| {
494 (
495 mr.min(l.red_level_count as u32),
496 mg.min(l.green_level_count as u32),
497 mb.min(l.blue_level_count as u32),
498 )
499 },
500 );
501 let max_i = lamps
502 .iter()
503 .map(|l| l.intensity_level_count as u32)
504 .min()
505 .unwrap_or(255);
506
507 let mut buf = vec![0u8; range_size + 1];
511 buf[0] = range_report_id;
512 buf[1] = 0x01; buf[2..4].copy_from_slice(&0u16.to_le_bytes());
514 buf[4..6].copy_from_slice(&lamp_end.to_le_bytes());
515 buf[6] = scale_u8(r, max_r);
516 buf[7] = scale_u8(g, max_g);
517 buf[8] = scale_u8(b, max_b);
518 buf[9] = scale_u8(intensity, max_i);
519
520 fd.feat_set(&buf)
521 }
522
523 pub fn set_lamp_colors(&self, colors: &[LampColor]) -> Result<()> {
546 if colors.is_empty() {
547 return Ok(());
548 }
549
550 let multi_info = require_report(&self.reports().multi_update, "multi_update")?;
551 let multi_report_id = multi_info.report_id;
552 let multi_size = multi_info.size;
553
554 let slot_count = (multi_size.saturating_sub(2)) / 6;
557 if slot_count == 0 {
558 return Err(Error::MissingReport {
559 report_name: "multi_update (invalid size)".to_string(),
560 });
561 }
562
563 let fd = HidrawFd::open(&self.info.hidraw_path)?;
564 self.try_set_autonomous_with_fd(&fd, false)?;
565
566 let attrs = self.get_attributes_with_fd(&fd)?;
568 let lamps = self.read_all_lamps_with_fd(&fd, attrs.lamp_count)?;
569
570 for c in colors {
572 if c.lamp_id >= attrs.lamp_count {
573 return Err(Error::LampIdOutOfRange {
574 lamp_id: c.lamp_id,
575 lamp_count: attrs.lamp_count,
576 });
577 }
578 }
579
580 let mut seen = HashSet::with_capacity(colors.len());
582 for c in colors {
583 if !seen.insert(c.lamp_id) {
584 return Err(Error::DuplicateLampId { lamp_id: c.lamp_id });
585 }
586 }
587
588 let scaled: Vec<LampColor> = colors
592 .iter()
593 .map(|c| {
594 let lamp = &lamps[c.lamp_id as usize];
595 if lamp.is_programmable {
596 LampColor {
597 lamp_id: c.lamp_id,
598 red: scale_u8(c.red, lamp.red_level_count as u32),
599 green: scale_u8(c.green, lamp.green_level_count as u32),
600 blue: scale_u8(c.blue, lamp.blue_level_count as u32),
601 intensity: scale_u8(c.intensity, lamp.intensity_level_count as u32),
602 }
603 } else {
604 LampColor {
607 lamp_id: c.lamp_id,
608 red: 0,
609 green: 0,
610 blue: 0,
611 intensity: scale_u8(c.intensity, lamp.intensity_level_count as u32),
612 }
613 }
614 })
615 .collect();
616
617 let total_chunks = scaled.len().div_ceil(slot_count);
618
619 for (chunk_idx, chunk) in scaled.chunks(slot_count).enumerate() {
620 let is_last = chunk_idx == total_chunks - 1;
621 let mut buf = vec![0u8; multi_size + 1];
622 buf[0] = multi_report_id;
623
624 buf[1] = chunk.len() as u8; buf[2] = if is_last { 0x01 } else { 0x00 }; let ids_start = 3;
632 for (j, c) in chunk.iter().enumerate() {
633 let off = ids_start + j * 2;
634 buf[off..off + 2].copy_from_slice(&c.lamp_id.to_le_bytes());
635 }
636
637 let rgbi_start = ids_start + slot_count * 2;
639 for (j, c) in chunk.iter().enumerate() {
640 let off = rgbi_start + j * 4;
641 buf[off] = c.red;
642 buf[off + 1] = c.green;
643 buf[off + 2] = c.blue;
644 buf[off + 3] = c.intensity;
645 }
646
647 fd.feat_set(&buf)?;
648 }
649
650 Ok(())
651 }
652
653 pub fn summary(&self) -> &'static str {
658 let reports = self.reports();
659 let has_range = reports.range_update.is_some();
660 let has_multi = reports.multi_update.is_some();
661 match (has_range, has_multi) {
662 (true, true) => "LampArray (range+multi update)",
663 (true, false) => "LampArray (range update)",
664 (false, true) => "LampArray (multi update)",
665 (false, false) => "LampArray",
666 }
667 }
668}
669
670pub struct LedRgbDevice<'a> {
682 info: &'a DeviceInfo,
683}
684
685impl<'a> LedRgbDevice<'a> {
686 pub fn new(info: &'a DeviceInfo) -> Self {
687 debug_assert!(matches!(info.kind, DeviceKind::LedRgb(_)));
688 Self { info }
689 }
690
691 pub fn path(&self) -> &str {
693 &self.info.hidraw_path
694 }
695
696 pub fn name(&self) -> &str {
698 &self.info.name
699 }
700
701 fn channels(&self) -> &LedRgbChannelInfo {
702 match &self.info.kind {
703 DeviceKind::LedRgb(c) => c,
704 _ => unreachable!(),
705 }
706 }
707
708 pub fn set_color(&self, r: u8, g: u8, b: u8, intensity: u8) -> Result<()> {
718 let ch = self.channels();
719 let fd = HidrawFd::open(&self.info.hidraw_path)?;
720 let mut buf = vec![0u8; ch.report_size + 1];
721 buf[0] = ch.report_id;
722
723 buf[1 + ch.red_offset] = scale_u8(r, ch.red_logical_max);
726 buf[1 + ch.blue_offset] = scale_u8(b, ch.blue_logical_max);
727 buf[1 + ch.green_offset] = scale_u8(g, ch.green_logical_max);
728
729 if let Some(off) = ch.intensity_offset {
730 let int_max = ch.intensity_logical_max.unwrap_or(255);
731 buf[1 + off] = scale_u8(intensity, int_max);
732 }
733
734 match ch.report_type {
735 ReportType::Feature => fd.feat_set(&buf),
736 ReportType::Output => fd.output_set(&buf),
737 ReportType::Input => Err(Error::UnsupportedReportType),
738 }
739 }
740
741 pub fn get_attributes(&self) -> LedRgbAttributes<'_> {
743 let ch = self.channels();
744 LedRgbAttributes {
745 name: &self.info.name,
746 path: &self.info.hidraw_path,
747 protocol: "LED Page RGB (Usage Page 0x08, Section 11.7)",
748 report_id: ch.report_id,
749 channel_size: ch.channel_size,
750 has_intensity: ch.intensity_offset.is_some(),
751 }
752 }
753
754 pub fn summary(&self) -> &'static str {
756 "LED RGB"
757 }
758}