This commit is contained in:
Timofey Gelazoniya 2024-08-27 21:08:03 +03:00
commit e25e4df3cf
Signed by: zeldon
GPG Key ID: 047886915281DD2A
10 changed files with 941 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

311
Cargo.lock generated Normal file
View File

@ -0,0 +1,311 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "cc"
version = "1.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6"
dependencies = [
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "env_logger"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580"
dependencies = [
"humantime",
"is-terminal",
"log",
"regex",
"termcolor",
]
[[package]]
name = "hermit-abi"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
[[package]]
name = "hidapi"
version = "2.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03b876ecf37e86b359573c16c8366bc3eba52b689884a0fc42ba3f67203d2a8b"
dependencies = [
"cc",
"cfg-if",
"libc",
"pkg-config",
"windows-sys 0.48.0",
]
[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "is-terminal"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b"
dependencies = [
"hermit-abi",
"libc",
"windows-sys 0.52.0",
]
[[package]]
name = "libc"
version = "0.2.158"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
[[package]]
name = "log"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "pkg-config"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
[[package]]
name = "pretty_env_logger"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "865724d4dbe39d9f3dd3b52b88d859d66bcb2d6a0acfd5ea68a65fb66d4bdc1c"
dependencies = [
"env_logger",
"log",
]
[[package]]
name = "razer-battery-report"
version = "0.1.0"
dependencies = [
"hidapi",
"log",
"pretty_env_logger",
]
[[package]]
name = "regex"
version = "1.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
[[package]]
name = "winapi-util"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"

20
Cargo.toml Normal file
View File

@ -0,0 +1,20 @@
[package]
name = "razer-battery-report"
version = "0.1.0"
authors = ["xzeldon <contact@zeldon.ru>"]
edition = "2021"
description = "Razer Battery Level Tray Indicator"
# Slower builds, faster executables
[profile.release]
lto = "fat"
codegen-units = 1
opt-level = 3
[dependencies]
# Communicate with HID devices
hidapi = "2.6.3"
# Logging
log = "0.4.22"
pretty_env_logger = "0.5.0"

20
LICENSE Normal file
View File

@ -0,0 +1,20 @@
Copyright 2024 Timofey Gelazoniya <contact@zeldon.ru>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

47
README.md Normal file
View File

@ -0,0 +1,47 @@
<h1 align="center">razer-battery-report</h1>
<p align="center">
<b>Razer Battery Level Tray¹ Indicator</b>
</p>
![stdout](/img/log.jpg)
Show your wireless Razer devices battery levels in your system tray¹.
> This is a work in progress and currently support only **Razer DeathAdder V3 Pro**.
> This works pretty well on **Windows**, should work on **Linux** if you *add udev rule to get access to usb devices* (see [here](https://github.com/libusb/hidapi/blob/master/udev/69-hid.rules)). But I haven't tested yet.
> ¹ — Tray feature coming soon
## Usage
### Downloading a Prebuilt Binary
> *Todo*
### Building from Source
To build, you must have [Rust](https://www.rust-lang.org/) and
[Git](https://git-scm.com/) installed on your system.
1. Clone this repository: `git clone https://github.com/xzeldon/razer-battery-report.git`
2. Navigate into your local repository: `cd razer-battery-report`
3. Build: `cargo build razer-battery-report --release`
4. Executable will be located at `target/release/razer-battery-report`
## Adding new devices yourself
* add device with `name`, `pid`, `interface`, `usage_page`, `usage` to [devices.rs](/src/devices.rs)
* add `transaction_id` to switch statement in `DeviceInfo` in [devices.rs](/src/devices.rs)
> You can grab `pid` and other data from the [openrazer](https://github.com/openrazer/openrazer/blob/352d13c416f42e572016c02fd10a52fc9848644a/driver/razermouse_driver.h#L9)
## Todo
- [ ] Tray Applet
- [ ] Prebuilt Binary
- [ ] Command Line Arguments for update frequency
- [ ] Support for other Razer Devices (I only have DeathAdder V3 Pro, so I won't be able to test it with other devices)
## Acknowledgments
* Linux Drivers for Razer devices: https://github.com/openrazer/openrazer
* This python script: https://github.com/spozer/razer-battery-checker
* 🖱️ Logitech Battery Level Tray Indicator (Elem): https://github.com/Fuwn/elem

BIN
img/log.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

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
}
}