init
This commit is contained in:
commit
e25e4df3cf
|
@ -0,0 +1 @@
|
|||
/target
|
|
@ -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"
|
|
@ -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"
|
|
@ -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.
|
|
@ -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
|
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
];
|
|
@ -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();
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue