1use crate::traits::ToJson;
77use anyhow::{Result, anyhow};
78use serde::{Deserialize, Serialize};
79use std::{
80 fmt::Display,
81 fs::{read, read_dir, read_to_string},
82 path::Path,
83};
84
85#[derive(Debug, Serialize, Deserialize, Clone)]
87pub struct Video {
88 pub devices: Vec<DRM>,
89}
90
91impl Video {
92 pub fn new() -> Result<Self> {
93 let prefix = Path::new("/sys/class/drm/");
94 let mut devices = vec![];
95
96 for i in 0..=u8::MAX {
97 let path = prefix.join(format!("card{i}"));
98 if !path.is_dir() {
99 continue;
100 }
101 let dir_contents = read_dir(path)?.filter(|dir| match &dir {
102 Ok(dir) => dir.path().is_dir(),
103 Err(_) => false,
104 });
105
106 for d in dir_contents {
107 let d = d?.path(); let fname = match d.file_name() {
109 Some(fname) => fname.to_str().unwrap_or(""),
110 None => "",
111 };
112 if d.is_dir() && fname.contains("card") {
113 let drm = DRM::new(d)?;
114 if !drm.is_empty_info() {
115 devices.push(drm);
116 }
117 }
118 }
119 }
120 Ok(Self { devices })
121 }
122}
123
124impl ToJson for Video {}
125
126#[derive(Debug, Serialize, Deserialize, Clone)]
128pub struct DRM {
129 pub name: Option<String>,
131
132 pub enabled: bool,
134
135 pub edid: Option<EDID>,
137
138 pub modes: Vec<String>,
140}
141
142impl DRM {
143 pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
144 let path = path.as_ref();
145 let name = path
146 .components()
147 .last()
148 .and_then(|n| Some(n.as_os_str().display().to_string()));
149 let enabled = {
150 let txt = read_to_string(path.join("enabled"));
151 match txt {
152 Ok(txt) => {
153 let contents = txt.trim();
154 if contents == "enabled" { true } else { false }
155 }
156 Err(_) => false,
157 }
158 };
159 let modes = read_to_string(path.join("modes"))?
160 .lines()
161 .map(|s| s.to_string())
162 .collect::<Vec<_>>();
163 let edid = EDID::new(path);
164
165 Ok(Self {
166 name,
167 enabled,
168 edid: match edid {
169 Ok(edid) => Some(edid),
170 Err(why) => {
171 if enabled {
173 return Err(why);
174 } else {
175 None
176 }
177 }
178 },
179 modes,
180 })
181 }
182
183 pub fn is_empty_info(&self) -> bool {
184 !self.enabled && self.edid.is_none() && self.modes.is_empty()
185 }
186}
187
188#[derive(Debug, Serialize, Deserialize, Clone)]
192pub struct EDID {
193 pub raw: Vec<u8>,
195
196 pub manufacturer: String,
203
204 pub description: Option<String>,
207
208 pub product_code: u16,
213
214 pub serial_number: u32,
220
221 pub model: String,
225
226 pub serial: Option<String>,
230
231 pub week: u8,
237
238 pub year: u16,
242
243 pub edid_version: u8,
247
248 pub edid_revision: u8,
252
253 pub video_input: VideoInputParams,
257
258 pub hscreen_size: u8,
263
264 pub vscreen_size: u8,
268
269 pub diagonal_inches: Option<f32>,
273
274 pub aspect_ratio: Option<String>,
277
278 pub resolution_width: Option<u32>,
281
282 pub resolution_height: Option<u32>,
284
285 pub pixel_clock_mhz: Option<f32>,
287
288 pub display_gamma: u8,
294
295 pub detailed_timings: Vec<DetailedTiming>,
298
299 pub range_limits: Option<RangeLimits>,
302
303 pub extension_blocks: u8,
307
308 pub checksum: u8,
312}
313
314impl EDID {
315 pub fn new<P: AsRef<Path>>(edid_dir_path: P) -> Result<Self> {
317 let path = edid_dir_path.as_ref().join("edid");
318 let data = read(&path)
319 .map_err(|err| anyhow!("Failed to read EDID file at: {}: {}", path.display(), err))?;
320
321 if data.len() < 128 {
322 return Err(anyhow!("EDID data too short ({} bytes)", data.len()));
323 }
324
325 if data[0..8] != [0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00] {
326 return Err(anyhow!("Invalid EDID header on path {}", path.display()));
327 }
328
329 let manufacturer = {
330 let word = u16::from_be_bytes([data[8], data[9]]);
331
332 let c1 = ((word >> 10) & 0x1F) as u8 + 64;
333 let c2 = ((word >> 5) & 0x1F) as u8 + 64;
334 let c3 = (word & 0x1F) as u8 + 64;
335
336 format!("{}{}{}", c1 as char, c2 as char, c3 as char)
337 };
338
339 let product_code = u16::from_le_bytes([data[10], data[11]]);
340 let serial_number = u32::from_le_bytes([data[12], data[13], data[14], data[15]]);
341
342 let week = data[16];
343 let year = data[17] as u16 + 1990;
344 let edid_version = data[18];
345 let edid_revision = data[19];
346
347 let video_input = VideoInputParams::new(&data);
348 let hscreen_size = data[21];
349 let vscreen_size = data[22];
350 let display_gamma = data[23];
351
352 let diagonal_inches = if hscreen_size > 0 && vscreen_size > 0 {
353 let diag_cm = ((hscreen_size as f32).powi(2) + (vscreen_size as f32).powi(2)).sqrt();
354 Some(diag_cm / 2.54)
355 } else {
356 None
357 };
358
359 let mut model = String::new();
360 let mut description = None::<String>;
361 let mut serial = None::<String>;
362 let mut detailed_timings = Vec::new();
363 let mut range_limits = None;
364
365 for i in 0..4 {
366 let start = 54 + i * 18;
367 let block = &data[start..start + 18];
368
369 if block[0] == 0x00 && block[1] == 0x00 {
370 match block[3] {
371 0xFC => model = extract_text(block),
372 0xFE => description = Some(extract_text(block)),
373 0xFF => serial = Some(extract_text(block)),
374 0xFD => range_limits = Some(RangeLimits::parse(block)),
375 _ => {}
376 }
377 } else {
378 detailed_timings.push(DetailedTiming::parse(block));
379 }
380 }
381
382 let (resolution_width, resolution_height, pixel_clock_mhz, aspect_ratio) = detailed_timings
383 .first()
384 .map_or((None, None, None, None), |dtd| {
385 (
386 Some(dtd.h_active),
387 Some(dtd.v_active),
388 Some(dtd.pixel_clock_hz as f32 / 1_000_000.),
389 Some(dtd.aspect_ratio.clone()),
390 )
391 });
392
393 let extension_blocks = data[126];
394 let checksum = data[127];
395
396 Ok(Self {
397 raw: data,
398
399 manufacturer,
400 product_code,
401 serial_number,
402 week,
403 year,
404 edid_version,
405 edid_revision,
406 video_input,
407 hscreen_size,
408 vscreen_size,
409 diagonal_inches,
410 display_gamma,
411 pixel_clock_mhz,
412 aspect_ratio,
413 resolution_width,
414 resolution_height,
415
416 model,
417 description,
418 serial,
419
420 detailed_timings,
421 range_limits,
422
423 extension_blocks,
424 checksum,
425 })
426 }
427}
428
429fn extract_text(block: &[u8]) -> String {
430 let data = &block[5..18];
431 let end = data
432 .iter()
433 .position(|&b| b == 0x0A || b == 0x00)
434 .unwrap_or(data.len());
435 String::from_utf8_lossy(&data[..end]).trim().to_string()
436}
437
438#[derive(Debug, Serialize, Deserialize, Clone)]
443pub struct DetailedTiming {
444 pub pixel_clock_hz: u64,
446
447 pub h_active: u32,
449
450 pub h_blanking: u32,
452
453 pub v_active: u32,
455
456 pub v_blanking: u32,
458
459 pub h_front_porch: u32,
461
462 pub h_sync_pulse: u32,
464
465 pub v_front_porch: u32,
467
468 pub v_sync_pulse: u32,
470
471 pub h_back_porch: u32,
473
474 pub v_back_porch: u32,
476
477 pub h_sync_positive: bool,
479
480 pub v_sync_positive: bool,
482
483 pub aspect_ratio: String,
485}
486
487impl DetailedTiming {
488 pub fn parse(block: &[u8]) -> Self {
490 let pixel_clock_10khz = u16::from_le_bytes([block[0], block[1]]);
491 let pixel_clock_hz = pixel_clock_10khz as u64 * 10_000;
492
493 let h_active_low = block[2] as u32;
495 let h_blanking_low = block[3] as u32;
496 let h_active_high = ((block[4] >> 4) as u32) << 8;
497 let h_blanking_high = ((block[4] & 0x0F) as u32) << 8;
498
499 let h_active = h_active_high | h_active_low;
500 let h_blanking = h_blanking_high | h_blanking_low;
501
502 let v_active_low = block[5] as u32;
504 let v_blanking_low = block[6] as u32;
505 let v_active_high = ((block[7] >> 4) as u32) << 8;
506 let v_blanking_high = ((block[7] & 0x0F) as u32) << 8;
507
508 let v_active = v_active_high | v_active_low;
509 let v_blanking = v_blanking_high | v_blanking_low;
510
511 let h_sync_offset_low = (block[8] >> 4) as u32;
513 let h_sync_pulse_low = (block[8] & 0x0F) as u32;
514 let v_sync_offset_low = (block[9] >> 4) as u32;
515 let v_sync_pulse_low = (block[9] & 0x0F) as u32;
516
517 let h_sync_offset_high = ((block[11] >> 2) & 0x03) as u32;
518 let h_sync_pulse_high = (block[11] & 0x03) as u32;
519 let v_sync_offset_high = ((block[11] >> 6) & 0x03) as u32;
520 let v_sync_pulse_high = ((block[11] >> 4) & 0x03) as u32;
521
522 let h_front_porch = (h_sync_offset_high << 4) | h_sync_offset_low;
523 let h_sync_pulse = (h_sync_pulse_high << 4) | h_sync_pulse_low;
524 let v_front_porch = (v_sync_offset_high << 4) | v_sync_offset_low;
525 let v_sync_pulse = (v_sync_pulse_high << 4) | v_sync_pulse_low;
526
527 let h_back_porch = h_blanking.saturating_sub(h_front_porch + h_sync_pulse);
530 let v_back_porch = v_blanking.saturating_sub(v_front_porch + v_sync_pulse);
531
532 let h_sync_positive = (block[17] & 0x02) != 0;
533 let v_sync_positive = (block[17] & 0x04) != 0;
534
535 let aspect_ratio = calc_aspect_ratio(h_active, v_active);
536
537 Self {
538 pixel_clock_hz,
539 h_active,
540 h_blanking,
541 v_active,
542 v_blanking,
543 h_front_porch,
544 h_sync_pulse,
545 v_front_porch,
546 v_sync_pulse,
547 h_back_porch,
548 v_back_porch,
549 h_sync_positive,
550 v_sync_positive,
551 aspect_ratio,
552 }
553 }
554}
555
556fn calc_aspect_ratio(width: u32, height: u32) -> String {
557 if width == 0 || height == 0 {
558 return "??:??".to_string();
559 }
560
561 let ratio = width as f64 / height as f64;
562
563 if (ratio - 2.3333).abs() < 0.05 {
564 "21:9".to_string()
565 } else if (ratio - 1.7777).abs() < 0.05 {
566 "16:9".to_string()
567 } else if (ratio - 1.6).abs() < 0.05 {
568 "16:10".to_string()
569 } else if (ratio - 1.5).abs() < 0.05 {
570 "3:2".to_string()
571 } else if (ratio - 1.3333).abs() < 0.05 {
572 "4:3".to_string()
573 } else if (ratio - 1.25).abs() < 0.05 {
574 "5:4".to_string()
575 } else {
576 format!("{ratio:.2}:1")
577 }
578}
579
580#[derive(Debug, Serialize, Deserialize, Clone)]
585pub struct RangeLimits {
586 pub min_v_freq_hz: u8,
588
589 pub max_v_freq_hz: u8,
591
592 pub min_h_freq_khz: u8,
594
595 pub max_h_freq_khz: u8,
597
598 pub max_pixel_clock_mhz: u16,
601}
602
603impl RangeLimits {
604 pub fn parse(block: &[u8]) -> Self {
606 Self {
607 min_v_freq_hz: block[5],
608 max_v_freq_hz: block[6],
609 min_h_freq_khz: block[7],
610 max_h_freq_khz: block[8],
611 max_pixel_clock_mhz: block[9] as u16 * 10,
612 }
613 }
614}
615
616#[derive(Debug, Serialize, Deserialize, Clone)]
618pub enum VideoInputParams {
619 Digital(VideoInputParamsDigital),
620 Analog(VideoInputParamsAnalog),
621}
622
623impl VideoInputParams {
624 pub fn new(data: &[u8]) -> Self {
625 let d = data[20];
626 let bit_depth = ((d >> 7) & 0b00000111) as u8;
627 if bit_depth == 1 {
628 Self::Digital(VideoInputParamsDigital::new(data))
629 } else if bit_depth == 0 {
630 Self::Analog(VideoInputParamsAnalog::new(data))
631 } else {
632 panic!("Unknown 7 bit of 20 byte ({bit_depth})!")
633 }
634 }
635}
636
637#[derive(Debug, Serialize, Deserialize, Clone)]
639pub struct VideoInputParamsDigital {
640 pub bit_depth: BitDepth,
642
643 pub video_interface: VideoInterface,
645}
646
647impl VideoInputParamsDigital {
648 pub fn new(data: &[u8]) -> Self {
649 let d = data[20];
650 let bit_depth = BitDepth::from(((d >> 4) & 0b00000111) as u8);
651 let video_interface = VideoInterface::from((d & 0b00000111) as u8);
652
653 Self {
654 bit_depth,
655 video_interface,
656 }
657 }
658}
659
660#[derive(Debug, Serialize, Deserialize, Clone)]
662pub enum BitDepth {
663 Undefined,
664
665 B6,
667
668 B8,
670
671 B10,
673
674 B12,
676
677 B14,
679
680 B16,
682
683 Reserved,
685
686 Unknown(u8),
688}
689
690impl From<u8> for BitDepth {
691 fn from(value: u8) -> Self {
692 match value {
693 0b000 => Self::Undefined,
694 0b001 => Self::B6,
695 0b010 => Self::B8,
696 0b011 => Self::B10,
697 0b100 => Self::B12,
698 0b101 => Self::B14,
699 0b110 => Self::B16,
700 0b111 => Self::Reserved,
701 _ => Self::Unknown(value),
702 }
703 }
704}
705
706impl Display for BitDepth {
707 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
708 write!(
709 f,
710 "{}",
711 match self {
712 Self::Undefined => "Undefined".to_string(),
713 Self::B6 => "6 bits".to_string(),
714 Self::B8 => "8 bits".to_string(),
715 Self::B10 => "10 bits".to_string(),
716 Self::B12 => "12 bits".to_string(),
717 Self::B14 => "14 bits".to_string(),
718 Self::B16 => "16 bits".to_string(),
719 Self::Reserved => "Reserved value".to_string(),
720 Self::Unknown(val) => format!("Unknown ({val})"),
721 }
722 )
723 }
724}
725
726#[derive(Debug, Serialize, Deserialize, Clone)]
728pub enum VideoInterface {
729 Undefined,
730 DVI,
731 HDMIa,
732 HDMIb,
733 MDDI,
734 DisplayPort,
735 Unknown(u8),
736}
737
738impl From<u8> for VideoInterface {
739 fn from(value: u8) -> Self {
740 match value {
741 0b0000 => Self::Undefined,
742 0b0001 => Self::DVI,
743 0b0010 => Self::HDMIa,
744 0b0011 => Self::HDMIb,
745 0b0100 => Self::MDDI,
746 0b0101 => Self::DisplayPort,
747 _ => Self::Unknown(value),
748 }
749 }
750}
751
752impl Display for VideoInterface {
753 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
754 write!(
755 f,
756 "{}",
757 match self {
758 Self::Undefined => "Undefined".to_string(),
759 Self::DVI => "DVI".to_string(),
760 Self::HDMIa => "HDMI-a".to_string(),
761 Self::HDMIb => "HDMI-b".to_string(),
762 Self::MDDI => "MDDI".to_string(),
763 Self::DisplayPort => "Display Port".to_string(),
764 Self::Unknown(val) => format!("Unknown (code: {val})"),
765 }
766 )
767 }
768}
769
770#[derive(Debug, Serialize, Deserialize, Clone)]
771pub struct VideoInputParamsAnalog {
772 pub white_sync_levels: u8,
781
782 pub blank_to_black_setup: u8,
784
785 pub separate_sync_supported: u8,
787
788 pub composite_sync_supported: u8,
790
791 pub sync_on_green_supported: u8,
793
794 pub sync_on_green_isused: u8,
797}
798
799impl VideoInputParamsAnalog {
800 pub fn new(data: &[u8]) -> Self {
802 let d = data[20];
803 let white_sync_levels = ((d >> 5) & 0b00000011) as u8;
804 let blank_to_black_setup = (d >> 4) as u8;
805 let separate_sync_supported = (d >> 3) as u8;
806 let composite_sync_supported = (d >> 2) as u8;
807 let sync_on_green_supported = (d >> 1) as u8;
808 let sync_on_green_isused = (d >> 0) as u8; Self {
811 white_sync_levels,
812 blank_to_black_setup,
813 separate_sync_supported,
814 composite_sync_supported,
815 sync_on_green_supported,
816 sync_on_green_isused,
817 }
818 }
819}