m27qx 0.1.0

Control Gigabyte M27Q X via USB
use std::{io::Write, time::Duration};

use anyhow::Context;
use clap::{Parser, ValueEnum};
use rusb::{DeviceHandle, GlobalContext};

const M27QX_VID: u16 = 0x0BDA;
const M27QX_PID: u16 = 0x1100;
const INTERFACE_NUMBER: u8 = 0;

#[derive(Copy, Clone, Debug, ValueEnum)]
#[value(rename_all = "verbatim")]
enum PictureMode {
    Standard = 0x00,
    #[value(name = "FPS")]
    Fps = 0x01,
    #[value(name = "RTS/RPG")]
    RtsRpg = 0x02,
    Movie = 0x03,
    Reader = 0x04,
    #[value(name = "sRGB")]
    Srgb = 0x05,
    Custom1 = 0x06,
    Custom2 = 0x07,
    Custom3 = 0x08,
}

fn forall(cli: Cli) -> anyhow::Result<()> {
    let mut found = 0;
    for device in rusb::devices().context("listing USB devices")?.iter() {
        let descriptor = device
            .device_descriptor()
            .context("Reading device descriptor")?;
        if descriptor.vendor_id() == M27QX_VID && descriptor.product_id() == M27QX_PID {
            found += 1;

            let handle = device.open().context("Opening USB device")?;
            let is_active = handle
                .kernel_driver_active(INTERFACE_NUMBER)
                .unwrap_or(false);
            if is_active {
                handle
                    .detach_kernel_driver(INTERFACE_NUMBER)
                    .context("Detaching kernel driver")?;
            }
            handle
                .claim_interface(INTERFACE_NUMBER)
                .context("Claiming interface")?;

            if let Some(picture_mode) = cli.picture_mode {
                set_picture_mode(&handle, picture_mode).context("Setting picture mode")?;
            }
            if cli.kvm {
                kvm(&handle).context("Triggering KVM switch")?;
            }

            if is_active {
                // When using the KVM switch the device will disconnect, so ignore failure to reattach.
                handle.attach_kernel_driver(INTERFACE_NUMBER).ok();
            }
        }
    }
    if found == 0 {
        anyhow::bail!("No M27Q-X devices found");
    }
    Ok(())
}

fn set_configuration(handle: &DeviceHandle<GlobalContext>, command: &[u8]) -> anyhow::Result<()> {
    const MYSTERY_PREFIX: [u8; 64] = [
        0x40, 0xc6, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x6e, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00,
    ];
    let checksum = command.iter().fold(0, |acc, &val| acc ^ val);
    let mut buffer = [0u8; 192];
    let mut cursor = &mut buffer[..];
    cursor.write_all(&MYSTERY_PREFIX)?;
    cursor.write_all(command)?;
    cursor.write_all(&[checksum])?;

    let request_type = rusb::request_type(
        rusb::Direction::Out,
        rusb::RequestType::Class,
        rusb::Recipient::Interface,
    );
    handle
        .write_control(
            request_type,
            rusb::constants::LIBUSB_REQUEST_SET_CONFIGURATION,
            0x0200,
            0,
            &buffer,
            Duration::from_secs(1),
        )
        .context("USB write_control")?;
    Ok(())
}

fn send_command(
    handle: &DeviceHandle<GlobalContext>,
    command: u8,
    value: u8,
) -> anyhow::Result<()> {
    set_configuration(handle, &[0x51, 0x85, 0x03, 0xe0, command, 0x00, value])
}

fn set_picture_mode(handle: &DeviceHandle<GlobalContext>, mode: PictureMode) -> anyhow::Result<()> {
    send_command(handle, 0x2c, mode as u8)
}

fn kvm(handle: &DeviceHandle<GlobalContext>) -> anyhow::Result<()> {
    send_command(handle, 0x69, 0x01)
}

#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Cli {
    /// Picture mode to set on the monitor
    #[arg(long, value_enum, ignore_case = true)]
    picture_mode: Option<PictureMode>,
    /// Trigger the KVM switch
    #[arg(long)]
    kvm: bool,
}

fn main() -> anyhow::Result<()> {
    let cli = Cli::parse();
    forall(cli)
}