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

Commit

Permalink
Conditional Features for razer-cli (#14)
Browse files Browse the repository at this point in the history
Major changes are:
* syntax of `razer-cli` changed, script depending on it requires adjustment
* features are introduced and can be selectively activated
* 2 modes: `auto` and `manual --pid 0x029f`
  - in `auto` mode the model is detected automatically and only supported features are activated
  - in `manual` mode provided pid is used and all features are activated
* `razer-cli manual --pid 0x029f info` does not fail on the first error, instead prints the error and continues to dump info
* [model number prefix](https://mysupport.razer.com/app/answers/detail/a_id/5481) is used now to detect devices, allowing to differentiate between devices with similar PIDs (e.g. Black and Mercur colors have the same PID, but Mercur doesn't have lid logo).
* Supported features can be specified per model
* model prefix id is read from Windows registry, for Linux not implemented yet.
* cli entries are now dynamically generated based on supported features.
  • Loading branch information
tdakhran authored Jun 13, 2024
1 parent 60c37a1 commit 6aed60b
Show file tree
Hide file tree
Showing 10 changed files with 501 additions and 207 deletions.
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

0 comments on commit 6aed60b

Please sign in to comment.