This commit is contained in:
2024-08-27 21:08:03 +03:00
commit e25e4df3cf
10 changed files with 941 additions and 0 deletions

209
src/controller.rs Normal file
View File

@ -0,0 +1,209 @@
use hidapi::{HidApi, HidDevice};
use log::{info, warn};
use std::ffi::CString;
use std::thread;
use std::time::Duration;
use crate::devices::RAZER_DEVICE_LIST;
const MAX_TRIES_SEND: u8 = 10;
const TIME_BETWEEN_SEND: Duration = Duration::from_millis(500);
pub struct RazerReport {
pub status: u8,
pub transaction_id: u8,
pub remaining_packets: u16,
pub protocol_type: u8,
pub data_size: u8,
pub command_class: u8,
pub command_id: u8,
pub arguments: [u8; 80],
pub crc: u8,
pub reserved: u8,
}
impl RazerReport {
pub const STATUS_NEW_COMMAND: u8 = 0x00;
pub const STATUS_BUSY: u8 = 0x01;
pub const STATUS_SUCCESSFUL: u8 = 0x02;
pub const STATUS_FAILURE: u8 = 0x03;
pub const STATUS_NO_RESPONSE: u8 = 0x04;
pub const STATUS_NOT_SUPPORTED: u8 = 0x05;
pub fn new() -> Self {
RazerReport {
status: 0,
transaction_id: 0,
remaining_packets: 0,
protocol_type: 0,
data_size: 0,
command_class: 0,
command_id: 0,
arguments: [0; 80],
crc: 0,
reserved: 0,
}
}
pub fn from_bytes(data: &[u8]) -> Result<Self, &'static str> {
if data.len() != 90 {
return Err("Expected 90 bytes of data as razer report");
}
let mut report = RazerReport::new();
report.status = data[0];
report.transaction_id = data[1];
report.remaining_packets = u16::from_be_bytes([data[2], data[3]]);
report.protocol_type = data[4];
report.data_size = data[5];
report.command_class = data[6];
report.command_id = data[7];
report.arguments.copy_from_slice(&data[8..88]);
report.crc = data[88];
report.reserved = data[89];
Ok(report)
}
pub fn pack(&self) -> Vec<u8> {
let mut data = vec![
self.status,
self.transaction_id,
(self.remaining_packets >> 8) as u8,
(self.remaining_packets & 0xFF) as u8,
self.protocol_type,
self.data_size,
self.command_class,
self.command_id,
];
data.extend_from_slice(&self.arguments);
data.push(self.crc);
data.push(self.reserved);
data
}
pub fn calculate_crc(&self) -> u8 {
let data = self.pack();
data[2..88].iter().fold(0, |crc, &byte| crc ^ byte)
}
pub fn is_valid(&self) -> bool {
self.calculate_crc() == self.crc
}
}
pub struct DeviceController {
pub handle: HidDevice,
pub name: String,
pub pid: u16,
pub report_id: u8,
pub transaction_id: u8,
}
impl DeviceController {
pub fn new(name: String, pid: u16, path: String) -> Result<Self, Box<dyn std::error::Error>> {
let api = HidApi::new()?;
// Convert the path String to a CString
let c_path = CString::new(path)?;
let handle = api.open_path(c_path.as_ref())?;
let transaction_id = RAZER_DEVICE_LIST
.iter()
.find(|device| device.pid == pid)
.map_or(0x3F, |device| device.transaction_id());
Ok(DeviceController {
handle,
name,
pid,
report_id: 0x00,
transaction_id,
})
}
pub fn get_battery_level(&self) -> Result<i32, Box<dyn std::error::Error>> {
let request = self.create_command(0x07, 0x80, 0x02);
let response = self.send_payload(request)?;
let battery_level = (response.arguments[1] as f32 / 255.0) * 100.0;
// println!("{}\t battery level: {:.2}%", self.name, battery_level);
Ok(battery_level.round() as i32)
}
pub fn get_charging_status(&self) -> Result<bool, Box<dyn std::error::Error>> {
let request = self.create_command(0x07, 0x84, 0x02);
let response = self.send_payload(request)?;
let charging_status = response.arguments[1] != 0;
// println!("{}\t charging status: {}", self.name, charging_status);
Ok(charging_status)
}
pub fn send_payload(
&self,
mut request: RazerReport,
) -> Result<RazerReport, Box<dyn std::error::Error>> {
request.crc = request.calculate_crc();
for _ in 0..MAX_TRIES_SEND {
self.usb_send(&request)?;
let response = self.usb_receive()?;
if response.remaining_packets != request.remaining_packets
|| response.command_class != request.command_class
|| response.command_id != request.command_id
{
return Err("Response doesn't match request".into());
}
match response.status {
RazerReport::STATUS_SUCCESSFUL => return Ok(response),
RazerReport::STATUS_BUSY => info!("Device is busy"),
RazerReport::STATUS_NO_RESPONSE => info!("Command timed out"),
RazerReport::STATUS_NOT_SUPPORTED => return Err("Command not supported".into()),
RazerReport::STATUS_FAILURE => return Err("Command failed".into()),
_ => return Err("Error unknown report status".into()),
}
thread::sleep(TIME_BETWEEN_SEND);
warn!("Trying to resend command");
}
Err(format!("Abort command (tries: {})", MAX_TRIES_SEND).into())
}
pub fn create_command(&self, command_class: u8, command_id: u8, data_size: u8) -> RazerReport {
let mut report = RazerReport::new();
report.status = RazerReport::STATUS_NEW_COMMAND;
report.transaction_id = self.transaction_id;
report.command_class = command_class;
report.command_id = command_id;
report.data_size = data_size;
report
}
pub fn usb_send(&self, report: &RazerReport) -> Result<(), Box<dyn std::error::Error>> {
let mut data = vec![self.report_id];
data.extend_from_slice(&report.pack());
self.handle.send_feature_report(&data)?;
thread::sleep(Duration::from_millis(60));
Ok(())
}
pub fn usb_receive(&self) -> Result<RazerReport, Box<dyn std::error::Error>> {
let expected_length = 91;
let mut buf = vec![0u8; expected_length];
let bytes_read = self.handle.get_feature_report(&mut buf)?;
if bytes_read != expected_length {
return Err("Error while getting feature report".into());
}
let report = RazerReport::from_bytes(&buf[1..])?;
if !report.is_valid() {
return Err("Get report has no valid crc".into());
}
Ok(report)
}
}

48
src/devices.rs Normal file
View File

@ -0,0 +1,48 @@
pub struct DeviceInfo {
pub name: &'static str,
pub pid: u16,
pub interface: u8,
pub usage_page: u16,
pub usage: u16,
pub vid: u16,
}
impl DeviceInfo {
pub const fn new(
name: &'static str,
pid: u16,
interface: u8,
usage_page: u16,
usage: u16,
) -> Self {
DeviceInfo {
name,
pid,
interface,
usage_page,
usage,
vid: 0x1532,
}
}
pub const fn transaction_id(&self) -> u8 {
match self.pid {
pid if pid == RAZER_DEATHADDER_V3_PRO_WIRED.pid
|| pid == RAZER_DEATHADDER_V3_PRO_WIRELESS.pid =>
{
0x1F
}
_ => 0x3F,
}
}
}
pub const RAZER_DEATHADDER_V3_PRO_WIRED: DeviceInfo =
DeviceInfo::new("Razer DeathAdder V3 Pro", 0x00B6, 0, 1, 2);
pub const RAZER_DEATHADDER_V3_PRO_WIRELESS: DeviceInfo =
DeviceInfo::new("Razer DeathAdder V3 Pro", 0x00B7, 0, 1, 2);
pub const RAZER_DEVICE_LIST: [DeviceInfo; 2] = [
RAZER_DEATHADDER_V3_PRO_WIRED,
RAZER_DEATHADDER_V3_PRO_WIRELESS,
];

154
src/main.rs Normal file
View File

@ -0,0 +1,154 @@
use std::{
collections::HashMap,
sync::{Arc, Mutex},
thread,
time::Duration,
};
use log::{error, info};
use manager::DeviceManager;
mod controller;
mod devices;
mod manager;
const BATTERY_UPDATE_INTERVAL: u64 = 60; // seconds
const DEVICE_FETCH_INTERVAL: u64 = 5; // seconds
struct MemoryDevice {
name: String,
#[allow(unused)]
id: u32,
battery_level: i32,
old_battery_level: i32,
is_charging: bool,
}
impl MemoryDevice {
fn new(name: String, id: u32) -> Self {
MemoryDevice {
name,
id,
battery_level: -1,
old_battery_level: 50,
is_charging: false,
}
}
}
struct BatteryChecker {
device_manager: Arc<Mutex<DeviceManager>>,
devices: Arc<Mutex<HashMap<u32, MemoryDevice>>>,
}
impl BatteryChecker {
fn new() -> Self {
BatteryChecker {
device_manager: Arc::new(Mutex::new(DeviceManager::new())),
devices: Arc::new(Mutex::new(HashMap::new())),
}
}
fn run(&self) {
let devices = Arc::clone(&self.devices);
let device_manager = Arc::clone(&self.device_manager);
// Device fetching thread
thread::spawn(move || loop {
let (removed_devices, connected_devices) = {
let mut manager = device_manager.lock().unwrap();
manager.fetch_devices()
};
{
let mut devices = devices.lock().unwrap();
for id in removed_devices {
if let Some(device) = devices.remove(&id) {
info!("Device removed: {}", device.name);
}
}
for id in &connected_devices {
if !devices.contains_key(id) {
if let Some(name) = device_manager.lock().unwrap().get_device_name(*id) {
devices.insert(*id, MemoryDevice::new(name.clone(), *id));
info!("New device: {}", name);
} else {
error!("Failed to get device name for id: {}", id);
}
}
}
}
if !connected_devices.is_empty() {
Self::update(&devices, &device_manager, &connected_devices);
}
thread::sleep(Duration::from_secs(DEVICE_FETCH_INTERVAL));
});
// Battery check thread
loop {
let device_ids: Vec<u32> = {
let devices = self.devices.lock().unwrap();
devices.keys().cloned().collect()
};
Self::update(&self.devices, &self.device_manager, &device_ids);
thread::sleep(Duration::from_secs(BATTERY_UPDATE_INTERVAL));
}
}
fn update(
devices: &Arc<Mutex<HashMap<u32, MemoryDevice>>>,
manager: &Arc<Mutex<DeviceManager>>,
device_ids: &[u32],
) {
let mut devices = devices.lock().unwrap();
let manager = manager.lock().unwrap();
for &id in device_ids {
if let Some(device) = devices.get_mut(&id) {
if let Some(battery_level) = manager.get_device_battery_level(id) {
if let Some(is_charging) = manager.is_device_charging(id) {
info!("{} battery level: {}%", device.name, battery_level);
info!("{} charging status: {}", device.name, is_charging);
device.old_battery_level = device.battery_level;
device.battery_level = battery_level;
device.is_charging = is_charging;
Self::check_notify(device);
}
}
}
}
}
fn check_notify(device: &MemoryDevice) {
if device.battery_level == -1 {
return;
}
if !device.is_charging
&& (device.battery_level <= 5
|| (device.old_battery_level > 15 && device.battery_level <= 15))
{
info!("{}: Battery low ({}%)", device.name, device.battery_level);
} else if device.old_battery_level <= 99
&& device.battery_level == 100
&& device.is_charging
{
info!(
"{}: Battery fully charged ({}%)",
device.name, device.battery_level
);
}
}
}
fn main() {
std::env::set_var("RUST_LOG", "trace");
pretty_env_logger::init();
let checker = BatteryChecker::new();
checker.run();
}

131
src/manager.rs Normal file
View File

@ -0,0 +1,131 @@
use hidapi::HidApi;
use log::warn;
use std::collections::HashSet;
use std::sync::{Arc, Mutex};
use std::vec::Vec;
use crate::controller::DeviceController;
use crate::devices::RAZER_DEVICE_LIST;
pub struct DeviceManager {
pub device_controllers: Arc<Mutex<Vec<DeviceController>>>,
}
impl DeviceManager {
pub fn new() -> Self {
Self {
device_controllers: Arc::new(Mutex::new(Vec::new())),
}
}
pub fn fetch_devices(&mut self) -> (Vec<u32>, Vec<u32>) {
let old_ids: HashSet<u32> = {
let controllers = self.device_controllers.lock().unwrap();
controllers
.iter()
.map(|controller| controller.pid as u32)
.collect()
};
let new_controllers = self.get_connected_devices();
let new_ids: HashSet<u32> = new_controllers
.iter()
.map(|controller| controller.pid as u32)
.collect();
let removed_devices: Vec<u32> = old_ids.difference(&new_ids).cloned().collect();
let connected_devices: Vec<u32> = new_ids.difference(&old_ids).cloned().collect();
*self.device_controllers.lock().unwrap() = new_controllers;
(removed_devices, connected_devices)
}
pub fn get_device_name(&self, id: u32) -> Option<String> {
let controllers = self.device_controllers.lock().unwrap();
controllers
.iter()
.find(|controller| controller.pid as u32 == id)
.map(|controller| controller.name.clone())
}
pub fn get_device_battery_level(&self, id: u32) -> Option<i32> {
let controllers = self.device_controllers.lock().unwrap();
let controller = controllers
.iter()
.find(|controller| controller.pid as u32 == id)?;
match controller.get_battery_level() {
Ok(level) => Some(level),
Err(err) => {
warn!("Failed to get battery level: {:?}", err);
None
}
}
}
pub fn is_device_charging(&self, id: u32) -> Option<bool> {
let controllers = self.device_controllers.lock().unwrap();
let controller = controllers
.iter()
.find(|controller| controller.pid as u32 == id)?;
match controller.get_charging_status() {
Ok(status) => Some(status),
Err(err) => {
warn!("Failed to get charging status: {:?}", err);
None
}
}
}
fn get_connected_devices(&self) -> Vec<DeviceController> {
let mut connected_devices = Vec::new();
let mut added_devices = HashSet::new();
for device in RAZER_DEVICE_LIST.iter() {
// Create a new HidApi instance
let api = match HidApi::new() {
Ok(api) => api,
Err(err) => {
warn!("Failed to initialize HidApi: {:?}", err);
continue;
}
};
// Iterate over the device list to find matching devices
for hid_device in api.device_list() {
if hid_device.vendor_id() == device.vid
&& hid_device.product_id() == device.pid
&& hid_device.interface_number() == device.interface.into()
{
// Check platform-specific usage if on Windows
if cfg!(target_os = "windows")
&& (hid_device.usage_page() != device.usage_page
|| hid_device.usage() != device.usage)
{
continue;
}
// Only add the device if it hasn't been added yet
if !added_devices.contains(&device.pid) {
// Create a new DeviceController
match DeviceController::new(
device.name.to_owned(),
device.pid,
hid_device.path().to_string_lossy().into_owned(),
) {
Ok(controller) => {
connected_devices.push(controller);
added_devices.insert(device.pid);
}
Err(err) => warn!("Failed to create device controller: {:?}", err),
}
}
}
}
}
connected_devices
}
}