feature: tray applet
This commit is contained in:
parent
14c87b57c8
commit
75b70514e1
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "razer-battery-report"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
authors = ["xzeldon <contact@zeldon.ru>"]
|
||||
edition = "2021"
|
||||
description = "Razer Battery Level Tray Indicator"
|
||||
|
@ -18,3 +18,10 @@ hidapi = "2.6.3"
|
|||
# Logging
|
||||
log = "0.4.22"
|
||||
pretty_env_logger = "0.5.0"
|
||||
|
||||
# Event Loop and Tray Icon
|
||||
tao = "0.30.0"
|
||||
tray-icon = "0.17.0"
|
||||
|
||||
# Image manipulation
|
||||
image = "0.25.2"
|
||||
|
|
15
README.md
15
README.md
|
@ -1,19 +1,19 @@
|
|||
<h1 align="center">razer-battery-report</h1>
|
||||
|
||||
<p align="center">
|
||||
<b>Razer Battery Level Tray¹ Indicator</b>
|
||||
<b>Razer Battery Level Tray Indicator</b>
|
||||
</p>
|
||||
|
||||
![stdout](/img/log.jpg)
|
||||
<p align="center">
|
||||
<img src="img/demo.png">
|
||||
</p>
|
||||
|
||||
Show your wireless Razer devices battery levels in your system tray¹.
|
||||
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
|
||||
|
@ -36,7 +36,10 @@ To build, you must have [Rust](https://www.rust-lang.org/) and
|
|||
> 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
|
||||
- [x] Tray Applet
|
||||
- [ ] Force update devices button in tray menu
|
||||
- [ ] Colored tray icons for different battery levels
|
||||
- [ ] Show log window button in tray menu
|
||||
- [ ] 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)
|
||||
|
|
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
After Width: | Height: | Size: 62 KiB |
|
@ -92,6 +92,7 @@ impl RazerReport {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DeviceController {
|
||||
pub handle: HidDevice,
|
||||
pub name: String,
|
||||
|
@ -104,9 +105,7 @@ 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
|
||||
|
@ -127,7 +126,6 @@ impl DeviceController {
|
|||
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)
|
||||
}
|
||||
|
||||
|
@ -135,7 +133,6 @@ impl DeviceController {
|
|||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
use watchman::Watchman;
|
||||
#![windows_subsystem = "windows"]
|
||||
|
||||
use tray::TrayApp;
|
||||
|
||||
mod controller;
|
||||
mod devices;
|
||||
mod manager;
|
||||
mod watchman;
|
||||
mod tray;
|
||||
|
||||
fn main() {
|
||||
std::env::set_var("RUST_LOG", "trace");
|
||||
pretty_env_logger::init();
|
||||
let checker = Watchman::new();
|
||||
let checker = TrayApp::new();
|
||||
checker.run();
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ use std::vec::Vec;
|
|||
use crate::controller::DeviceController;
|
||||
use crate::devices::RAZER_DEVICE_LIST;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DeviceManager {
|
||||
pub device_controllers: Arc<Mutex<Vec<DeviceController>>>,
|
||||
}
|
||||
|
|
|
@ -0,0 +1,239 @@
|
|||
use std::{
|
||||
collections::HashMap,
|
||||
rc::Rc,
|
||||
sync::{mpsc, Arc, Mutex},
|
||||
thread,
|
||||
};
|
||||
|
||||
use crate::manager::DeviceManager;
|
||||
use log::{error, info};
|
||||
use tao::event_loop::EventLoopBuilder;
|
||||
use tray_icon::{
|
||||
menu::{Menu, MenuEvent, MenuItem},
|
||||
TrayIcon, TrayIconBuilder,
|
||||
};
|
||||
|
||||
const BATTERY_UPDATE_INTERVAL: std::time::Duration = std::time::Duration::from_secs(60);
|
||||
const DEVICE_FETCH_INTERVAL: std::time::Duration = std::time::Duration::from_secs(5);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MemoryDevice {
|
||||
pub name: String,
|
||||
#[allow(unused)]
|
||||
pub pid: u32,
|
||||
pub battery_level: i32,
|
||||
pub old_battery_level: i32,
|
||||
pub is_charging: bool,
|
||||
}
|
||||
|
||||
impl MemoryDevice {
|
||||
fn new(name: String, pid: u32) -> Self {
|
||||
Self {
|
||||
name,
|
||||
pid,
|
||||
battery_level: -1,
|
||||
old_battery_level: 50,
|
||||
is_charging: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TrayApp {
|
||||
device_manager: Arc<Mutex<DeviceManager>>,
|
||||
devices: Arc<Mutex<HashMap<u32, MemoryDevice>>>,
|
||||
tray_icon: Rc<Mutex<Option<TrayIcon>>>,
|
||||
}
|
||||
|
||||
impl TrayApp {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
device_manager: Arc::new(Mutex::new(DeviceManager::new())),
|
||||
devices: Arc::new(Mutex::new(HashMap::new())),
|
||||
tray_icon: Rc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(&self) {
|
||||
let icon = Self::create_icon();
|
||||
let event_loop = EventLoopBuilder::new().build();
|
||||
let tray_menu = Self::create_menu();
|
||||
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
|
||||
self.spawn_device_fetch_thread(sender.clone());
|
||||
self.spawn_battery_check_thread(sender);
|
||||
|
||||
self.run_event_loop(event_loop, icon, tray_menu, receiver);
|
||||
}
|
||||
|
||||
fn create_icon() -> tray_icon::Icon {
|
||||
let icon = include_bytes!("../assets/mouse_white.png");
|
||||
let image = image::load_from_memory(icon)
|
||||
.expect("Failed to open icon")
|
||||
.into_rgba8();
|
||||
let (width, height) = image.dimensions();
|
||||
let rgba = image.into_raw();
|
||||
|
||||
tray_icon::Icon::from_rgba(rgba, width, height).expect("Failed to create icon")
|
||||
}
|
||||
|
||||
fn create_menu() -> Menu {
|
||||
let tray_menu = Menu::new();
|
||||
let quit_item = MenuItem::new("Exit", true, None);
|
||||
tray_menu.append_items(&[&quit_item]).unwrap();
|
||||
tray_menu
|
||||
}
|
||||
|
||||
fn spawn_device_fetch_thread(&self, tx: mpsc::Sender<Vec<u32>>) {
|
||||
let devices = Arc::clone(&self.devices);
|
||||
let device_manager = Arc::clone(&self.device_manager);
|
||||
|
||||
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 let std::collections::hash_map::Entry::Vacant(e) = devices.entry(id) {
|
||||
if let Some(name) = device_manager.lock().unwrap().get_device_name(id) {
|
||||
e.insert(MemoryDevice::new(name.clone(), id));
|
||||
info!("New device: {}", name);
|
||||
} else {
|
||||
error!("Failed to get device name for id: {}", id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !connected_devices.is_empty() {
|
||||
tx.send(connected_devices).unwrap();
|
||||
}
|
||||
|
||||
thread::sleep(DEVICE_FETCH_INTERVAL);
|
||||
});
|
||||
}
|
||||
|
||||
fn spawn_battery_check_thread(&self, tx: mpsc::Sender<Vec<u32>>) {
|
||||
let devices = Arc::clone(&self.devices);
|
||||
|
||||
thread::spawn(move || loop {
|
||||
let device_ids: Vec<u32> = devices.lock().unwrap().keys().cloned().collect();
|
||||
tx.send(device_ids).unwrap();
|
||||
thread::sleep(BATTERY_UPDATE_INTERVAL);
|
||||
});
|
||||
}
|
||||
|
||||
fn run_event_loop(
|
||||
&self,
|
||||
event_loop: tao::event_loop::EventLoop<()>,
|
||||
icon: tray_icon::Icon,
|
||||
tray_menu: Menu,
|
||||
rx: mpsc::Receiver<Vec<u32>>,
|
||||
) {
|
||||
let tray_icon = Rc::clone(&self.tray_icon);
|
||||
let devices = Arc::clone(&self.devices);
|
||||
let device_manager = Arc::clone(&self.device_manager);
|
||||
|
||||
let menu_channel = MenuEvent::receiver();
|
||||
|
||||
event_loop.run(move |event, _, control_flow| {
|
||||
*control_flow = tao::event_loop::ControlFlow::WaitUntil(
|
||||
std::time::Instant::now() + std::time::Duration::from_millis(100),
|
||||
);
|
||||
|
||||
if let Ok(device_ids) = rx.try_recv() {
|
||||
Self::update(&devices, &device_manager, &device_ids, &tray_icon);
|
||||
}
|
||||
|
||||
if let tao::event::Event::NewEvents(tao::event::StartCause::Init) = event {
|
||||
Self::create_tray_icon(&tray_icon, &tray_menu, icon.clone());
|
||||
}
|
||||
|
||||
if let Ok(event) = menu_channel.try_recv() {
|
||||
if event.id == tray_menu.items()[0].id() {
|
||||
*control_flow = tao::event_loop::ControlFlow::Exit;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn create_tray_icon(
|
||||
tray_icon: &Rc<Mutex<Option<TrayIcon>>>,
|
||||
tray_menu: &Menu,
|
||||
icon: tray_icon::Icon,
|
||||
) {
|
||||
let tray_builder = TrayIconBuilder::new()
|
||||
.with_menu(Box::new(tray_menu.clone()))
|
||||
.with_tooltip("Service is running")
|
||||
.with_icon(icon)
|
||||
.build();
|
||||
|
||||
match tray_builder {
|
||||
Ok(tray) => *tray_icon.lock().unwrap() = Some(tray),
|
||||
Err(err) => error!("Failed to create tray icon: {}", err),
|
||||
}
|
||||
}
|
||||
|
||||
fn update(
|
||||
devices: &Arc<Mutex<HashMap<u32, MemoryDevice>>>,
|
||||
manager: &Arc<Mutex<DeviceManager>>,
|
||||
device_ids: &[u32],
|
||||
tray_icon: &Rc<Mutex<Option<TrayIcon>>>,
|
||||
) {
|
||||
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), Some(is_charging)) = (
|
||||
manager.get_device_battery_level(id),
|
||||
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);
|
||||
|
||||
if let Some(tray_icon) = tray_icon.lock().unwrap().as_mut() {
|
||||
let _ = tray_icon
|
||||
.set_tooltip(Some(format!("{}: {}%", device.name, battery_level)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
144
src/watchman.rs
144
src/watchman.rs
|
@ -1,144 +0,0 @@
|
|||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, Mutex},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crate::manager::DeviceManager;
|
||||
use log::{error, info};
|
||||
|
||||
const BATTERY_UPDATE_INTERVAL: u64 = 60; // seconds
|
||||
const DEVICE_FETCH_INTERVAL: u64 = 5; // seconds
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MemoryDevice {
|
||||
pub name: String,
|
||||
#[allow(unused)]
|
||||
pub id: u32,
|
||||
pub battery_level: i32,
|
||||
pub old_battery_level: i32,
|
||||
pub 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Watchman {
|
||||
device_manager: Arc<Mutex<DeviceManager>>,
|
||||
devices: Arc<Mutex<HashMap<u32, MemoryDevice>>>,
|
||||
}
|
||||
|
||||
impl Watchman {
|
||||
pub fn new() -> Self {
|
||||
Watchman {
|
||||
device_manager: Arc::new(Mutex::new(DeviceManager::new())),
|
||||
devices: Arc::new(Mutex::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub 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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue