vitaly 0.1.32

VIA/Vial API client and cli tool for guiless keyboard configuration.
Documentation
use anyhow::{Result, anyhow};
use palette::rgb::Rgb;
use palette::{FromColor, Hsv, Srgb};
use std::cmp::min;
use std::fmt;

use crate::protocol::{
    CMD_VIA_LIGHTING_GET_VALUE, CMD_VIA_LIGHTING_SAVE, CMD_VIA_LIGHTING_SET_VALUE, MESSAGE_LENGTH,
    VIA_UNHANDLED, VIALRGB_DIRECT_FASTSET, VIALRGB_GET_INFO, VIALRGB_GET_MODE,
    VIALRGB_GET_NUMBER_LEDS, VIALRGB_GET_SUPPORTED, VIALRGB_SET_MODE, send_recv,
};
use hidapi::HidDevice;

pub fn rgb_to_hsv(color: &str, max_brightness: u8) -> Result<(u8, u8, u8)> {
    let rgb: Srgb<u8> = color.parse()?;
    let ru8: Srgb = Srgb::from(rgb);
    let hsv: Hsv = Hsv::from_color(ru8);
    let hu8: Hsv<_, u8> = hsv.into_format::<u8>();
    let (h, s, v) = hu8.into_components();
    let normal_brightness = ((v as f64) * (max_brightness as f64) / 255.0).round() as u8;
    Ok((h.into_inner(), s, normal_brightness))
}

pub fn hsv_to_rgb(h: u8, s: u8, v: u8, max_brightness: u8) -> String {
    let normal_brightness = ((v as f64) / (max_brightness as f64) * 255.0).round() as u8;
    let hsv8: Hsv<_, u8> = Hsv::<Srgb, u8>::from_components((h, s, normal_brightness));
    let hsv: Hsv<Rgb, f32> = hsv8.into_format();
    let rgb: Rgb<Rgb> = Rgb::from_color(hsv);
    let rgb8: Rgb<_, u8> = rgb.into_format::<u8>();
    let (r, g, b) = rgb8.into_components();
    format!("#{:02x}{:02x}{:02x}", r, g, b)
}

#[derive(Debug)]
pub struct RGBInfo {
    pub version: u16,
    pub effect: u16,
    pub effect_speed: u8,
    pub color_h: u8,
    pub color_s: u8,
    pub color_v: u8,
    pub max_brightness: u8,
    pub effects: Vec<u16>,
    pub leds_count: u16,
}

impl RGBInfo {
    pub fn effect_name(id: u16) -> Result<&'static str> {
        match id {
            0 => Ok("Disable"),
            1 => Ok("Direct Control"),
            2 => Ok("Solid Color"),
            3 => Ok("Alphas Mods"),
            4 => Ok("Gradient Up Down"),
            5 => Ok("Gradient Left Right"),
            6 => Ok("Breathing"),
            7 => Ok("Band Sat"),
            8 => Ok("Band Val"),
            9 => Ok("Band Pinwheel Sat"),
            10 => Ok("Band Pinwheel Val"),
            11 => Ok("Band Spiral Sat"),
            12 => Ok("Band Spiral Val"),
            13 => Ok("Cycle All"),
            14 => Ok("Cycle Left Right"),
            15 => Ok("Cycle Up Down"),
            16 => Ok("Rainbow Moving Chevron"),
            17 => Ok("Cycle Out In"),
            18 => Ok("Cycle Out In Dual"),
            19 => Ok("Cycle Pinwheel"),
            20 => Ok("Cycle Spiral"),
            21 => Ok("Dual Beacon"),
            22 => Ok("Rainbow Beacon"),
            23 => Ok("Rainbow Pinwheels"),
            24 => Ok("Raindrops"),
            25 => Ok("Jellybean Raindrops"),
            26 => Ok("Hue Breathing"),
            27 => Ok("Hue Pendulum"),
            28 => Ok("Hue Wave"),
            29 => Ok("Typing Heatmap"),
            30 => Ok("Digital Rain"),
            31 => Ok("Solid Reactive Simple"),
            32 => Ok("Solid Reactive"),
            33 => Ok("Solid Reactive Wide"),
            34 => Ok("Solid Reactive Multiwide"),
            35 => Ok("Solid Reactive Cross"),
            36 => Ok("Solid Reactive Multicross"),
            37 => Ok("Solid Reactive Nexus"),
            38 => Ok("Solid Reactive Multinexus"),
            39 => Ok("Splash"),
            40 => Ok("Multisplash"),
            41 => Ok("Solid Splash"),
            42 => Ok("Solid Multisplash"),
            43 => Ok("Pixel Rain"),
            44 => Ok("Pixel Fractal"),
            _ => Err(anyhow!("no such effect")),
        }
    }

    pub fn dump_supported_effects(&self) {
        println!("supported_effects:");
        for effect in &self.effects {
            if let Ok(name) = RGBInfo::effect_name(*effect) {
                println!("{}) {}", effect, name);
            };
        }
    }

    pub fn set_color(&mut self, color: &str) -> Result<()> {
        let (h, s, v) = rgb_to_hsv(color, self.max_brightness)?;
        self.color_h = h;
        self.color_s = s;
        self.color_v = v;
        Ok(())
    }
}

impl fmt::Display for RGBInfo {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
        writeln!(
            f,
            "RGB verions: {}, leds_count: {}, max_brightness: {}",
            self.version, self.leds_count, self.max_brightness
        )?;
        writeln!(f, "current settings:")?;
        if let Ok(name) = RGBInfo::effect_name(self.effect) {
            writeln!(f, "\teffect: {} - {}", self.effect, name)?;
        };
        writeln!(f, "\teffect_speed: {}", self.effect_speed)?;
        writeln!(
            f,
            "\tcolor: (h={}, s={}, v={}) - {}",
            self.color_h,
            self.color_s,
            self.color_v,
            hsv_to_rgb(
                self.color_h,
                self.color_s,
                self.color_v,
                self.max_brightness
            )
        )?;
        Ok(())
    }
}

pub fn load_rgb_info(device: &HidDevice) -> Result<RGBInfo> {
    let version: u16;
    let max_brightness: u8;
    let mut effect: u16 = 0;
    let mut effect_speed: u8 = 0;
    let mut color_h: u8 = 0;
    let mut color_s: u8 = 0;
    let mut color_v: u8 = 0;
    let mut leds_count: u16 = 0;

    let mut effects: Vec<u16> = Vec::new();
    effects.push(0);

    match send_recv(device, &[CMD_VIA_LIGHTING_GET_VALUE, VIALRGB_GET_INFO]) {
        Ok(data) => {
            if data[0] != VIA_UNHANDLED {
                version = (data[2] as u16) + ((data[3] as u16) << 8);
                max_brightness = data[4];
                let mut effect: u16 = 0;
                'top: loop {
                    let e2 = (effect >> 8 & 0xFF) as u8;
                    let e1 = (effect & 0xFF) as u8;

                    match send_recv(
                        device,
                        &[CMD_VIA_LIGHTING_GET_VALUE, VIALRGB_GET_SUPPORTED, e1, e2],
                    ) {
                        Ok(data) => {
                            for i in 0..15 {
                                effect = (data[i * 2 + 2] as u16) + ((data[i * 2 + 3] as u16) << 8);
                                if effect == 0xFFFF {
                                    break 'top;
                                }
                                effects.push(effect);
                            }
                        }
                        Err(e) => return Err(e),
                    }
                }
            } else {
                version = 0;
                max_brightness = 0;
            }
        }
        Err(e) => return Err(e),
    }

    if version == 1 {
        match send_recv(device, &[CMD_VIA_LIGHTING_GET_VALUE, VIALRGB_GET_MODE]) {
            Ok(data) => {
                effect = (data[2] as u16) + ((data[3] as u16) << 8);
                effect_speed = data[4];
                color_h = data[5];
                color_s = data[6];
                color_v = data[7];
            }
            Err(e) => return Err(e),
        }
        match send_recv(
            device,
            &[CMD_VIA_LIGHTING_GET_VALUE, VIALRGB_GET_NUMBER_LEDS],
        ) {
            Ok(data) => {
                leds_count = (data[2] as u16) + ((data[3] as u16) << 8);
            }
            Err(e) => return Err(e),
        }
    }

    Ok(RGBInfo {
        version,
        max_brightness,
        effects,
        effect,
        effect_speed,
        color_h,
        color_s,
        color_v,
        leds_count,
    })
}

pub fn set_rgb_mode(device: &HidDevice, rgb_info: &RGBInfo) -> Result<()> {
    let e1 = (rgb_info.effect & 0xFF) as u8;
    let e2 = ((rgb_info.effect >> 8) & 0xFF) as u8;
    match send_recv(
        device,
        &[
            CMD_VIA_LIGHTING_SET_VALUE,
            VIALRGB_SET_MODE,
            e1,
            e2,
            rgb_info.effect_speed,
            rgb_info.color_h,
            rgb_info.color_s,
            rgb_info.color_v,
        ],
    ) {
        Ok(_) => {}
        Err(e) => return Err(e),
    }

    Ok(())
}

pub fn persist_rgb(device: &HidDevice) -> Result<()> {
    match send_recv(device, &[CMD_VIA_LIGHTING_SAVE]) {
        Ok(_) => {}
        Err(e) => return Err(e),
    }

    Ok(())
}

const LEDS_PER_REQ: u16 = 8;

fn set_leds_range(
    device: &HidDevice,
    from: u16,
    to: u16,
    color: &str,
    max_brightness: u8,
) -> Result<()> {
    if to - from > LEDS_PER_REQ {
        let mut current_from = from;
        while current_from < to {
            set_leds_range(
                device,
                current_from,
                min(to, current_from + LEDS_PER_REQ),
                color,
                max_brightness,
            )?;
            current_from += LEDS_PER_REQ + 1;
        }
        return Ok(());
    }
    let (h, s, v) = rgb_to_hsv(color, max_brightness)?;
    let mut buff: [u8; MESSAGE_LENGTH] = [0u8; MESSAGE_LENGTH];
    buff[0] = CMD_VIA_LIGHTING_SET_VALUE;
    buff[1] = VIALRGB_DIRECT_FASTSET;
    buff[2] = (from & 0xFF) as u8;
    buff[3] = (from >> 8 & 0xFF) as u8;
    buff[4] = (to - from + 1) as u8;
    for i in 0..=(to - from) {
        let start = (5 + i * 3) as usize;
        buff[start] = h;
        buff[start + 1] = s;
        buff[start + 2] = v;
    }
    //println!("{:?}", buff);
    match send_recv(device, &buff) {
        Ok(_) => {
            //println!("{:?}", data);
        }
        Err(e) => return Err(e),
    }
    //println!("{}->{} = {} = {} {} {}", from, to, color, h, s, v);
    Ok(())
}

pub fn set_leds_direct(device: &HidDevice, command: &str, max_brightness: u8) -> Result<()> {
    let instructions = command.replace(" ", "");
    for instruction in instructions.split(";") {
        if let Some((leds, color)) = instruction.split_once("=") {
            for led in leds.split(",") {
                if let Some((led_from, led_to)) = led.split_once("-") {
                    set_leds_range(
                        device,
                        led_from.parse()?,
                        led_to.parse()?,
                        color,
                        max_brightness,
                    )?;
                } else {
                    let led_num: u16 = led.parse()?;
                    set_leds_range(device, led_num, led_num, color, max_brightness)?;
                }
            }
        } else {
            return Err(anyhow!("Bad instruction"));
        }
    }
    Ok(())
}