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