Skip to content

Instantly share code, notes, and snippets.

@Zazama

Zazama/lib.rs Secret

Created February 1, 2023 16:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Zazama/906be7d3d1b05117bf3f220eb45c06e9 to your computer and use it in GitHub Desktop.
Save Zazama/906be7d3d1b05117bf3f220eb45c06e9 to your computer and use it in GitHub Desktop.
Controlling the Glorious Model O's Lighting Effects using Rust
// 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