ferrix_lib/drm.rs
1/* drm.rs
2 *
3 * Copyright 2025 Michail Krasnov <mskrasnov07@ya.ru>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 *
18 * SPDX-License-Identifier: GPL-3.0-or-later
19 */
20
21//! Get information about video
22//!
23//! ## Example
24//! ```no-test
25//! use ferrix_lib::drm::Video;
26//! use ferrix_lib::traits::ToJson;
27//!
28//! let video = Video::new().unwrap();
29//! for dev in &video.devices {
30//! dbg!(dev);
31//! }
32//! let json = video.to_json().unwrap();
33//! dbg!(json);
34//! ```
35//!
36//! ## EDID structure, version 1.4
37//!
38//! <small>From <a href="https://en.wikipedia.org/wiki/Extended_Display_Identification_Data">WikiPedia</a></small>
39//!
40//! | Bytes | Description |
41//! |-------|-------------|
42//! | 0-7 | Fixed header pattern `00 FF FF FF FF FF FF 00` |
43//! | 8-9 | Manufacturer ID. "IBM", "PHL" |
44//! | 10-11 | Manufacturer product code. 16-bit hex number, little endian. "PHL" + "C0CF" |
45//! | 12-15 | Serial number. 32 bits, little-endian |
46//! | 16 | Week of manufacture; or `FF` model year flag |
47//! | 17 | Year of manufacture, or year or model, if model year flag is set. Year = datavalue + 1990 |
48//! | 18 | EDID version, usually `01` (for 1.3 and 1.4) |
49//! | 19 | EDID revision, usually `03` (for 1.3) or `04` (for 1.4) |
50//! | 20 | Video input parameters bitmap |
51//! | 21 | Horizontal screen size, in cm (range 1-255). If vertical screen size is 0, landscape aspect ratio (range 1.00-3.54), datavalue = (ARx100) - 99 (example: 16:9, 79; 4:3, 34.) |
52//! | 22 | Vertical screen size, in cm |
53//! | 23 | Display gamma, factory default (range 1.00 - 3.54), datavalue = (gamma x 100) - 100 = (gamma - 1) x 100. If 255, gamma is defined by DI-EXT block |
54//! | 24 | Supported features bitmap |
55//! | ... | ... |
56//!
57//! **EDID Detailed Timing Descriptor** (TODO)
58//!
59//! | Bytes | Description |
60//! |-------|-----------------------------------------------------|
61//! | 0-1 | Pixel clock. `00` - reserved; otherwise in 10 kHz units (0.01 - 655.35 MHz, little-endian) |
62//! | 2 | Horizontal active pixels 8 lsbits (0-255) |
63//! | 3 | Horizontal blanking pixels 8 lsbits (0-255) |
64//! | 4 | ... |
65//! | 5 | Vertical active lines 8 lsbits (0-255) |
66//! | 6 | Vertical blanking lines 8 lsbits (0-255) |
67//! | 7 | ... |
68//! | 8 | Horizontal front porch (sync offset) pixels 8 lsbits (0-255) from blanking start |
69//! | 9 | Horizontal sync pulse width pixels 8 lsbits (0-255) |
70//! | 10 | ... |
71//! | 11 | ... |
72//! | 12 | Horizontal image size, mm, 8 lsbits (0-255 mm, 161 in) |
73//! | 13 | Vertical image size, mm, ... |
74//! | ... | ... |
75
76use 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/// Information about video devices
86#[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(); // {prefix}/{card_i}/{card_i}-*
108 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 // println!("Read drm data: {} ({fname})", d.display());
114 devices.push(DRM::new(d)?);
115 }
116 }
117 }
118 Ok(Self { devices })
119 }
120}
121
122impl ToJson for Video {}
123
124/// Information about selected display
125#[derive(Debug, Serialize, Deserialize, Clone)]
126pub struct DRM {
127 /// Is enabled
128 pub enabled: bool,
129
130 /// Data from EDID
131 pub edid: Option<EDID>,
132
133 /// Supported modes of this screen (in HxV format)
134 pub modes: Vec<String>,
135}
136
137impl DRM {
138 pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
139 let path = path.as_ref();
140 let enabled = {
141 let txt = read_to_string(path.join("enabled"));
142 match txt {
143 Ok(txt) => {
144 let contents = txt.trim();
145 if contents == "enabled" { true } else { false }
146 }
147 Err(_) => false,
148 }
149 };
150 let modes = read_to_string(path.join("modes"))?
151 .lines()
152 .map(|s| s.to_string())
153 .collect::<Vec<_>>();
154 let edid = EDID::new(path);
155
156 Ok(Self {
157 enabled,
158 edid: match edid {
159 Ok(edid) => Some(edid),
160 Err(why) => {
161 // может быть, просто вываливать ошибку если не смогли прочитать EDID?
162 if enabled {
163 return Err(why);
164 } else {
165 None
166 }
167 }
168 },
169 modes,
170 })
171 }
172}
173
174/// Information from `edid` file (EDID v1.4 only supported yet)
175///
176/// Read [Wikipedia](https://en.wikipedia.org/wiki/Extended_Display_Identification_Data) for details.
177#[derive(Debug, Serialize, Deserialize, Clone)]
178pub struct EDID {
179 // NAME TYPE BYTES
180 /// Manufacturer ID. This is a legacy Plug and Play ID assigned
181 /// by UEFI forum which is a *big-endian* 16-bit value made up
182 /// of three 5-bit letters: 00001 - 'A', 00010 - 'B', etc.
183 pub manufacturer: String, // 8-9
184
185 /// Manufacturer product code. 16-bit hex-nubmer, little-endian.
186 /// For example, "LGC" + "C0CF"
187 pub product_code: u16, // 10-11
188
189 /// Serial number. 32 bits, little-endian
190 pub serial_number: u32, // 12-15
191
192 /// Week of manufacture; or `FF` model year flag
193 ///
194 /// > **NOTE:** week numbering isn't consistent between
195 /// > manufacturers
196 pub week: u8, // 16
197
198 /// Year of manufacture, or year of model, if model year flag
199 /// is set
200 pub year: u16, // 17
201
202 /// EDID version, usually `01` for 1.3 and 1.4
203 pub edid_version: u8, // 18
204
205 /// EDID revision, usually `03` for 1.3 or `04` for 1.4
206 pub edid_revision: u8, // 19
207
208 /// Video input parameters
209 pub video_input: VideoInputParams, // 20
210
211 /// Horizontal screen size, in centimetres (range 1-255)
212 pub hscreen_size: u8, // 21
213
214 /// Vertical screen size, in centimetres
215 pub vscreen_size: u8, // 22
216
217 /// Display gamma, factory default
218 pub display_gamma: u8, // 23
219}
220
221impl EDID {
222 pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
223 let data = read(path.as_ref().join("edid"))?;
224 if data.len() < 128 || data[0..8] != [0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00] {
225 return Err(anyhow!(
226 "Invalid EDID header on path {}",
227 path.as_ref().display(),
228 ));
229 }
230
231 let manufacturer = {
232 let word = ((data[8] as u16) << 8) | data[9] as u16;
233
234 let c1 = ((word >> 10) & 0x1F) as u8 + 64;
235 let c2 = ((word >> 5) & 0x1f) as u8 + 64;
236 let c3 = (word & 0x1f) as u8 + 64;
237
238 format!("{}{}{}", c1 as char, c2 as char, c3 as char)
239 };
240 let product_code = u16::from_le_bytes([data[10], data[11]]);
241 let serial_number = u32::from_le_bytes([data[12], data[13], data[14], data[15]]);
242 let week = data[16];
243 let year = data[17] as u16 + 1990;
244 let edid_version = data[18];
245 let edid_revision = data[19];
246 let video_input = VideoInputParams::new(&data);
247 let hscreen_size = data[21];
248 let vscreen_size = data[22];
249 let display_gamma = data[23];
250
251 Ok(Self {
252 manufacturer,
253 product_code,
254 serial_number,
255 week,
256 year,
257 edid_version,
258 edid_revision,
259 video_input,
260 hscreen_size,
261 vscreen_size,
262 display_gamma,
263 })
264 }
265}
266
267/// Video input parameters bitmap
268#[derive(Debug, Serialize, Deserialize, Clone)]
269pub enum VideoInputParams {
270 Digital(VideoInputParamsDigital),
271 Analog(VideoInputParamsAnalog),
272}
273
274impl VideoInputParams {
275 pub fn new(data: &[u8]) -> Self {
276 let d = data[20];
277 let bit_depth = ((d >> 7) & 0b00000111) as u8;
278 if bit_depth == 1 {
279 Self::Digital(VideoInputParamsDigital::new(data))
280 } else if bit_depth == 0 {
281 Self::Analog(VideoInputParamsAnalog::new(data))
282 } else {
283 panic!("Unknown 7 bit of 20 byte ({bit_depth})!")
284 }
285 }
286}
287
288/// Digital input
289#[derive(Debug, Serialize, Deserialize, Clone)]
290pub struct VideoInputParamsDigital {
291 /// Bit depth
292 pub bit_depth: BitDepth,
293
294 /// Video interface type
295 pub video_interface: VideoInterface,
296}
297
298impl VideoInputParamsDigital {
299 pub fn new(data: &[u8]) -> Self {
300 let d = data[20];
301 let bit_depth = BitDepth::from(((d >> 4) & 0b00000111) as u8);
302 let video_interface = VideoInterface::from((d & 0b00000111) as u8);
303
304 Self {
305 bit_depth,
306 video_interface,
307 }
308 }
309}
310
311/// Bit depth
312#[derive(Debug, Serialize, Deserialize, Clone)]
313pub enum BitDepth {
314 Undefined,
315
316 /// 6 bits per color
317 B6,
318
319 /// 8 bits per color
320 B8,
321
322 /// 10 bits per color
323 B10,
324
325 /// 12 bits per color
326 B12,
327
328 /// 14 bits per color
329 B14,
330
331 /// 16 bits per color
332 B16,
333
334 /// Reserved value
335 Reserved,
336
337 /// Unknown value (while EDID parsing)
338 Unknown(u8),
339}
340
341impl From<u8> for BitDepth {
342 fn from(value: u8) -> Self {
343 match value {
344 0b000 => Self::Undefined,
345 0b001 => Self::B6,
346 0b010 => Self::B8,
347 0b011 => Self::B10,
348 0b100 => Self::B12,
349 0b101 => Self::B14,
350 0b110 => Self::B16,
351 0b111 => Self::Reserved,
352 _ => Self::Unknown(value),
353 }
354 }
355}
356
357impl Display for BitDepth {
358 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
359 write!(
360 f,
361 "{}",
362 match self {
363 Self::Undefined => "Undefined".to_string(),
364 Self::B6 => "6 bits".to_string(),
365 Self::B8 => "8 bits".to_string(),
366 Self::B10 => "10 bits".to_string(),
367 Self::B12 => "12 bits".to_string(),
368 Self::B14 => "14 bits".to_string(),
369 Self::B16 => "16 bits".to_string(),
370 Self::Reserved => "Reserved value".to_string(),
371 Self::Unknown(val) => format!("Unknown ({val})"),
372 }
373 )
374 }
375}
376
377/// Video interface (EDID data may be incorrect)
378#[derive(Debug, Serialize, Deserialize, Clone)]
379pub enum VideoInterface {
380 Undefined,
381 DVI,
382 HDMIa,
383 HDMIb,
384 MDDI,
385 DisplayPort,
386 Unknown(u8),
387}
388
389impl From<u8> for VideoInterface {
390 fn from(value: u8) -> Self {
391 match value {
392 0b0000 => Self::Undefined,
393 0b0001 => Self::DVI,
394 0b0010 => Self::HDMIa,
395 0b0011 => Self::HDMIb,
396 0b0100 => Self::MDDI,
397 0b0101 => Self::DisplayPort,
398 _ => Self::Unknown(value),
399 }
400 }
401}
402
403impl Display for VideoInterface {
404 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
405 write!(
406 f,
407 "{}",
408 match self {
409 Self::Undefined => "Undefined".to_string(),
410 Self::DVI => "DVI".to_string(),
411 Self::HDMIa => "HDMI-a".to_string(),
412 Self::HDMIb => "HDMI-b".to_string(),
413 Self::MDDI => "MDDI".to_string(),
414 Self::DisplayPort => "Display Port".to_string(),
415 Self::Unknown(val) => format!("Unknown (code: {val})"),
416 }
417 )
418 }
419}
420
421#[derive(Debug, Serialize, Deserialize, Clone)]
422pub struct VideoInputParamsAnalog {
423 /// Video white and sync levels, relative to blank:
424 ///
425 /// | Binary value | Data |
426 /// |--------------|---------|
427 /// | `00` | +0.7/-0.3 V |
428 /// | `01` | +0.714/-0.286 V |
429 /// | `10` | +1.0/-0.4 V |
430 /// | `11` | +0.7/0 V (EVC) |
431 pub white_sync_levels: u8,
432
433 /// Blank-to-black setyp (pedestal) expected
434 pub blank_to_black_setup: u8,
435
436 /// Separate sync supported
437 pub separate_sync_supported: u8,
438
439 /// Composite sync supported
440 pub composite_sync_supported: u8,
441
442 /// Sync on green supported
443 pub sync_on_green_supported: u8,
444
445 /// VSync pulse must be serrated when composite or sync-on-green
446 /// is used
447 pub sync_on_green_isused: u8,
448}
449
450impl VideoInputParamsAnalog {
451 /// NOTE: THIS FUNCTION MAY BE INCORRECT
452 pub fn new(data: &[u8]) -> Self {
453 let d = data[20];
454 let white_sync_levels = ((d >> 5) & 0b00000011) as u8;
455 let blank_to_black_setup = (d >> 4) as u8;
456 let separate_sync_supported = (d >> 3) as u8;
457 let composite_sync_supported = (d >> 2) as u8;
458 let sync_on_green_supported = (d >> 1) as u8;
459 let sync_on_green_isused = (d >> 0) as u8; // WARN: may be incorrect
460
461 Self {
462 white_sync_levels,
463 blank_to_black_setup,
464 separate_sync_supported,
465 composite_sync_supported,
466 sync_on_green_supported,
467 sync_on_green_isused,
468 }
469 }
470}