Initial POC
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
214
Cargo.lock
generated
Normal file
214
Cargo.lock
generated
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitvec"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
|
||||||
|
dependencies = [
|
||||||
|
"funty",
|
||||||
|
"radium",
|
||||||
|
"tap",
|
||||||
|
"wyz",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg_aliases"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "evdev"
|
||||||
|
version = "0.13.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "25b686663ba7f08d92880ff6ba22170f1df4e83629341cba34cf82cd65ebea99"
|
||||||
|
dependencies = [
|
||||||
|
"bitvec",
|
||||||
|
"cfg-if",
|
||||||
|
"futures-core",
|
||||||
|
"libc",
|
||||||
|
"nix",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "funty"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-core"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "keyboard_controller"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"evdev",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.186"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mio"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nix"
|
||||||
|
version = "0.29.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"cfg-if",
|
||||||
|
"cfg_aliases",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-lite"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.43"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "radium"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "socket2"
|
||||||
|
version = "0.6.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.114"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tap"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio"
|
||||||
|
version = "1.52.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"mio",
|
||||||
|
"pin-project-lite",
|
||||||
|
"socket2",
|
||||||
|
"tokio-macros",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-macros"
|
||||||
|
version = "2.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.61.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wyz"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
|
||||||
|
dependencies = [
|
||||||
|
"tap",
|
||||||
|
]
|
||||||
8
Cargo.toml
Normal file
8
Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[package]
|
||||||
|
name = "keyboard_controller"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
evdev = { version = "0.13.2", features = ["stream-trait"] }
|
||||||
|
tokio = { version = "1.52.1", features = ["macros", "rt", "sync", "time", "tokio-macros"] }
|
||||||
105
src/main.rs
Normal file
105
src/main.rs
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
mod virtual_gamepad;
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use evdev::{Device, EventType, KeyCode};
|
||||||
|
use tokio::sync::mpsc::{Receiver, Sender, channel};
|
||||||
|
use virtual_gamepad::VirtualGamepad;
|
||||||
|
|
||||||
|
async fn watch_keyboard(path: PathBuf, target: KeyCode, tx: Sender<PathBuf>) {
|
||||||
|
let device = Device::open(path.clone()).unwrap();
|
||||||
|
let Ok(mut stream) = device.into_event_stream() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
while let Ok(event) = stream.next_event().await {
|
||||||
|
if event.event_type() == EventType::KEY
|
||||||
|
&& event.code() == target.code()
|
||||||
|
&& event.value() == 0
|
||||||
|
{
|
||||||
|
let _ = tx.send(path).await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enumerate_keyboards() -> Vec<PathBuf> {
|
||||||
|
evdev::enumerate()
|
||||||
|
.filter_map(|(path, device)| {
|
||||||
|
if device.supported_keys()?.contains(KeyCode::KEY_SPACE) {
|
||||||
|
Some(path)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn find_target_keyboard(device_paths: &Vec<PathBuf>) -> Option<Device>{
|
||||||
|
|
||||||
|
let (tx, mut rx): (Sender<PathBuf>, Receiver<PathBuf>) = channel(1);
|
||||||
|
|
||||||
|
let mut handles = vec![];
|
||||||
|
|
||||||
|
for path in device_paths {
|
||||||
|
handles.push(tokio::spawn(watch_keyboard(
|
||||||
|
path.clone(),
|
||||||
|
KeyCode::KEY_SPACE,
|
||||||
|
tx.clone(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
drop(tx);
|
||||||
|
|
||||||
|
let path = rx.recv().await?;
|
||||||
|
|
||||||
|
for handle in handles {
|
||||||
|
handle.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
Device::open(path).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main(flavor = "local")]
|
||||||
|
async fn main() {
|
||||||
|
|
||||||
|
|
||||||
|
let devices_paths = enumerate_keyboards();
|
||||||
|
|
||||||
|
println!("Found the following devices");
|
||||||
|
for path in &devices_paths {
|
||||||
|
if let Ok(d) = Device::open(path){
|
||||||
|
println!("\t{} ({})", d.name().unwrap_or("null"), path.display())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let Some(kb) = find_target_keyboard(&devices_paths).await else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
let Ok(mut kb) = kb.into_event_stream() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut gamepad = VirtualGamepad::new("Virtual Gamepad 1");
|
||||||
|
|
||||||
|
if !kb.device_mut().grab().is_ok() {
|
||||||
|
println!("Failed to grab device");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Device is grabbed");
|
||||||
|
|
||||||
|
gamepad.handle_events(&mut kb).await;
|
||||||
|
|
||||||
|
match kb.device_mut().ungrab() {
|
||||||
|
Err(err) => println!("Failed to release device: {err}"),
|
||||||
|
_ => ()
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("Device released");
|
||||||
|
|
||||||
|
}
|
||||||
147
src/virtual_gamepad.rs
Normal file
147
src/virtual_gamepad.rs
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
use evdev::uinput::VirtualDevice;
|
||||||
|
use evdev::{
|
||||||
|
AbsInfo, AbsoluteAxisCode, AttributeSet, EventStream, EventType, InputEvent, KeyCode, UinputAbsSetup
|
||||||
|
};
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[derive(PartialEq, Debug)]
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum Direction {
|
||||||
|
Up = 0,
|
||||||
|
Down,
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct VirtualGamepad {
|
||||||
|
_name: String,
|
||||||
|
ui: VirtualDevice,
|
||||||
|
|
||||||
|
btn_bindings: HashMap<u16, KeyCode>,
|
||||||
|
|
||||||
|
dpad_bindings: HashMap<u16, Direction>,
|
||||||
|
|
||||||
|
dpad_state: [i8; 4],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VirtualGamepad {
|
||||||
|
pub fn new(name: &str) -> VirtualGamepad {
|
||||||
|
let mut keys = AttributeSet::<KeyCode>::new();
|
||||||
|
keys.insert(KeyCode::BTN_SOUTH);
|
||||||
|
keys.insert(KeyCode::BTN_EAST);
|
||||||
|
keys.insert(KeyCode::BTN_NORTH);
|
||||||
|
keys.insert(KeyCode::BTN_WEST);
|
||||||
|
|
||||||
|
keys.insert(KeyCode::BTN_TL);
|
||||||
|
keys.insert(KeyCode::BTN_TR);
|
||||||
|
keys.insert(KeyCode::BTN_TL2);
|
||||||
|
keys.insert(KeyCode::BTN_TR2);
|
||||||
|
|
||||||
|
keys.insert(KeyCode::BTN_START);
|
||||||
|
keys.insert(KeyCode::BTN_SELECT);
|
||||||
|
keys.insert(KeyCode::BTN_MODE);
|
||||||
|
|
||||||
|
keys.insert(KeyCode::BTN_THUMBL);
|
||||||
|
keys.insert(KeyCode::BTN_THUMBR);
|
||||||
|
|
||||||
|
let mut btn_bindings = HashMap::new();
|
||||||
|
|
||||||
|
btn_bindings.insert(KeyCode::KEY_J.code(), KeyCode::BTN_SOUTH);
|
||||||
|
btn_bindings.insert(KeyCode::KEY_I.code(), KeyCode::BTN_EAST);
|
||||||
|
btn_bindings.insert(KeyCode::KEY_O.code(), KeyCode::BTN_NORTH);
|
||||||
|
btn_bindings.insert(KeyCode::KEY_L.code(), KeyCode::BTN_WEST);
|
||||||
|
btn_bindings.insert(KeyCode::KEY_M.code(), KeyCode::BTN_TR);
|
||||||
|
btn_bindings.insert(KeyCode::KEY_K.code(), KeyCode::BTN_THUMBL);
|
||||||
|
btn_bindings.insert(KeyCode::KEY_SEMICOLON.code(), KeyCode::BTN_TL);
|
||||||
|
btn_bindings.insert(KeyCode::KEY_ENTER.code(), KeyCode::BTN_START);
|
||||||
|
btn_bindings.insert(KeyCode::KEY_ESC.code(), KeyCode::BTN_SELECT);
|
||||||
|
|
||||||
|
let mut dpad_bindings = HashMap::new();
|
||||||
|
|
||||||
|
dpad_bindings.insert(KeyCode::KEY_SPACE.code(), Direction::Up);
|
||||||
|
dpad_bindings.insert(KeyCode::KEY_E.code(), Direction::Down);
|
||||||
|
dpad_bindings.insert(KeyCode::KEY_F.code(), Direction::Right);
|
||||||
|
dpad_bindings.insert(KeyCode::KEY_W.code(), Direction::Left);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let ui = VirtualDevice::builder()
|
||||||
|
.unwrap()
|
||||||
|
.name(&name)
|
||||||
|
.with_keys(&keys)
|
||||||
|
.unwrap()
|
||||||
|
.with_absolute_axis(&UinputAbsSetup::new(
|
||||||
|
AbsoluteAxisCode::ABS_HAT0X,
|
||||||
|
AbsInfo::new(0, -1, 1, 0, 0, 0),
|
||||||
|
))
|
||||||
|
.unwrap()
|
||||||
|
.with_absolute_axis(&UinputAbsSetup::new(
|
||||||
|
AbsoluteAxisCode::ABS_HAT0Y,
|
||||||
|
AbsInfo::new(0, -1, 1, 0, 0, 0),
|
||||||
|
))
|
||||||
|
.unwrap()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
|
||||||
|
VirtualGamepad {
|
||||||
|
ui,
|
||||||
|
btn_bindings,
|
||||||
|
dpad_bindings,
|
||||||
|
_name: name.to_owned(),
|
||||||
|
dpad_state: [0, 0, 0, 0],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn press_btn(&mut self, btn: KeyCode, press: bool) {
|
||||||
|
let _ = self
|
||||||
|
.ui
|
||||||
|
.emit(&[InputEvent::new(EventType::KEY.0, btn.code(), press as i32)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn press_dpad(&mut self, btn: Direction, press: bool) {
|
||||||
|
match btn {
|
||||||
|
Direction::Up => self.dpad_state[0] = press as i8,
|
||||||
|
Direction::Down => self.dpad_state[1] = press as i8,
|
||||||
|
Direction::Left => self.dpad_state[2] = press as i8,
|
||||||
|
Direction::Right => self.dpad_state[3] = press as i8,
|
||||||
|
};
|
||||||
|
|
||||||
|
if [Direction::Up, Direction::Down].contains(&btn) {
|
||||||
|
let _ = self.ui.emit(&[InputEvent::new(
|
||||||
|
EventType::ABSOLUTE.0,
|
||||||
|
AbsoluteAxisCode::ABS_HAT0Y.0,
|
||||||
|
(-self.dpad_state[0] + self.dpad_state[1]) as i32,
|
||||||
|
)]);
|
||||||
|
} else {
|
||||||
|
let _ = self.ui.emit(&[InputEvent::new(
|
||||||
|
EventType::ABSOLUTE.0,
|
||||||
|
AbsoluteAxisCode::ABS_HAT0X.0,
|
||||||
|
(-self.dpad_state[2] + self.dpad_state[3]) as i32,
|
||||||
|
)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_events(&mut self, es: &mut EventStream) {
|
||||||
|
while let Ok(event) = es.next_event().await {
|
||||||
|
if event.event_type() != EventType::KEY {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(btn) = self.btn_bindings.get(&event.code()) {
|
||||||
|
self.press_btn(btn.clone(), event.value() > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(dir) = self.dpad_bindings.get(&event.code()) {
|
||||||
|
self.press_dpad(dir.clone(), event.value() > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.code() == KeyCode::KEY_END.code() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user