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    /// DRM name
130    pub name: Option<String>,
131
132    /// Is enabled
133    pub enabled: bool,
134
135    /// Data from EDID
136    pub edid: Option<EDID>,
137
138    /// Supported modes of this screen (in HxV format)
139    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                    // может быть, просто вываливать ошибку если не смогли прочитать EDID?
172                    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/// Information from `edid` file (EDID v1.4 only supported yet)
189///
190/// Read [Wikipedia](https://en.wikipedia.org/wiki/Extended_Display_Identification_Data) for details.
191#[derive(Debug, Serialize, Deserialize, Clone)]
192pub struct EDID {
193    /// Raw EDID bytes (at least 128 bytes)
194    pub raw: Vec<u8>,
195
196    /// Manufacturer ID
197    ///
198    /// This is a legacy Plug and Play ID assigned by the UEFI forum, which is a big-endian
199    /// 16-bit value made up three 5-bit letters: 00001 = 'A', 00010 = 'B', etc.
200    ///
201    /// > Byte offset: 8-9
202    pub manufacturer: String,
203
204    /// General alphanumeric description of the display. Extracted from the Monitor Descriptor
205    /// block with type `0xFE`
206    pub description: Option<String>,
207
208    /// Manufacturer product code. 16-bit hex-nubmer, little-endian.
209    /// For example, "LGC" + "C0CF"
210    ///
211    /// > Byte offset: 10-11
212    pub product_code: u16,
213
214    /// Serial number. 32 bits, little-endian
215    ///
216    /// Note: This may be zero in the serial number is provided as a text string instead.
217    ///
218    /// > Byte offset: 12-15
219    pub serial_number: u32,
220
221    /// Display product name/model
222    ///
223    /// Extracted from the Monitor Descriptor block with type `0xFC`
224    pub model: String,
225
226    /// Text serial number
227    ///
228    /// Extracted from the Monitor Descriptor block with type `0xFF`
229    pub serial: Option<String>,
230
231    /// Week of manufacture; or `FF` model year flag
232    ///
233    /// > **WARN:** week numbering isn't consistent between manufacturers.
234    ///
235    /// > Byte offset: 16
236    pub week: u8,
237
238    /// Year of manufacture, or year of model, if model year flag is set
239    ///
240    /// > Byte offset: 17
241    pub year: u16,
242
243    /// EDID version, usually `01` for 1.3 and 1.4
244    ///
245    /// > Byte offset: 18
246    pub edid_version: u8,
247
248    /// EDID revision, usually `03` for 1.3 or `04` for 1.4
249    ///
250    /// > Byte offset: 19
251    pub edid_revision: u8,
252
253    /// Video input parameters (signal type, voltage levels, etc.)
254    ///
255    /// > Byte offset: 20
256    pub video_input: VideoInputParams,
257
258    /// Horizontal screen size, in centimetres (range 1-255). A value of `0` indicates the size
259    /// is not specified
260    ///
261    /// > Byte offset: 21
262    pub hscreen_size: u8,
263
264    /// Vertical screen size, in centimetres. A value of `0` indicates the size is not specified
265    ///
266    /// > Byte offset: 22
267    pub vscreen_size: u8,
268
269    /// Calculated screen diagonal in inches, derived from `hscreen_size` and `vscreen_size`
270    ///
271    /// `None` if either dimension is `0`.
272    pub diagonal_inches: Option<f32>,
273
274    /// Calculated aspect ratio of the preferred timing mode (e.g. "16:10", "4:3", etc.
275    /// or "1.60:1")
276    pub aspect_ratio: Option<String>,
277
278    /// Active horizontal resolution in pixels. Derived from the first Detailed Timing Descriptor
279    /// (preferred timing)
280    pub resolution_width: Option<u32>,
281
282    /// Active vertical resolution in pixels. Derived from the first DTD (preferred timing)
283    pub resolution_height: Option<u32>,
284
285    /// Pixel clock frequency in MHz. Derived from the first DTD (preferred timing)
286    pub pixel_clock_mhz: Option<f32>,
287
288    /// Display gamma, factory default
289    ///
290    /// Formula: `(value + 100) / 100`
291    ///
292    /// > Byte offset: 23
293    pub display_gamma: u8,
294
295    /// List of all parsed DTDs. The first entry is typically the "Preferred Timing" (native
296    /// resolution)
297    pub detailed_timings: Vec<DetailedTiming>,
298
299    /// Supported vertical/horizontal frequency ranges and maximum pixel clock. Extracted from the
300    /// Monitor Range Limits Descriptor (type `0xFD`)
301    pub range_limits: Option<RangeLimits>,
302
303    /// Number of 128-byte extension blocks following this base block
304    ///
305    /// > Byte offset: 126
306    pub extension_blocks: u8,
307
308    /// Checksum of the base block. The sum of all 128 bytes should be `0` modulo `256`
309    ///
310    /// > Byte offset: 127
311    pub checksum: u8,
312}
313
314impl EDID {
315    /// Parses an EDID structure from a `edid` file located in the given directory path
316    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/// Single Detailed Timing Descriptor (DTD) block
439///
440/// A DTD contains precise timing information for a specific display mode, including active
441/// resolution, banking intervals, sync pulses, and polarity.
442#[derive(Debug, Serialize, Deserialize, Clone)]
443pub struct DetailedTiming {
444    /// Pixel clock frequency in Hz
445    pub pixel_clock_hz: u64,
446
447    /// Active horizontal resolution in pixels
448    pub h_active: u32,
449
450    /// Total horizontal blanking interval in pixels
451    pub h_blanking: u32,
452
453    /// Active vertical resolution in lines
454    pub v_active: u32,
455
456    /// Total vertical blanking interval in lines
457    pub v_blanking: u32,
458
459    /// Horizontal front porch (sync offset) in px
460    pub h_front_porch: u32,
461
462    /// Horizontal sync pulse width in px
463    pub h_sync_pulse: u32,
464
465    /// Vertical front porch (sync offset) in lines
466    pub v_front_porch: u32,
467
468    /// Vertical sync pulse width in lines
469    pub v_sync_pulse: u32,
470
471    /// Horizontal back porch in pixels
472    pub h_back_porch: u32,
473
474    /// Vertical back porch in lines
475    pub v_back_porch: u32,
476
477    /// `true` if the horizontal sync pulse is positive polarity
478    pub h_sync_positive: bool,
479
480    /// `true` if the vertical sync pulse is positive polarity
481    pub v_sync_positive: bool,
482
483    /// Calculated aspect ratio for this specific timing mode
484    pub aspect_ratio: String,
485}
486
487impl DetailedTiming {
488    /// Parses an 18-byte DTD block into a structured format
489    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        /* Horizontal parameters */
494        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        /* Vertical parameters */
503        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        /* Sync */
512        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        /* Back porch */
528        // blanking - front_porch - sync_pulse
529        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/// Monitor Range Limits Descriptor (type `0xFD`)
581///
582/// Defines the operational limits of the display to help the graphics driver select
583/// valid timing modes withoyt reading the EDID string
584#[derive(Debug, Serialize, Deserialize, Clone)]
585pub struct RangeLimits {
586    /// Minimum vertical field rate (refresh rate) in Hz
587    pub min_v_freq_hz: u8,
588
589    /// Maximum vertical field rate (refresh rate) in Hz
590    pub max_v_freq_hz: u8,
591
592    /// Minimum horizontal line rate in kHz
593    pub min_h_freq_khz: u8,
594
595    /// Maximum horizontal line rate in kHz
596    pub max_h_freq_khz: u8,
597
598    /// Maximum supported pixel clock in MHz (stored as tens of MHz in EDID, multiplied
599    /// bu `10` here)
600    pub max_pixel_clock_mhz: u16,
601}
602
603impl RangeLimits {
604    /// Parses an 18-byte MRL Descriptor block
605    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/// Video input parameters bitmap
617#[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/// Digital input
638#[derive(Debug, Serialize, Deserialize, Clone)]
639pub struct VideoInputParamsDigital {
640    /// Bit depth
641    pub bit_depth: BitDepth,
642
643    /// Video interface type
644    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/// Bit depth
661#[derive(Debug, Serialize, Deserialize, Clone)]
662pub enum BitDepth {
663    Undefined,
664
665    /// 6 bits per color
666    B6,
667
668    /// 8 bits per color
669    B8,
670
671    /// 10 bits per color
672    B10,
673
674    /// 12 bits per color
675    B12,
676
677    /// 14 bits per color
678    B14,
679
680    /// 16 bits per color
681    B16,
682
683    /// Reserved value
684    Reserved,
685
686    /// Unknown value (while EDID parsing)
687    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/// Video interface (EDID data may be incorrect)
727#[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    /// Video white and sync levels, relative to blank:
773    ///
774    /// | Binary value | Data    |
775    /// |--------------|---------|
776    /// | `00` | +0.7/-0.3 V     |
777    /// | `01` | +0.714/-0.286 V |
778    /// | `10` | +1.0/-0.4 V     |
779    /// | `11` | +0.7/0 V (EVC)  |
780    pub white_sync_levels: u8,
781
782    /// Blank-to-black setyp (pedestal) expected
783    pub blank_to_black_setup: u8,
784
785    /// Separate sync supported
786    pub separate_sync_supported: u8,
787
788    /// Composite sync supported
789    pub composite_sync_supported: u8,
790
791    /// Sync on green supported
792    pub sync_on_green_supported: u8,
793
794    /// VSync pulse must be serrated when composite or sync-on-green
795    /// is used
796    pub sync_on_green_isused: u8,
797}
798
799impl VideoInputParamsAnalog {
800    /// NOTE: THIS FUNCTION MAY BE INCORRECT
801    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; // WARN: may be incorrect
809
810        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}