More multi-controller fixes, better controller polling logic, clean up dead relay code

This commit is contained in:
DatCaptainHorse
2025-11-01 00:53:15 +02:00
parent a54cf759fa
commit 1d88a03b93
18 changed files with 1149 additions and 1289 deletions

View File

@@ -5079,9 +5079,9 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "vimputti"
version = "0.1.4"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffb370ee43e3ee4ca5329886e64dc5b27c83dc8cced5a63c2418777dac9a41a8"
checksum = "6440b3684270f355fb89193bfb0de957686119626b8b207f21d91024a892d05c"
dependencies = [
"anyhow",
"libc",

View File

@@ -22,7 +22,7 @@ rand = "0.9"
rustls = { version = "0.23", features = ["ring"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
vimputti = "0.1.4"
vimputti = "0.1.7"
chrono = "0.4"
prost = "0.14"
prost-types = "0.14"

View File

@@ -29,10 +29,8 @@ impl ControllerInput {
client: &vimputti::client::VimputtiClient,
) -> Result<Self> {
let config = controller_string_to_type(&controller_type)?;
Ok(Self {
config: config.clone(),
device: client.create_device(config).await?,
})
let device = client.create_device(config.clone()).await?;
Ok(Self { config, device })
}
pub fn device_mut(&mut self) -> &mut vimputti::client::VirtualController {
@@ -121,9 +119,60 @@ async fn command_loop(
slot.session_id == session_id && slot.session_slot == session_slot as u32
})
.map(|(slot_num, _)| *slot_num);
let slot = existing_slot.or_else(|| get_free_slot(&controllers));
if let Some(slot) = slot {
if let Some(existing_slot) = existing_slot {
if let Some(controller_slot) = controllers.get_mut(&existing_slot) {
let rumble_tx = rumble_tx.clone();
let attach_tx = attach_tx.clone();
controller_slot
.controller
.device_mut()
.on_rumble(move |strong, weak, duration_ms| {
let _ = rumble_tx.try_send((
existing_slot,
strong,
weak,
duration_ms,
data.session_id.clone(),
));
})
.await
.map_err(|e| {
tracing::warn!(
"Failed to register rumble callback for slot {}: {}",
existing_slot,
e
);
})
.ok();
// Return to attach_tx what slot was assigned
let attach_info = ProtoControllerAttach {
id: data.id.clone(),
session_slot: existing_slot as i32,
session_id: session_id.clone(),
};
match attach_tx.send(attach_info).await {
Ok(_) => {
tracing::info!(
"Controller {} re-attached to slot {} (session: {})",
data.id,
existing_slot,
session_id
);
}
Err(e) => {
tracing::error!(
"Failed to send re-attach info for slot {}: {}",
existing_slot,
e
);
}
}
}
} else if let Some(slot) = get_free_slot(&controllers) {
if let Ok(mut controller) =
ControllerInput::new(data.id.clone(), &vimputti_client).await
{
@@ -133,7 +182,13 @@ async fn command_loop(
controller
.device_mut()
.on_rumble(move |strong, weak, duration_ms| {
let _ = rumble_tx.try_send((slot, strong, weak, duration_ms, data.session_id.clone()));
let _ = rumble_tx.try_send((
slot,
strong,
weak,
duration_ms,
data.session_id.clone(),
));
})
.await
.map_err(|e| {
@@ -190,65 +245,10 @@ async fn command_loop(
if controllers.remove(&(data.session_slot as u32)).is_some() {
tracing::info!("Controller detached from slot {}", data.session_slot);
} else {
tracing::warn!("No controller found in slot {} to detach", data.session_slot);
}
}
Payload::ControllerButton(data) => {
if let Some(controller) = controllers.get(&(data.session_slot as u32)) {
if let Some(button) = vimputti::Button::from_ev_code(data.button as u16) {
let device = controller.controller.device();
device.button(button, data.pressed);
device.sync();
}
} else {
tracing::warn!("Controller slot {} not found for button event", data.session_slot);
}
}
Payload::ControllerStick(data) => {
if let Some(controller) = controllers.get(&(data.session_slot as u32)) {
let device = controller.controller.device();
if data.stick == 0 {
// Left stick
device.axis(vimputti::Axis::LeftStickX, data.x);
device.sync();
device.axis(vimputti::Axis::LeftStickY, data.y);
} else if data.stick == 1 {
// Right stick
device.axis(vimputti::Axis::RightStickX, data.x);
device.sync();
device.axis(vimputti::Axis::RightStickY, data.y);
}
device.sync();
} else {
tracing::warn!("Controller slot {} not found for stick event", data.session_slot);
}
}
Payload::ControllerTrigger(data) => {
if let Some(controller) = controllers.get(&(data.session_slot as u32)) {
let device = controller.controller.device();
if data.trigger == 0 {
// Left trigger
device.axis(vimputti::Axis::LowerLeftTrigger, data.value);
} else if data.trigger == 1 {
// Right trigger
device.axis(vimputti::Axis::LowerRightTrigger, data.value);
}
device.sync();
} else {
tracing::warn!("Controller slot {} not found for trigger event", data.session_slot);
}
}
Payload::ControllerAxis(data) => {
if let Some(controller) = controllers.get(&(data.session_slot as u32)) {
let device = controller.controller.device();
if data.axis == 0 {
// dpad x
device.axis(vimputti::Axis::DPadX, data.value);
} else if data.axis == 1 {
// dpad y
device.axis(vimputti::Axis::DPadY, data.value);
}
device.sync();
tracing::warn!(
"No controller found in slot {} to detach",
data.session_slot
);
}
}
Payload::ClientDisconnected(data) => {
@@ -274,6 +274,125 @@ async fn command_loop(
}
}
}
Payload::ControllerStateBatch(data) => {
if let Some(controller) = controllers.get(&(data.session_slot as u32)) {
let device = controller.controller.device();
// Handle inputs based on update type
if data.update_type == 0 {
// FULL_STATE: Update all values
let _ = device.sync().await;
for (btn_code, pressed) in data.button_changed_mask {
if let Some(button) = vimputti::Button::from_ev_code(btn_code as u16) {
let _ = device.button(button, pressed).await;
let _ = device.sync().await;
}
}
if let Some(x) = data.left_stick_x {
let _ = device.axis(vimputti::Axis::LeftStickX, x).await;
let _ = device.sync().await;
}
if let Some(y) = data.left_stick_y {
let _ = device.axis(vimputti::Axis::LeftStickY, y).await;
let _ = device.sync().await;
}
if let Some(x) = data.right_stick_x {
let _ = device.axis(vimputti::Axis::RightStickX, x).await;
let _ = device.sync().await;
}
if let Some(y) = data.right_stick_y {
let _ = device.axis(vimputti::Axis::RightStickY, y).await;
let _ = device.sync().await;
}
if let Some(value) = data.left_trigger {
let _ = device.axis(vimputti::Axis::LowerLeftTrigger, value).await;
let _ = device.sync().await;
}
if let Some(value) = data.right_trigger {
let _ = device.axis(vimputti::Axis::LowerRightTrigger, value).await;
let _ = device.sync().await;
}
if let Some(x) = data.dpad_x {
let _ = device.axis(vimputti::Axis::DPadX, x).await;
let _ = device.sync().await;
}
if let Some(y) = data.dpad_y {
let _ = device.axis(vimputti::Axis::DPadY, y).await;
let _ = device.sync().await;
}
} else {
// DELTA: Only update changed values
if let Some(changed_fields) = data.changed_fields {
let _ = device.sync().await;
if (changed_fields & (1 << 0)) != 0 {
for (btn_code, pressed) in data.button_changed_mask {
if let Some(button) =
vimputti::Button::from_ev_code(btn_code as u16)
{
let _ = device.button(button, pressed).await;
let _ = device.sync().await;
}
}
}
if (changed_fields & (1 << 1)) != 0 {
if let Some(x) = data.left_stick_x {
let _ = device.axis(vimputti::Axis::LeftStickX, x).await;
let _ = device.sync().await;
}
}
if (changed_fields & (1 << 2)) != 0 {
if let Some(y) = data.left_stick_y {
let _ = device.axis(vimputti::Axis::LeftStickY, y).await;
let _ = device.sync().await;
}
}
if (changed_fields & (1 << 3)) != 0 {
if let Some(x) = data.right_stick_x {
let _ = device.axis(vimputti::Axis::RightStickX, x).await;
let _ = device.sync().await;
}
}
if (changed_fields & (1 << 4)) != 0 {
if let Some(y) = data.right_stick_y {
let _ = device.axis(vimputti::Axis::RightStickY, y).await;
let _ = device.sync().await;
}
}
if (changed_fields & (1 << 5)) != 0 {
if let Some(value) = data.left_trigger {
let _ =
device.axis(vimputti::Axis::LowerLeftTrigger, value).await;
let _ = device.sync().await;
}
}
if (changed_fields & (1 << 6)) != 0 {
if let Some(value) = data.right_trigger {
let _ =
device.axis(vimputti::Axis::LowerRightTrigger, value).await;
let _ = device.sync().await;
}
}
if (changed_fields & (1 << 7)) != 0 {
if let Some(x) = data.dpad_x {
let _ = device.axis(vimputti::Axis::DPadX, x).await;
let _ = device.sync().await;
}
}
if (changed_fields & (1 << 8)) != 0 {
if let Some(y) = data.dpad_y {
let _ = device.axis(vimputti::Axis::DPadY, y).await;
let _ = device.sync().await;
}
}
}
}
} else {
tracing::warn!(
"Controller slot {} not found for state batch event",
data.session_slot
);
}
}
_ => {
//no-op
}

View File

@@ -102,77 +102,6 @@ pub struct ProtoControllerDetach {
#[prost(string, tag="2")]
pub session_id: ::prost::alloc::string::String,
}
/// ControllerButton message
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoControllerButton {
/// Session specific slot number (0-3)
#[prost(int32, tag="1")]
pub session_slot: i32,
/// Session ID of the client
#[prost(string, tag="2")]
pub session_id: ::prost::alloc::string::String,
/// Button code (linux input event code)
#[prost(int32, tag="3")]
pub button: i32,
/// true if pressed, false if released
#[prost(bool, tag="4")]
pub pressed: bool,
}
/// ControllerTriggers message
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoControllerTrigger {
/// Session specific slot number (0-3)
#[prost(int32, tag="1")]
pub session_slot: i32,
/// Session ID of the client
#[prost(string, tag="2")]
pub session_id: ::prost::alloc::string::String,
/// Trigger number (0 for left, 1 for right)
#[prost(int32, tag="3")]
pub trigger: i32,
/// trigger value (-32768 to 32767)
#[prost(int32, tag="4")]
pub value: i32,
}
/// ControllerSticks message
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoControllerStick {
/// Session specific slot number (0-3)
#[prost(int32, tag="1")]
pub session_slot: i32,
/// Session ID of the client
#[prost(string, tag="2")]
pub session_id: ::prost::alloc::string::String,
/// Stick number (0 for left, 1 for right)
#[prost(int32, tag="3")]
pub stick: i32,
/// X axis value (-32768 to 32767)
#[prost(int32, tag="4")]
pub x: i32,
/// Y axis value (-32768 to 32767)
#[prost(int32, tag="5")]
pub y: i32,
}
/// ControllerAxis message
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoControllerAxis {
/// Session specific slot number (0-3)
#[prost(int32, tag="1")]
pub session_slot: i32,
/// Session ID of the client
#[prost(string, tag="2")]
pub session_id: ::prost::alloc::string::String,
/// Axis number (0 for d-pad horizontal, 1 for d-pad vertical)
#[prost(int32, tag="3")]
pub axis: i32,
/// axis value (-1 to 1)
#[prost(int32, tag="4")]
pub value: i32,
}
/// ControllerRumble message
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
@@ -193,6 +122,86 @@ pub struct ProtoControllerRumble {
#[prost(int32, tag="5")]
pub duration: i32,
}
/// ControllerStateBatch - single message containing full or partial controller state
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoControllerStateBatch {
/// Session specific slot number (0-3)
#[prost(int32, tag="1")]
pub session_slot: i32,
/// Session ID of the client
#[prost(string, tag="2")]
pub session_id: ::prost::alloc::string::String,
#[prost(enumeration="proto_controller_state_batch::UpdateType", tag="3")]
pub update_type: i32,
/// Sequence number for packet loss detection
#[prost(uint32, tag="4")]
pub sequence: u32,
/// Button state map (Linux event codes)
#[prost(map="int32, bool", tag="5")]
pub button_changed_mask: ::std::collections::HashMap<i32, bool>,
/// Analog inputs
///
/// -32768 to 32767
#[prost(int32, optional, tag="6")]
pub left_stick_x: ::core::option::Option<i32>,
/// -32768 to 32767
#[prost(int32, optional, tag="7")]
pub left_stick_y: ::core::option::Option<i32>,
/// -32768 to 32767
#[prost(int32, optional, tag="8")]
pub right_stick_x: ::core::option::Option<i32>,
/// -32768 to 32767
#[prost(int32, optional, tag="9")]
pub right_stick_y: ::core::option::Option<i32>,
/// -32768 to 32767
#[prost(int32, optional, tag="10")]
pub left_trigger: ::core::option::Option<i32>,
/// -32768 to 32767
#[prost(int32, optional, tag="11")]
pub right_trigger: ::core::option::Option<i32>,
/// -1, 0, or 1
#[prost(int32, optional, tag="12")]
pub dpad_x: ::core::option::Option<i32>,
/// -1, 0, or 1
#[prost(int32, optional, tag="13")]
pub dpad_y: ::core::option::Option<i32>,
/// Bitmask indicating which fields have changed
/// Bit 0: button_changed_mask, Bit 1: left_stick_x, Bit 2: left_stick_y, etc.
#[prost(uint32, optional, tag="14")]
pub changed_fields: ::core::option::Option<u32>,
}
/// Nested message and enum types in `ProtoControllerStateBatch`.
pub mod proto_controller_state_batch {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
#[repr(i32)]
pub enum UpdateType {
/// Complete controller state
FullState = 0,
/// Only changed fields
Delta = 1,
}
impl UpdateType {
/// String value of the enum field names used in the ProtoBuf definition.
///
/// The values are not transformed in any way and thus are considered stable
/// (if the ProtoBuf definition does not change) and safe for programmatic use.
pub fn as_str_name(&self) -> &'static str {
match self {
UpdateType::FullState => "FULL_STATE",
UpdateType::Delta => "DELTA",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
match value {
"FULL_STATE" => Some(Self::FullState),
"DELTA" => Some(Self::Delta),
_ => None,
}
}
}
}
// WebRTC + signaling
#[allow(clippy::derive_partial_eq_without_eq)]
@@ -274,7 +283,7 @@ pub struct ProtoMessageBase {
pub struct ProtoMessage {
#[prost(message, optional, tag="1")]
pub message_base: ::core::option::Option<ProtoMessageBase>,
#[prost(oneof="proto_message::Payload", tags="2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24, 25")]
#[prost(oneof="proto_message::Payload", tags="2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 20, 21, 22, 23, 24, 25")]
pub payload: ::core::option::Option<proto_message::Payload>,
}
/// Nested message and enum types in `ProtoMessage`.
@@ -297,20 +306,15 @@ pub mod proto_message {
KeyDown(super::ProtoKeyDown),
#[prost(message, tag="8")]
KeyUp(super::ProtoKeyUp),
/// Controller input types
#[prost(message, tag="9")]
ControllerAttach(super::ProtoControllerAttach),
#[prost(message, tag="10")]
ControllerDetach(super::ProtoControllerDetach),
#[prost(message, tag="11")]
ControllerButton(super::ProtoControllerButton),
#[prost(message, tag="12")]
ControllerTrigger(super::ProtoControllerTrigger),
#[prost(message, tag="13")]
ControllerStick(super::ProtoControllerStick),
#[prost(message, tag="14")]
ControllerAxis(super::ProtoControllerAxis),
#[prost(message, tag="15")]
ControllerRumble(super::ProtoControllerRumble),
#[prost(message, tag="12")]
ControllerStateBatch(super::ProtoControllerStateBatch),
/// Signaling types
#[prost(message, tag="20")]
Ice(super::ProtoIce),