amdgpu 1.0.12

Subcomponent of AMDGPU fan control service
//! AMD GUI helper communication toolkit

use std::io::{Read, Write};
use std::os::unix::net::UnixStream;
use std::path::PathBuf;

use serde::{Deserialize, Serialize};
use tracing::info;

use crate::pidfile::PidResponse;

#[derive(Debug, thiserror::Error)]
pub enum PortsError {
    #[error("AMD GPU ports socket file not found. Is service running?")]
    NoSockFile,
    #[error("Failed to connect to /tmp/amdgpu-ports.sock. {0}")]
    UnableToConnect(#[from] std::io::Error),
    #[error("Failed to ports command. {0}")]
    Serialize(#[from] ron::Error),
}

#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub enum OutputType {
    Reserved,
    #[serde(rename = "v")]
    Vga,
    #[serde(rename = "m")]
    MiniDvi,
    #[serde(rename = "h")]
    Hdmi,
    #[serde(rename = "a")]
    Audio,
    #[serde(rename = "o")]
    OpticalAudio,
    #[serde(rename = "d")]
    Dvi,
    #[serde(rename = "t")]
    Thunderbolt,
    #[serde(rename = "D")]
    DisplayPort,
    #[serde(rename = "M")]
    MiniDisplayPort,
    #[serde(rename = "f")]
    FireWire400,
    #[serde(rename = "p")]
    Ps2,
    #[serde(rename = "s")]
    Sata,
    #[serde(rename = "e")]
    ESata,
    #[serde(rename = "E")]
    Ethernet,
    #[serde(rename = "F")]
    FireWire800,
    #[serde(rename = "1")]
    UsbTypeA,
    #[serde(rename = "2")]
    UsbTypeB,
    #[serde(rename = "3")]
    UsbTypeC,
    #[serde(rename = "4")]
    MicroUsb,
    #[serde(rename = "5")]
    MimiUsb,
}

impl OutputType {
    pub fn to_coords(&self) -> (u32, u32) {
        match self {
            OutputType::Reserved => (0, 0),
            //
            OutputType::Vga => (0, 0),
            OutputType::MiniDvi => (80, 0),
            OutputType::Hdmi => (160, 0),
            OutputType::Audio => (240, 0),
            OutputType::OpticalAudio => (320, 0),
            //
            OutputType::Dvi => (0, 80),
            OutputType::Thunderbolt => (80, 80),
            OutputType::DisplayPort => (160, 80),
            OutputType::MiniDisplayPort => (240, 80),
            OutputType::FireWire400 => (320, 80),
            //
            OutputType::Ps2 => (0, 160),
            OutputType::Sata => (80, 160),
            OutputType::ESata => (160, 160),
            OutputType::Ethernet => (240, 160),
            OutputType::FireWire800 => (320, 160),
            //
            OutputType::UsbTypeA => (0, 240),
            OutputType::UsbTypeB => (80, 240),
            OutputType::UsbTypeC => (160, 240),
            OutputType::MicroUsb => (240, 240),
            OutputType::MimiUsb => (320, 240),
        }
    }
    pub fn name(&self) -> &str {
        match self {
            OutputType::Reserved => "-----",
            //
            OutputType::Vga => "Vga",
            OutputType::MiniDvi => "MiniDvi",
            OutputType::Hdmi => "Hdmi",
            OutputType::Audio => "Audio",
            OutputType::OpticalAudio => "OptimalAudio",
            //
            OutputType::Dvi => "Dvi",
            OutputType::Thunderbolt => "Thunderbolt",
            OutputType::DisplayPort => "DisplayPort",
            OutputType::MiniDisplayPort => "MiniDisplayPort",
            OutputType::FireWire400 => "FireWire400",
            //
            OutputType::Ps2 => "Ps2",
            OutputType::Sata => "Sata",
            OutputType::ESata => "ESata",
            OutputType::Ethernet => "Ethernet",
            OutputType::FireWire800 => "FireWire800",
            //
            OutputType::UsbTypeA => "UsbTypeA",
            OutputType::UsbTypeB => "UsbTypeB",
            OutputType::UsbTypeC => "UsbTypeC",
            OutputType::MicroUsb => "MicroUsb",
            OutputType::MimiUsb => "MimiUsb",
        }
    }

    pub fn parse_str(s: &str) -> Option<Self> {
        Some(match s {
            "DP" => Self::DisplayPort,
            "eDP" => Self::MiniDisplayPort,
            "DVI" => Self::Dvi,
            "HDMI" => Self::Hdmi,
            _ => return None,
        })
    }

    pub fn all() -> [OutputType; 20] {
        [
            OutputType::Vga,
            OutputType::MiniDvi,
            OutputType::Hdmi,
            OutputType::Audio,
            OutputType::OpticalAudio,
            OutputType::Dvi,
            OutputType::Thunderbolt,
            OutputType::DisplayPort,
            OutputType::MiniDisplayPort,
            OutputType::FireWire400,
            OutputType::Ps2,
            OutputType::Sata,
            OutputType::ESata,
            OutputType::Ethernet,
            OutputType::FireWire800,
            OutputType::UsbTypeA,
            OutputType::UsbTypeB,
            OutputType::UsbTypeC,
            OutputType::MicroUsb,
            OutputType::MimiUsb,
        ]
    }
}

#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct Output {
    #[serde(rename = "c")]
    pub card: String,
    #[serde(rename = "t")]
    pub port_type: String,
    #[serde(rename = "T")]
    pub ty: Option<OutputType>,
    #[serde(rename = "m")]
    pub port_name: Option<String>,
    #[serde(rename = "n")]
    pub port_number: u8,
    #[serde(rename = "s")]
    pub status: Status,
    #[serde(rename = "M")]
    pub modes: Vec<OutputMode>,
    pub display_power_managment: bool,
}

#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct OutputMode {
    #[serde(rename = "w")]
    pub width: u16,
    #[serde(rename = "h")]
    pub height: u16,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub enum Status {
    #[serde(rename = "c")]
    Connected,
    #[serde(rename = "d")]
    Disconnected,
}

impl Default for Status {
    fn default() -> Self {
        Self::Disconnected
    }
}

impl Output {
    fn to_path(&self) -> PathBuf {
        PathBuf::new().join("/sys/class/drm").join(format!(
            "card{}-{}{}-{}",
            self.card,
            self.port_type,
            self.port_name
                .as_deref()
                .map(|s| format!("-{s}"))
                .unwrap_or_default(),
            self.port_number
        ))
    }

    fn status_path(&self) -> PathBuf {
        self.to_path().join("status")
    }

    pub fn read_status(&self) -> Option<Status> {
        Some(
            match std::fs::read_to_string(self.status_path()).ok()?.trim() {
                "connected" => Status::Connected,
                "disconnected" => Status::Disconnected,
                _ => return None,
            },
        )
    }
}

#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub enum Command {
    Ports,
}

#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub enum Response {
    Ports(Vec<Output>),
    NoOp,
}

impl PidResponse for Response {
    fn kill_response() -> Self {
        Self::NoOp
    }
}

pub fn sock_file() -> PathBuf {
    std::path::Path::new("/tmp").join("amdgpu-ports.sock")
}

pub fn send_command(cmd: Command) -> crate::Result<Response> {
    let sock_path = sock_file();

    if !sock_path.exists() {
        return Err(PortsError::NoSockFile.into());
    }

    let mut stream = UnixStream::connect(&sock_path).map_err(PortsError::UnableToConnect)?;
    let s = ron::to_string(&cmd).map_err(PortsError::Serialize)?;
    if stream.write_all(format!("{}\n", s).as_bytes()).is_ok() {
        info!("Command send");
    }

    let res: Response = {
        let mut s = String::with_capacity(100);
        let _ = stream.read_to_string(&mut s);
        ron::from_str(&s).map_err(PortsError::Serialize)?
    };

    Ok(res)
}