use crate::traits::ToJson;
use anyhow::{Result, anyhow};
use serde::{Deserialize, Serialize};
use std::{
fmt::Display,
fs::{read, read_dir, read_to_string},
path::Path,
};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Video {
pub devices: Vec<DRM>,
}
impl Video {
pub fn new() -> Result<Self> {
let prefix = Path::new("/sys/class/drm/");
let mut devices = vec![];
for i in 0..=u8::MAX {
let path = prefix.join(format!("card{i}"));
if !path.is_dir() {
continue;
}
let dir_contents = read_dir(path)?.filter(|dir| match &dir {
Ok(dir) => dir.path().is_dir(),
Err(_) => false,
});
for d in dir_contents {
let d = d?.path(); let fname = match d.file_name() {
Some(fname) => fname.to_str().unwrap_or(""),
None => "",
};
if d.is_dir() && fname.contains("card") {
let drm = DRM::new(d)?;
if !drm.is_empty_info() {
devices.push(drm);
}
}
}
}
Ok(Self { devices })
}
}
impl ToJson for Video {}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DRM {
pub name: Option<String>,
pub enabled: bool,
pub edid: Option<EDID>,
pub modes: Vec<String>,
}
impl DRM {
pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = path.as_ref();
let name = path
.components()
.last()
.and_then(|n| Some(n.as_os_str().display().to_string()));
let enabled = {
let txt = read_to_string(path.join("enabled"));
match txt {
Ok(txt) => {
let contents = txt.trim();
if contents == "enabled" { true } else { false }
}
Err(_) => false,
}
};
let modes = read_to_string(path.join("modes"))?
.lines()
.map(|s| s.to_string())
.collect::<Vec<_>>();
let edid = EDID::new(path);
Ok(Self {
name,
enabled,
edid: match edid {
Ok(edid) => Some(edid),
Err(why) => {
if enabled {
return Err(why);
} else {
None
}
}
},
modes,
})
}
pub fn is_empty_info(&self) -> bool {
!self.enabled && self.edid.is_none() && self.modes.is_empty()
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct EDID {
pub raw: Vec<u8>,
pub manufacturer: String,
pub description: Option<String>,
pub product_code: u16,
pub serial_number: u32,
pub model: String,
pub serial: Option<String>,
pub week: u8,
pub year: u16,
pub edid_version: u8,
pub edid_revision: u8,
pub video_input: VideoInputParams,
pub hscreen_size: u8,
pub vscreen_size: u8,
pub diagonal_inches: Option<f32>,
pub aspect_ratio: Option<String>,
pub resolution_width: Option<u32>,
pub resolution_height: Option<u32>,
pub pixel_clock_mhz: Option<f32>,
pub display_gamma: u8,
pub detailed_timings: Vec<DetailedTiming>,
pub range_limits: Option<RangeLimits>,
pub extension_blocks: u8,
pub checksum: u8,
}
impl EDID {
pub fn new<P: AsRef<Path>>(edid_dir_path: P) -> Result<Self> {
let path = edid_dir_path.as_ref().join("edid");
let data = read(&path)
.map_err(|err| anyhow!("Failed to read EDID file at: {}: {}", path.display(), err))?;
if data.len() < 128 {
return Err(anyhow!("EDID data too short ({} bytes)", data.len()));
}
if data[0..8] != [0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00] {
return Err(anyhow!("Invalid EDID header on path {}", path.display()));
}
let manufacturer = {
let word = u16::from_be_bytes([data[8], data[9]]);
let c1 = ((word >> 10) & 0x1F) as u8 + 64;
let c2 = ((word >> 5) & 0x1F) as u8 + 64;
let c3 = (word & 0x1F) as u8 + 64;
format!("{}{}{}", c1 as char, c2 as char, c3 as char)
};
let product_code = u16::from_le_bytes([data[10], data[11]]);
let serial_number = u32::from_le_bytes([data[12], data[13], data[14], data[15]]);
let week = data[16];
let year = data[17] as u16 + 1990;
let edid_version = data[18];
let edid_revision = data[19];
let video_input = VideoInputParams::new(&data);
let hscreen_size = data[21];
let vscreen_size = data[22];
let display_gamma = data[23];
let diagonal_inches = if hscreen_size > 0 && vscreen_size > 0 {
let diag_cm = ((hscreen_size as f32).powi(2) + (vscreen_size as f32).powi(2)).sqrt();
Some(diag_cm / 2.54)
} else {
None
};
let mut model = String::new();
let mut description = None::<String>;
let mut serial = None::<String>;
let mut detailed_timings = Vec::new();
let mut range_limits = None;
for i in 0..4 {
let start = 54 + i * 18;
let block = &data[start..start + 18];
if block[0] == 0x00 && block[1] == 0x00 {
match block[3] {
0xFC => model = extract_text(block),
0xFE => description = Some(extract_text(block)),
0xFF => serial = Some(extract_text(block)),
0xFD => range_limits = Some(RangeLimits::parse(block)),
_ => {}
}
} else {
detailed_timings.push(DetailedTiming::parse(block));
}
}
let (resolution_width, resolution_height, pixel_clock_mhz, aspect_ratio) = detailed_timings
.first()
.map_or((None, None, None, None), |dtd| {
(
Some(dtd.h_active),
Some(dtd.v_active),
Some(dtd.pixel_clock_hz as f32 / 1_000_000.),
Some(dtd.aspect_ratio.clone()),
)
});
let extension_blocks = data[126];
let checksum = data[127];
Ok(Self {
raw: data,
manufacturer,
product_code,
serial_number,
week,
year,
edid_version,
edid_revision,
video_input,
hscreen_size,
vscreen_size,
diagonal_inches,
display_gamma,
pixel_clock_mhz,
aspect_ratio,
resolution_width,
resolution_height,
model,
description,
serial,
detailed_timings,
range_limits,
extension_blocks,
checksum,
})
}
}
fn extract_text(block: &[u8]) -> String {
let data = &block[5..18];
let end = data
.iter()
.position(|&b| b == 0x0A || b == 0x00)
.unwrap_or(data.len());
String::from_utf8_lossy(&data[..end]).trim().to_string()
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DetailedTiming {
pub pixel_clock_hz: u64,
pub h_active: u32,
pub h_blanking: u32,
pub v_active: u32,
pub v_blanking: u32,
pub h_front_porch: u32,
pub h_sync_pulse: u32,
pub v_front_porch: u32,
pub v_sync_pulse: u32,
pub h_back_porch: u32,
pub v_back_porch: u32,
pub h_sync_positive: bool,
pub v_sync_positive: bool,
pub aspect_ratio: String,
}
impl DetailedTiming {
pub fn parse(block: &[u8]) -> Self {
let pixel_clock_10khz = u16::from_le_bytes([block[0], block[1]]);
let pixel_clock_hz = pixel_clock_10khz as u64 * 10_000;
let h_active_low = block[2] as u32;
let h_blanking_low = block[3] as u32;
let h_active_high = ((block[4] >> 4) as u32) << 8;
let h_blanking_high = ((block[4] & 0x0F) as u32) << 8;
let h_active = h_active_high | h_active_low;
let h_blanking = h_blanking_high | h_blanking_low;
let v_active_low = block[5] as u32;
let v_blanking_low = block[6] as u32;
let v_active_high = ((block[7] >> 4) as u32) << 8;
let v_blanking_high = ((block[7] & 0x0F) as u32) << 8;
let v_active = v_active_high | v_active_low;
let v_blanking = v_blanking_high | v_blanking_low;
let h_sync_offset_low = (block[8] >> 4) as u32;
let h_sync_pulse_low = (block[8] & 0x0F) as u32;
let v_sync_offset_low = (block[9] >> 4) as u32;
let v_sync_pulse_low = (block[9] & 0x0F) as u32;
let h_sync_offset_high = ((block[11] >> 2) & 0x03) as u32;
let h_sync_pulse_high = (block[11] & 0x03) as u32;
let v_sync_offset_high = ((block[11] >> 6) & 0x03) as u32;
let v_sync_pulse_high = ((block[11] >> 4) & 0x03) as u32;
let h_front_porch = (h_sync_offset_high << 4) | h_sync_offset_low;
let h_sync_pulse = (h_sync_pulse_high << 4) | h_sync_pulse_low;
let v_front_porch = (v_sync_offset_high << 4) | v_sync_offset_low;
let v_sync_pulse = (v_sync_pulse_high << 4) | v_sync_pulse_low;
let h_back_porch = h_blanking.saturating_sub(h_front_porch + h_sync_pulse);
let v_back_porch = v_blanking.saturating_sub(v_front_porch + v_sync_pulse);
let h_sync_positive = (block[17] & 0x02) != 0;
let v_sync_positive = (block[17] & 0x04) != 0;
let aspect_ratio = calc_aspect_ratio(h_active, v_active);
Self {
pixel_clock_hz,
h_active,
h_blanking,
v_active,
v_blanking,
h_front_porch,
h_sync_pulse,
v_front_porch,
v_sync_pulse,
h_back_porch,
v_back_porch,
h_sync_positive,
v_sync_positive,
aspect_ratio,
}
}
}
fn calc_aspect_ratio(width: u32, height: u32) -> String {
if width == 0 || height == 0 {
return "??:??".to_string();
}
let ratio = width as f64 / height as f64;
if (ratio - 2.3333).abs() < 0.05 {
"21:9".to_string()
} else if (ratio - 1.7777).abs() < 0.05 {
"16:9".to_string()
} else if (ratio - 1.6).abs() < 0.05 {
"16:10".to_string()
} else if (ratio - 1.5).abs() < 0.05 {
"3:2".to_string()
} else if (ratio - 1.3333).abs() < 0.05 {
"4:3".to_string()
} else if (ratio - 1.25).abs() < 0.05 {
"5:4".to_string()
} else {
format!("{ratio:.2}:1")
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct RangeLimits {
pub min_v_freq_hz: u8,
pub max_v_freq_hz: u8,
pub min_h_freq_khz: u8,
pub max_h_freq_khz: u8,
pub max_pixel_clock_mhz: u16,
}
impl RangeLimits {
pub fn parse(block: &[u8]) -> Self {
Self {
min_v_freq_hz: block[5],
max_v_freq_hz: block[6],
min_h_freq_khz: block[7],
max_h_freq_khz: block[8],
max_pixel_clock_mhz: block[9] as u16 * 10,
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum VideoInputParams {
Digital(VideoInputParamsDigital),
Analog(VideoInputParamsAnalog),
}
impl VideoInputParams {
pub fn new(data: &[u8]) -> Self {
let d = data[20];
let bit_depth = ((d >> 7) & 0b00000111) as u8;
if bit_depth == 1 {
Self::Digital(VideoInputParamsDigital::new(data))
} else if bit_depth == 0 {
Self::Analog(VideoInputParamsAnalog::new(data))
} else {
panic!("Unknown 7 bit of 20 byte ({bit_depth})!")
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct VideoInputParamsDigital {
pub bit_depth: BitDepth,
pub video_interface: VideoInterface,
}
impl VideoInputParamsDigital {
pub fn new(data: &[u8]) -> Self {
let d = data[20];
let bit_depth = BitDepth::from(((d >> 4) & 0b00000111) as u8);
let video_interface = VideoInterface::from((d & 0b00000111) as u8);
Self {
bit_depth,
video_interface,
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum BitDepth {
Undefined,
B6,
B8,
B10,
B12,
B14,
B16,
Reserved,
Unknown(u8),
}
impl From<u8> for BitDepth {
fn from(value: u8) -> Self {
match value {
0b000 => Self::Undefined,
0b001 => Self::B6,
0b010 => Self::B8,
0b011 => Self::B10,
0b100 => Self::B12,
0b101 => Self::B14,
0b110 => Self::B16,
0b111 => Self::Reserved,
_ => Self::Unknown(value),
}
}
}
impl Display for BitDepth {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Undefined => "Undefined".to_string(),
Self::B6 => "6 bits".to_string(),
Self::B8 => "8 bits".to_string(),
Self::B10 => "10 bits".to_string(),
Self::B12 => "12 bits".to_string(),
Self::B14 => "14 bits".to_string(),
Self::B16 => "16 bits".to_string(),
Self::Reserved => "Reserved value".to_string(),
Self::Unknown(val) => format!("Unknown ({val})"),
}
)
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum VideoInterface {
Undefined,
DVI,
HDMIa,
HDMIb,
MDDI,
DisplayPort,
Unknown(u8),
}
impl From<u8> for VideoInterface {
fn from(value: u8) -> Self {
match value {
0b0000 => Self::Undefined,
0b0001 => Self::DVI,
0b0010 => Self::HDMIa,
0b0011 => Self::HDMIb,
0b0100 => Self::MDDI,
0b0101 => Self::DisplayPort,
_ => Self::Unknown(value),
}
}
}
impl Display for VideoInterface {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Undefined => "Undefined".to_string(),
Self::DVI => "DVI".to_string(),
Self::HDMIa => "HDMI-a".to_string(),
Self::HDMIb => "HDMI-b".to_string(),
Self::MDDI => "MDDI".to_string(),
Self::DisplayPort => "Display Port".to_string(),
Self::Unknown(val) => format!("Unknown (code: {val})"),
}
)
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct VideoInputParamsAnalog {
pub white_sync_levels: u8,
pub blank_to_black_setup: u8,
pub separate_sync_supported: u8,
pub composite_sync_supported: u8,
pub sync_on_green_supported: u8,
pub sync_on_green_isused: u8,
}
impl VideoInputParamsAnalog {
pub fn new(data: &[u8]) -> Self {
let d = data[20];
let white_sync_levels = ((d >> 5) & 0b00000011) as u8;
let blank_to_black_setup = (d >> 4) as u8;
let separate_sync_supported = (d >> 3) as u8;
let composite_sync_supported = (d >> 2) as u8;
let sync_on_green_supported = (d >> 1) as u8;
let sync_on_green_isused = (d >> 0) as u8;
Self {
white_sync_levels,
blank_to_black_setup,
separate_sync_supported,
composite_sync_supported,
sync_on_green_supported,
sync_on_green_isused,
}
}
}