Skip to main content

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                    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/// Information about selected display
127#[derive(Debug, Serialize, Deserialize, Clone)]
128pub struct DRM {
129    /// Is enabled
130    pub enabled: bool,
131
132    /// Data from EDID
133    pub edid: Option<EDID>,
134
135    /// Supported modes of this screen (in HxV format)
136    pub modes: Vec<String>,
137}
138
139impl DRM {
140    pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
141        let path = path.as_ref();
142        let enabled = {
143            let txt = read_to_string(path.join("enabled"));
144            match txt {
145                Ok(txt) => {
146                    let contents = txt.trim();
147                    if contents == "enabled" { true } else { false }
148                }
149                Err(_) => false,
150            }
151        };
152        let modes = read_to_string(path.join("modes"))?
153            .lines()
154            .map(|s| s.to_string())
155            .collect::<Vec<_>>();
156        let edid = EDID::new(path);
157
158        Ok(Self {
159            enabled,
160            edid: match edid {
161                Ok(edid) => Some(edid),
162                Err(why) => {
163                    // может быть, просто вываливать ошибку если не смогли прочитать EDID?
164                    if enabled {
165                        return Err(why);
166                    } else {
167                        None
168                    }
169                }
170            },
171            modes,
172        })
173    }
174
175    pub fn is_empty_info(&self) -> bool {
176        !self.enabled && self.edid.is_none() && self.modes.is_empty()
177    }
178}
179
180/// Information from `edid` file (EDID v1.4 only supported yet)
181///
182/// Read [Wikipedia](https://en.wikipedia.org/wiki/Extended_Display_Identification_Data) for details.
183#[derive(Debug, Serialize, Deserialize, Clone)]
184pub struct EDID {
185    //  NAME          TYPE       BYTES
186    /// Manufacturer ID. This is a legacy Plug and Play ID assigned
187    /// by UEFI forum which is a *big-endian* 16-bit value made up
188    /// of three 5-bit letters: 00001 - 'A', 00010 - 'B', etc.
189    pub manufacturer: String, // 8-9
190
191    /// Manufacturer product code. 16-bit hex-nubmer, little-endian.
192    /// For example, "LGC" + "C0CF"
193    pub product_code: u16, // 10-11
194
195    /// Serial number. 32 bits, little-endian
196    pub serial_number: u32, // 12-15
197
198    /// Week of manufacture; or `FF` model year flag
199    ///
200    /// > **NOTE:** week numbering isn't consistent between
201    /// > manufacturers
202    pub week: u8, // 16
203
204    /// Year of manufacture, or year of model, if model year flag
205    /// is set
206    pub year: u16, // 17
207
208    /// EDID version, usually `01` for 1.3 and 1.4
209    pub edid_version: u8, // 18
210
211    /// EDID revision, usually `03` for 1.3 or `04` for 1.4
212    pub edid_revision: u8, // 19
213
214    /// Video input parameters
215    pub video_input: VideoInputParams, // 20
216
217    /// Horizontal screen size, in centimetres (range 1-255)
218    pub hscreen_size: u8, // 21
219
220    /// Vertical screen size, in centimetres
221    pub vscreen_size: u8, // 22
222
223    /// Display gamma, factory default
224    pub display_gamma: u8, // 23
225}
226
227impl EDID {
228    pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
229        let data = read(path.as_ref().join("edid"))?;
230        if data.len() < 128 || data[0..8] != [0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00] {
231            return Err(anyhow!(
232                "Invalid EDID header on path {}",
233                path.as_ref().display(),
234            ));
235        }
236
237        let manufacturer = {
238            let word = ((data[8] as u16) << 8) | data[9] as u16;
239
240            let c1 = ((word >> 10) & 0x1F) as u8 + 64;
241            let c2 = ((word >> 5) & 0x1f) as u8 + 64;
242            let c3 = (word & 0x1f) as u8 + 64;
243
244            format!("{}{}{}", c1 as char, c2 as char, c3 as char)
245        };
246        let product_code = u16::from_le_bytes([data[10], data[11]]);
247        let serial_number = u32::from_le_bytes([data[12], data[13], data[14], data[15]]);
248        let week = data[16];
249        let year = data[17] as u16 + 1990;
250        let edid_version = data[18];
251        let edid_revision = data[19];
252        let video_input = VideoInputParams::new(&data);
253        let hscreen_size = data[21];
254        let vscreen_size = data[22];
255        let display_gamma = data[23];
256
257        Ok(Self {
258            manufacturer,
259            product_code,
260            serial_number,
261            week,
262            year,
263            edid_version,
264            edid_revision,
265            video_input,
266            hscreen_size,
267            vscreen_size,
268            display_gamma,
269        })
270    }
271}
272
273/// Video input parameters bitmap
274#[derive(Debug, Serialize, Deserialize, Clone)]
275pub enum VideoInputParams {
276    Digital(VideoInputParamsDigital),
277    Analog(VideoInputParamsAnalog),
278}
279
280impl VideoInputParams {
281    pub fn new(data: &[u8]) -> Self {
282        let d = data[20];
283        let bit_depth = ((d >> 7) & 0b00000111) as u8;
284        if bit_depth == 1 {
285            Self::Digital(VideoInputParamsDigital::new(data))
286        } else if bit_depth == 0 {
287            Self::Analog(VideoInputParamsAnalog::new(data))
288        } else {
289            panic!("Unknown 7 bit of 20 byte ({bit_depth})!")
290        }
291    }
292}
293
294/// Digital input
295#[derive(Debug, Serialize, Deserialize, Clone)]
296pub struct VideoInputParamsDigital {
297    /// Bit depth
298    pub bit_depth: BitDepth,
299
300    /// Video interface type
301    pub video_interface: VideoInterface,
302}
303
304impl VideoInputParamsDigital {
305    pub fn new(data: &[u8]) -> Self {
306        let d = data[20];
307        let bit_depth = BitDepth::from(((d >> 4) & 0b00000111) as u8);
308        let video_interface = VideoInterface::from((d & 0b00000111) as u8);
309
310        Self {
311            bit_depth,
312            video_interface,
313        }
314    }
315}
316
317/// Bit depth
318#[derive(Debug, Serialize, Deserialize, Clone)]
319pub enum BitDepth {
320    Undefined,
321
322    /// 6 bits per color
323    B6,
324
325    /// 8 bits per color
326    B8,
327
328    /// 10 bits per color
329    B10,
330
331    /// 12 bits per color
332    B12,
333
334    /// 14 bits per color
335    B14,
336
337    /// 16 bits per color
338    B16,
339
340    /// Reserved value
341    Reserved,
342
343    /// Unknown value (while EDID parsing)
344    Unknown(u8),
345}
346
347impl From<u8> for BitDepth {
348    fn from(value: u8) -> Self {
349        match value {
350            0b000 => Self::Undefined,
351            0b001 => Self::B6,
352            0b010 => Self::B8,
353            0b011 => Self::B10,
354            0b100 => Self::B12,
355            0b101 => Self::B14,
356            0b110 => Self::B16,
357            0b111 => Self::Reserved,
358            _ => Self::Unknown(value),
359        }
360    }
361}
362
363impl Display for BitDepth {
364    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
365        write!(
366            f,
367            "{}",
368            match self {
369                Self::Undefined => "Undefined".to_string(),
370                Self::B6 => "6 bits".to_string(),
371                Self::B8 => "8 bits".to_string(),
372                Self::B10 => "10 bits".to_string(),
373                Self::B12 => "12 bits".to_string(),
374                Self::B14 => "14 bits".to_string(),
375                Self::B16 => "16 bits".to_string(),
376                Self::Reserved => "Reserved value".to_string(),
377                Self::Unknown(val) => format!("Unknown ({val})"),
378            }
379        )
380    }
381}
382
383/// Video interface (EDID data may be incorrect)
384#[derive(Debug, Serialize, Deserialize, Clone)]
385pub enum VideoInterface {
386    Undefined,
387    DVI,
388    HDMIa,
389    HDMIb,
390    MDDI,
391    DisplayPort,
392    Unknown(u8),
393}
394
395impl From<u8> for VideoInterface {
396    fn from(value: u8) -> Self {
397        match value {
398            0b0000 => Self::Undefined,
399            0b0001 => Self::DVI,
400            0b0010 => Self::HDMIa,
401            0b0011 => Self::HDMIb,
402            0b0100 => Self::MDDI,
403            0b0101 => Self::DisplayPort,
404            _ => Self::Unknown(value),
405        }
406    }
407}
408
409impl Display for VideoInterface {
410    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
411        write!(
412            f,
413            "{}",
414            match self {
415                Self::Undefined => "Undefined".to_string(),
416                Self::DVI => "DVI".to_string(),
417                Self::HDMIa => "HDMI-a".to_string(),
418                Self::HDMIb => "HDMI-b".to_string(),
419                Self::MDDI => "MDDI".to_string(),
420                Self::DisplayPort => "Display Port".to_string(),
421                Self::Unknown(val) => format!("Unknown (code: {val})"),
422            }
423        )
424    }
425}
426
427#[derive(Debug, Serialize, Deserialize, Clone)]
428pub struct VideoInputParamsAnalog {
429    /// Video white and sync levels, relative to blank:
430    ///
431    /// | Binary value | Data    |
432    /// |--------------|---------|
433    /// | `00` | +0.7/-0.3 V     |
434    /// | `01` | +0.714/-0.286 V |
435    /// | `10` | +1.0/-0.4 V     |
436    /// | `11` | +0.7/0 V (EVC)  |
437    pub white_sync_levels: u8,
438
439    /// Blank-to-black setyp (pedestal) expected
440    pub blank_to_black_setup: u8,
441
442    /// Separate sync supported
443    pub separate_sync_supported: u8,
444
445    /// Composite sync supported
446    pub composite_sync_supported: u8,
447
448    /// Sync on green supported
449    pub sync_on_green_supported: u8,
450
451    /// VSync pulse must be serrated when composite or sync-on-green
452    /// is used
453    pub sync_on_green_isused: u8,
454}
455
456impl VideoInputParamsAnalog {
457    /// NOTE: THIS FUNCTION MAY BE INCORRECT
458    pub fn new(data: &[u8]) -> Self {
459        let d = data[20];
460        let white_sync_levels = ((d >> 5) & 0b00000011) as u8;
461        let blank_to_black_setup = (d >> 4) as u8;
462        let separate_sync_supported = (d >> 3) as u8;
463        let composite_sync_supported = (d >> 2) as u8;
464        let sync_on_green_supported = (d >> 1) as u8;
465        let sync_on_green_isused = (d >> 0) as u8; // WARN: may be incorrect
466
467        Self {
468            white_sync_levels,
469            blank_to_black_setup,
470            separate_sync_supported,
471            composite_sync_supported,
472            sync_on_green_supported,
473            sync_on_green_isused,
474        }
475    }
476}