mirror of
https://github.com/xzeldon/razer-battery-report.git
synced 2025-07-16 05:34:36 +03:00
init
This commit is contained in:
209
src/controller.rs
Normal file
209
src/controller.rs
Normal 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
48
src/devices.rs
Normal 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
154
src/main.rs
Normal 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
131
src/manager.rs
Normal 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
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user