Skip to content
This repository was archived by the owner on Dec 2, 2024. It is now read-only.

Conditional Features for razer-cli #14

Merged
merged 5 commits into from
Jun 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ jobs:

- name: Install target for Windows
run: rustup target add x86_64-pc-windows-gnu
- name: Run fmt
run: cargo fmt --all --check
- name: Run clippy
run: cargo clippy --all-targets --all-features
run: cargo clippy --all-targets --all-features -- -Dwarnings
- name: Build for Windows
run: cargo build --verbose --release --target x86_64-pc-windows-gnu
- name: Build for Linux
Expand Down
45 changes: 45 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions librazer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@ edition = "2021"
anyhow = "1.0.80"
bincode = "1.3.3"
clap = { version = "4.5.1", features = ["derive"] }
const-str = "0.5.7"
const_format = "0.2.32"
hidapi = {version = "2.6.1", features = ["windows-native"]}
rand = "0.8.5"
serde = { version = "1.0.197", features = ["derive"] }
serde-big-array = "0.5.1"
serde_json = "1.0.114"
strum = "0.26.1"
strum_macros = "0.26.1"

[target.'cfg(windows)'.dependencies]
winreg = { version = "0.52", features = ["transactions"] }
44 changes: 44 additions & 0 deletions librazer/src/descriptor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
use crate::feature;

// model_number_prefix shall conform to https://mysupport.razer.com/app/answers/detail/a_id/5481
#[derive(Debug, Clone)]
pub struct Descriptor {
pub model_number_prefix: &'static str,
pub name: &'static str,
pub pid: u16,
pub features: &'static [&'static str],
}

pub const SUPPORTED: &[Descriptor] = &[
Descriptor {
model_number_prefix: "RZ09-0483T",
name: "Razer Blade 16” (2023) Black",
pid: 0x029f,
features: &[
"battery-care",
"fan",
"kbd-backlight",
"lid-logo",
"lights-always-on",
"perf",
],
},
Descriptor {
model_number_prefix: "RZ09-0482X",
name: "Razer Blade 14” (2023) Mercury",
pid: 0x029d,
features: &[
"battery-care",
"fan",
"kbd-backlight",
"lights-always-on",
"perf",
],
},
];

const _VALIDATE_FEATURES: () = {
crate::const_for! { device in SUPPORTED => {
feature::validate_features(device.features);
}}
};
101 changes: 50 additions & 51 deletions librazer/src/device.rs
Original file line number Diff line number Diff line change
@@ -1,64 +1,51 @@
use crate::descriptor::{Descriptor, SUPPORTED};
use crate::packet::Packet;

use anyhow::{anyhow, Context, Result};
use std::{thread, time};

pub struct DeviceInfo {
pub name: &'static str,
pub pid: u16,
pub path: Option<String>,
}

pub const SUPPORTED: &[DeviceInfo] = &[
DeviceInfo {
name: "Razer Blade 16 2023",
pid: 0x029f,
path: None,
},
DeviceInfo {
name: "Razer Blade 14 2023",
pid: 0x029d,
path: None,
},
];

pub struct Device {
device: hidapi::HidDevice,
info: DeviceInfo,
pub info: Descriptor,
}

// Read the model id and clip to conform with https://mysupport.razer.com/app/answers/detail/a_id/5481
fn read_device_model() -> Result<String> {
#[cfg(target_os = "windows")]
{
let hklm = winreg::RegKey::predef(winreg::enums::HKEY_LOCAL_MACHINE);
let bios = hklm.open_subkey("HARDWARE\\DESCRIPTION\\System\\BIOS")?;
let system_sku: String = bios.get_value("SystemSKU")?;
Ok(system_sku.chars().take(10).collect())
}
#[cfg(not(target_os = "windows"))]
anyhow::bail!("Automatic model detection is not implemented for this platform")
}

impl Device {
const RAZER_VID: u16 = 0x1532;

pub fn info(&self) -> &DeviceInfo {
pub fn info(&self) -> &Descriptor {
&self.info
}

pub fn new(pid: u16, name: &'static str) -> Result<Device> {
pub fn new(descriptor: Descriptor) -> Result<Device> {
let api = hidapi::HidApi::new().context("Failed to create hid api")?;

// there are multiple devices with the same pid, pick first that support feature report
for info in api
.device_list()
.filter(|info| (info.vendor_id(), info.product_id()) == (Device::RAZER_VID, pid))
{
for info in api.device_list().filter(|info| {
(info.vendor_id(), info.product_id()) == (Device::RAZER_VID, descriptor.pid)
}) {
let path = info.path();
let device = api.open_path(path)?;
if device.send_feature_report(&[0, 0]).is_ok() {
return Ok(Device {
device,
info: DeviceInfo {
name,
pid,
path: Some(path.to_str().unwrap().to_string()),
},
info: descriptor.clone(),
});
}
}
anyhow::bail!(
"No device with pid 0x{:04x} and feature report support found",
pid
)
anyhow::bail!("Failed to open device {:?}", descriptor)
}

pub fn send(&self, report: Packet) -> Result<Packet> {
Expand Down Expand Up @@ -89,27 +76,39 @@ impl Device {
response.ensure_matches_report(&report)
}

pub fn enumerate() -> Result<std::vec::Vec<DeviceInfo>> {
let api = hidapi::HidApi::new().context("Failed to create hid api")?;
Ok(api
pub fn enumerate() -> Result<(Vec<u16>, String)> {
let razer_pid_list: Vec<_> = hidapi::HidApi::new()?
.device_list()
.filter(|info| info.vendor_id() == Device::RAZER_VID)
.map(|info| DeviceInfo {
name: "",
pid: info.product_id(),
path: Some(info.path().to_str().unwrap().to_string()),
})
.collect())
.map(|info| info.product_id())
.collect::<std::collections::HashSet<_>>()
.into_iter()
.collect();

if razer_pid_list.is_empty() {
anyhow::bail!("No Razer devices found")
}

match read_device_model() {
Ok(model) if model.starts_with("RZ09-") => Ok((razer_pid_list, model)),
Ok(model) => anyhow::bail!("Detected model but it's not a Razer laptop: {}", model),
Err(e) => anyhow::bail!("Failed to detect model: {}", e),
}
}

pub fn detect() -> Result<Device> {
for discovered in Device::enumerate()? {
for supported in SUPPORTED {
if supported.pid == discovered.pid {
return Device::new(supported.pid, supported.name);
}
}
let (pid_list, model_number_prefix) = Device::enumerate()?;

match SUPPORTED
.iter()
.find(|supported| model_number_prefix == supported.model_number_prefix)
{
Some(supported) => Device::new(supported.clone()),
None => anyhow::bail!(
"Model {} with PIDs {:0>4x?} is not supported",
model_number_prefix,
pid_list
),
}
anyhow::bail!("Device is not supported")
}
}
73 changes: 73 additions & 0 deletions librazer/src/feature.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
use const_format::{map_ascii_case, Case};

pub trait Feature {
fn name(&self) -> &'static str;
}

macro_rules! feature_list {
($($type:ident,)*) => {
$(
#[derive(Default)]
pub struct $type {}

impl Feature for $type {
fn name(&self) -> &'static str {
map_ascii_case!(Case::Kebab, stringify!($type))
}
}
)*

pub const ALL_FEATURES: &[&'static str] = &[
$(map_ascii_case!(Case::Kebab, stringify!($type)),)*
];

#[macro_export]
macro_rules! iter_features {
($apply:expr) => {
{
let mut v = Vec::new();
$(
let entry = $type::default();
v.push($apply(entry.name(), entry));
)*
v
}
}
}
}
}

#[macro_export]
macro_rules! const_for {
($var:ident in $iter:expr => $block:block) => {
let mut iter = $iter;
while let [$var, tail @ ..] = iter {
iter = tail;
$block
}
};
}

const fn contains(array: &[&str], value: &str) -> bool {
const_for! { it in array => {
if const_str::equal!(*it, value) {
return true;
}
}}
false
}

pub const fn validate_features(features: &[&str]) {
const_for! { f in features => {
assert!(contains(ALL_FEATURES, f), "Feature is not in supported list");
}}
}

feature_list![
BatteryCare,
LidLogo,
LightsAlwaysOn,
KbdBacklight,
Fan,
Perf,
];
2 changes: 2 additions & 0 deletions librazer/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
pub mod command;
pub mod device;
pub mod feature;
pub mod types;

pub mod descriptor;
mod packet;
2 changes: 1 addition & 1 deletion razer-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ edition = "2021"

[dependencies]
librazer = { path = "../librazer" }
clap = { version = "4.5.1", features = ["derive"] }
clap = { version = "4.5.1", features = ["cargo"] }
clap-num = "1.1.1"
anyhow = "1.0.80"
Loading
Loading