Controlling the Glorious Model O's Lighting Effects using Rust
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This is the full code of | |
// https://zazama.de/blog/reverse-engineering-usb-rgb-devices-and-building-your-own-rgb-software-by-example-using-rust-and-glorious-model-o | |
// It's not split into multiple files to make it easier to put into a blog post. | |
// Check out the real repository for more code. | |
extern crate hidapi; | |
use hidapi::{HidApi, HidDevice, HidError, DeviceInfo}; | |
const VENDOR_ID: u16 = 0x258A; | |
const PRODUCT_ID: u16 = 0x27; | |
pub struct GloriousDevice { | |
data_device: HidDevice, | |
control_device: HidDevice | |
} | |
#[repr(u8)] | |
#[derive(Copy, Clone)] | |
pub enum EffectBrightness { | |
Low = 0x10, | |
Medium = 0x20, | |
High = 0x30, | |
Highest = 0x40 | |
} | |
impl EffectBrightness { | |
fn from_u8(value: u8) -> Self { | |
return match value & 0x70 { | |
0x10 => Self::Low, | |
0x20 => Self::Medium, | |
0x30 => Self::High, | |
0x40 => Self::Highest, | |
_ => Self::Highest | |
} | |
} | |
} | |
#[derive(Copy, Clone)] | |
pub struct RGBColor { | |
pub red: u8, | |
pub green: u8, | |
pub blue: u8 | |
} | |
impl RGBColor { | |
fn from_rbg_buffer(buffer: &[u8]) -> Self { | |
return Self { | |
red: *buffer.get(0).unwrap_or(&0), | |
green: *buffer.get(2).unwrap_or(&0), | |
blue: *buffer.get(1).unwrap_or(&0) | |
}; | |
} | |
fn to_rbg_buffer(&self) -> [u8; 3] { | |
return [self.red, self.blue, self.green]; | |
} | |
} | |
#[derive(Copy, Clone)] | |
pub enum LightingEffect { | |
Unknown, | |
Off, | |
SingleColor { color: RGBColor, brightness: EffectBrightness } | |
} | |
impl LightingEffect { | |
fn from_buffer(buffer: &[u8]) -> Self { | |
if buffer.len() < 131 { | |
return Self::Unknown; | |
} | |
// We remember, Byte[53] is the effect mode. | |
return match buffer[53] { | |
0x00 => LightingEffect::Off, | |
0x02 => LightingEffect::SingleColor { | |
color: RGBColor::from_rbg_buffer(&buffer[57..60]), | |
brightness: EffectBrightness::from_u8(buffer[56]) | |
}, | |
_ => LightingEffect::Unknown | |
} | |
} | |
fn set_in_buffer(&self, buffer: &mut [u8]) -> bool { | |
if buffer.len() < 131 { | |
return false; | |
} | |
match self { | |
LightingEffect::Off | LightingEffect::Unknown => { | |
buffer[53] = 0x00; | |
}, | |
LightingEffect::SingleColor { color, brightness } => { | |
buffer[53] = 0x02; | |
buffer[56] = *brightness as u8; | |
buffer[57..60].copy_from_slice(&color.to_rbg_buffer()); | |
} | |
} | |
if matches!(self, LightingEffect::Off) { | |
buffer[130] = 0x03; | |
} else { | |
buffer[130] = 0x00; | |
} | |
return true; | |
} | |
} | |
pub struct FeatureReport { | |
raw_data: Vec<u8>, | |
lighting_effect: LightingEffect, | |
} | |
impl FeatureReport { | |
pub fn from_buffer(buffer: &[u8]) -> Option<Self> { | |
if buffer.len() < 131 { | |
return None; | |
} | |
return Some(Self { | |
raw_data: Vec::from(buffer), | |
lighting_effect: LightingEffect::from_buffer(&buffer) | |
}); | |
} | |
pub fn to_buffer(&self) -> [u8; 520] { | |
let mut data = self.raw_data.clone(); | |
data.resize(520, 0x00); | |
return data.try_into().unwrap(); | |
} | |
pub fn lighting_effect(&self) -> LightingEffect { | |
return self.lighting_effect.clone(); | |
} | |
pub fn set_lighting_effect(&mut self, effect: LightingEffect) { | |
effect.set_in_buffer(&mut self.raw_data); | |
self.lighting_effect = effect; | |
} | |
} | |
impl GloriousDevice { | |
pub fn new() -> Result<Self, HidError> { | |
let api = HidApi::new()?; | |
// Get all model o device infos | |
let devices = Self::filter_model_o_devices(&api); | |
let data_device = Self::find_data_device(&api, &devices); | |
let control_device = Self::find_control_device(&api, &devices); | |
if data_device.is_some() && control_device.is_some() { | |
return Ok(GloriousDevice { | |
data_device: data_device.unwrap(), | |
control_device: control_device.unwrap() | |
}); | |
} | |
return Err(HidError::HidApiError { message: "Device not found".to_owned() }); | |
} | |
// Filter device list for vendor / product id | |
fn filter_model_o_devices(api: &HidApi) -> Vec<&DeviceInfo> { | |
return api | |
.device_list() | |
.filter(|info| | |
info.vendor_id() == VENDOR_ID && | |
info.product_id() == PRODUCT_ID | |
) | |
.collect(); | |
} | |
fn find_data_device(api: &HidApi, devices: &Vec<&DeviceInfo>) -> Option<HidDevice> { | |
return devices | |
.iter() | |
// Only check devices that can actually be opened | |
.filter_map(|info| info.open_device(&api).ok()) | |
.filter(|device| { | |
// We will create a ReportID 4 buffer to test if it gets sent | |
// by the HID driver. If it fails, we got the wrong device. | |
let mut buffer: [u8; 520] = [0x00; 520]; | |
buffer[0] = 0x04; | |
return device | |
.send_feature_report(&mut buffer) | |
.is_ok(); | |
}).next(); | |
} | |
fn find_control_device(api: &HidApi, devices: &Vec<&DeviceInfo>) -> Option<HidDevice> { | |
return devices | |
.iter() | |
// Only check devices that can actually be opened | |
.filter_map(|info| info.open_device(&api).ok()) | |
.filter(|device| { | |
// We will create a ReportID 5 buffer to test if it gets sent | |
// by the HID driver. If it fails, we got the wrong device. | |
let mut buffer: [u8; 6] = [0x05, 0x00, 0x00, 0x00, 0x00, 0x00]; | |
return device | |
.send_feature_report(&mut buffer) | |
.is_ok(); | |
}).next(); | |
} | |
fn prepare_settings_request(&self) -> Result<(), HidError> { | |
let mut req = [0x05, 0x11, 0, 0, 0, 0]; | |
self.control_device.send_feature_report(&mut req)?; | |
return Ok(()); | |
} | |
pub fn get_settings(&self) -> Result<FeatureReport, HidError> { | |
self.prepare_settings_request()?; | |
let mut buffer: [u8; 520] = [0x00; 520]; | |
buffer[0] = 0x04; | |
self.data_device.get_feature_report(&mut buffer)?; | |
return Ok( | |
FeatureReport::from_buffer(&buffer) | |
.ok_or(HidError::HidApiError { | |
message: "Bad data".to_owned() | |
})? | |
); | |
} | |
pub fn commit_settings( | |
&self, report: &FeatureReport | |
) -> Result<(), HidError> { | |
let mut report_buffer = Vec::from(report.to_buffer()); | |
// When sending the settings, Byte[3] is always 0x7B! | |
report_buffer[3] = 0x7B; | |
self.data_device.send_feature_report(&report_buffer)?; | |
return Ok(()); | |
} | |
} | |
#[cfg(test)] | |
mod tests { | |
use crate::{GloriousDevice, LightingEffect, EffectBrightness, RGBColor}; | |
#[test] | |
fn connect() { | |
GloriousDevice::new().unwrap(); | |
} | |
#[test] | |
fn lighting_effect() { | |
let device = GloriousDevice::new().unwrap(); | |
// Test Get_Report ID 4 working | |
let mut settings = device.get_settings().unwrap(); | |
let new_lighting_effect = LightingEffect::SingleColor { | |
color: RGBColor::from_rbg_buffer(&[0x22, 0x24, 0x23]), | |
brightness: EffectBrightness::High | |
}; | |
settings.set_lighting_effect(new_lighting_effect); | |
// Test Set_Report ID 4 working | |
device.commit_settings(&settings).unwrap(); | |
match device.get_settings().unwrap().lighting_effect() { | |
LightingEffect::SingleColor { color, brightness } => { | |
assert!(color.red == 0x22); | |
assert!(color.green == 0x23); | |
assert!(color.blue == 0x24); | |
assert!(matches!(brightness, EffectBrightness::High)); | |
}, | |
_ => assert!(false) | |
}; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment