commit e25e4df3cf765593eab07df2042695dcfa1369a4 Author: xzeldon Date: Tue Aug 27 21:08:03 2024 +0300 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..09ae9ec --- /dev/null +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..25725c4 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "razer-battery-report" +version = "0.1.0" +authors = ["xzeldon "] +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" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..987aa3b --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +Copyright 2024 Timofey Gelazoniya + +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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..3ce76a9 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +

razer-battery-report

+ +

+ Razer Battery Level Tray¹ Indicator +

+ +![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 \ No newline at end of file diff --git a/img/log.jpg b/img/log.jpg new file mode 100644 index 0000000..e1de1de Binary files /dev/null and b/img/log.jpg differ diff --git a/src/controller.rs b/src/controller.rs new file mode 100644 index 0000000..d714f33 --- /dev/null +++ b/src/controller.rs @@ -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 { + 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 { + 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> { + 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> { + 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> { + 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> { + 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> { + 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> { + 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) + } +} diff --git a/src/devices.rs b/src/devices.rs new file mode 100644 index 0000000..d97ca2d --- /dev/null +++ b/src/devices.rs @@ -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, +]; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..629933c --- /dev/null +++ b/src/main.rs @@ -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>, + devices: Arc>>, +} + +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 = { + 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>>, + manager: &Arc>, + 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(); +} diff --git a/src/manager.rs b/src/manager.rs new file mode 100644 index 0000000..7835818 --- /dev/null +++ b/src/manager.rs @@ -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>>, +} + +impl DeviceManager { + pub fn new() -> Self { + Self { + device_controllers: Arc::new(Mutex::new(Vec::new())), + } + } + + pub fn fetch_devices(&mut self) -> (Vec, Vec) { + let old_ids: HashSet = { + 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 = new_controllers + .iter() + .map(|controller| controller.pid as u32) + .collect(); + + let removed_devices: Vec = old_ids.difference(&new_ids).cloned().collect(); + let connected_devices: Vec = 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 { + 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 { + 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 { + 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 { + 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 + } +}