Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion backend/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "controller-tools"
version = "0.3.0"
version = "0.4.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
Expand Down
15 changes: 15 additions & 0 deletions backend/src/api.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
mod bluetooth;
mod generic;
mod nintendo;
mod playstation;
mod xbox;
Expand Down Expand Up @@ -112,6 +114,19 @@ pub fn get_controllers() -> Result<Vec<Controller>> {
}
}

let mut unknown_controllers: Vec<_> = hidapi
.device_list()
.filter(|device_info| {
device_info.interface_number() == -1
&& !generic::IGNORED_VENDORS.contains(&device_info.vendor_id())
})
.collect();
unknown_controllers.dedup_by(|a, b| a.path() == b.path());
for device_info in unknown_controllers {
let controller = generic::get_controller_data(device_info, &hidapi)?;
controllers.push(controller);
}

// for Xbox over USB, hidapi-rs is not finding controllers so fall back to using udev
let mut enumerator = Enumerator::new()?;
enumerator.match_subsystem("usb")?;
Expand Down
61 changes: 61 additions & 0 deletions backend/src/api/bluetooth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
use anyhow::Result;
use hidapi::DeviceInfo;
use std::io::BufRead;
use std::{fs::File, io, path::Path, process::Command};

/// Get the bluetooth address from the DeviceInfo's hidraw,
/// e.g. "/sys/class/hidraw/hidraw5/device/uevent".
/// This file contains the BT address as value of HID_UNIQ
pub fn get_bluetooth_address(device_info: &DeviceInfo) -> Result<String> {
let mut bt_address = "".to_string();
let hidraw_path = device_info.path().to_str()?;
let prefix = hidraw_path.replace("/dev", "/sys/class/hidraw");
let path = [prefix, "device/uevent".to_string()].join("/");
let lines = read_lines(path)?;
for line in lines {
let val = line?;
// HID_UNIQ points to the BT address we want to use to grab data from bluetoothctl
if val.starts_with("HID_UNIQ") {
match val.split("=").skip(1).next() {
Some(address) => {
bt_address = address.to_string();
}
None => {}
}
}
}
Ok(bt_address.to_string())
}

/// For Xbox controllers, "bluetoothctl info <address>" will return info about the controller
/// including its battery percentage. This important output is:
/// "Battery Percentage: 0x42 (66)"
pub fn get_battery_percentage(address: String) -> Result<u8> {
let mut percentage = 0;
let output = Command::new("bluetoothctl")
.args(["info", address.as_str()])
.output()?;
let content = String::from_utf8_lossy(&output.stdout).to_string();
for bt_line in content.lines() {
if bt_line.contains("Battery Percentage") {
// format is: "Battery Percentage: 0x42 (66)"
match bt_line.split(" ").skip(2).next() {
Some(percentage_hex) => {
if let Ok(pct) = i64::from_str_radix(&percentage_hex[2..], 16) {
percentage = pct as u8;
}
}
None => {}
}
}
}
Ok(percentage)
}

fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
where
P: AsRef<Path>,
{
let file = File::open(filename)?;
Ok(io::BufReader::new(file).lines())
}
59 changes: 59 additions & 0 deletions backend/src/api/generic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use super::bluetooth::{get_battery_percentage, get_bluetooth_address};
use super::nintendo::VENDOR_ID_NINTENDO;
use super::playstation::DS_VENDOR_ID;
use super::xbox::MS_VENDOR_ID;

use anyhow::Result;
use hidapi::{DeviceInfo, HidApi};
use log::error;

use super::Controller;

const VALVE_VENDOR_ID: u16 = 0x28de;
const FOCALTECH_VENDOR_ID: u16 = 0x2808; // touchpad?
pub const IGNORED_VENDORS: [u16; 5] = [
VALVE_VENDOR_ID,
FOCALTECH_VENDOR_ID,
VENDOR_ID_NINTENDO,
DS_VENDOR_ID,
MS_VENDOR_ID,
];

pub fn get_controller_data(device_info: &DeviceInfo, _hidapi: &HidApi) -> Result<Controller> {
let bluetooth = device_info.interface_number() == -1;
// let device = device_info.open_device(hidapi)?;

let capacity: u8 = match get_bluetooth_address(device_info) {
Ok(address) => match get_battery_percentage(address) {
Ok(percentage) => percentage,
Err(err) => {
error!("get_battery_percentage failed because {}", err);
0
}
},
Err(err) => {
error!("get_bluetooth_address failed because {}", err);
0
}
};

let mut name = device_info
.product_string()
.unwrap_or("Unknown Controller")
.to_string();
if name.starts_with("Stadia") {
// product string is e.g. Stadia-CG9S-4e9f, this would be better
name = "Stadia Controller".to_string();
}

let controller = Controller {
name,
product_id: device_info.product_id(),
vendor_id: device_info.vendor_id(),
capacity,
status: "unknown".to_string(),
bluetooth,
};

Ok(controller)
}
68 changes: 2 additions & 66 deletions backend/src/api/xbox.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
use std::io::BufRead;
use std::{fs::File, io, path::Path, process::Command};

use super::bluetooth::{get_battery_percentage, get_bluetooth_address};
use anyhow::Result;
use hidapi::{DeviceInfo, HidApi};
use log::error;
// use serde::{Deserialize, Serialize};

use super::Controller;

Expand Down Expand Up @@ -51,55 +48,6 @@ pub fn get_xbox_controller(product_id: u16, bluetooth: bool) -> Result<Controlle
Ok(controller)
}

/// Get the bluetooth address from the DeviceInfo's hidraw,
/// e.g. "/sys/class/hidraw/hidraw5/device/uevent".
/// This file contains the BT address as value of HID_UNIQ
fn get_bluetooth_address(device_info: &DeviceInfo) -> Result<String> {
let mut bt_address = "".to_string();
let hidraw_path = device_info.path().to_str()?;
let prefix = hidraw_path.replace("/dev", "/sys/class/hidraw");
let path = [prefix, "device/uevent".to_string()].join("/");
let lines = read_lines(path)?;
for line in lines {
let val = line?;
// HID_UNIQ points to the BT address we want to use to grab data from bluetoothctl
if val.starts_with("HID_UNIQ") {
match val.split("=").skip(1).next() {
Some(address) => {
bt_address = address.to_string();
}
None => {}
}
}
}
Ok(bt_address.to_string())
}

/// For Xbox controllers, "bluetoothctl info <address>" will return info about the controller
/// including its battery percentage. This important output is:
/// "Battery Percentage: 0x42 (66)"
fn get_battery_percentage(address: String) -> Result<u8> {
let mut percentage = 0;
let output = Command::new("bluetoothctl")
.args(["info", address.as_str()])
.output()?;
let content = String::from_utf8_lossy(&output.stdout).to_string();
for bt_line in content.lines() {
if bt_line.contains("Battery Percentage") {
// format is: "Battery Percentage: 0x42 (66)"
match bt_line.split(" ").skip(2).next() {
Some(percentage_hex) => {
if let Ok(pct) = i64::from_str_radix(&percentage_hex[2..], 16) {
percentage = pct as u8;
}
}
None => {}
}
}
}
Ok(percentage)
}

pub fn parse_xbox_controller_data(
device_info: &DeviceInfo,
_hidapi: &HidApi,
Expand Down Expand Up @@ -132,21 +80,9 @@ pub fn parse_xbox_controller_data(
product_id: device_info.product_id(),
vendor_id: device_info.vendor_id(),
capacity,
status: if capacity > 0 {
"discharging".to_string()
} else {
"unknown".to_string()
},
status: "unknown".to_string(),
bluetooth,
};

Ok(controller)
}

fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
where
P: AsRef<Path>,
{
let file = File::open(filename)?;
Ok(io::BufReader::new(file).lines())
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ControllerTools",
"version": "0.3.0",
"version": "0.4.0",
"description": "The missing game controller menu. Displays the current battery % and charging status",
"scripts": {
"build": "shx rm -rf dist && rollup -c",
Expand Down
22 changes: 22 additions & 0 deletions release.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/bin/bash

rm -rf build
mkdir -p build/bin

npm run build
cp -r dist build/

cargo build --release --manifest-path backend/Cargo.toml
cp ./backend/target/release/controller-tools build/bin/backend

cp package.json build/package.json
cp plugin.json build/plugin.json
cp main.py build/main.py
cp README.md build/README.md
cp LICENSE build/LICENSE

mv build ControllerTools
VERSION=$(cat package.json| jq -r '.version')
rm -f controller-tools-$VERSION.zip
zip -r controller-tools-$VERSION.zip ControllerTools/*
mv ControllerTools build
3 changes: 3 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from "decky-frontend-lib";
import { useEffect, useState, VFC } from "react";
import { BiBluetooth, BiUsb } from "react-icons/bi";
import { SiStadia } from "react-icons/si";
import { RiSwitchLine } from "react-icons/ri";
import { FaBatteryEmpty, FaBatteryFull, FaBatteryQuarter, FaBatteryHalf, FaBatteryThreeQuarters, FaPlaystation, FaXbox } from "react-icons/fa";
import { BsController, BsBatteryCharging } from "react-icons/bs";
Expand Down Expand Up @@ -43,6 +44,8 @@ function getVendorIcon(controller: Controller): JSX.Element {
return <RiSwitchLine />;
case 1118:
return <FaXbox />
case 6353: // 0x18D1 = Google
return <SiStadia />
default:
return <BsController />;
}
Expand Down