Skip to content
Open
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
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ members = [
"crates/buttplug_server_hwmgr_serial",
"crates/buttplug_server_hwmgr_websocket",
"crates/buttplug_server_hwmgr_xinput",
"crates/buttplug_server_hwmgr_sdl_gamepad",
"crates/buttplug_tests",
"crates/buttplug_transport_websocket_tungstenite",
"crates/intiface_engine",
Expand Down
31 changes: 31 additions & 0 deletions crates/buttplug_server_hwmgr_sdl_gamepad/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
[package]
name = "buttplug_server_hwmgr_sdl_gamepad"
version = "10.0.2"
authors = ["chiefautism"]
description = "Buttplug Hardware Manager - SDL2 Gamepad (cross-platform rumble for Xbox/PS/Switch controllers)"
license = "BSD-3-Clause"
homepage = "http://buttplug.io"
repository = "https://github.com/chiefautism/buttplug.git"
keywords = ["gamepad", "rumble", "haptics", "controller", "teledildonics"]
edition = "2024"

[lib]
name = "buttplug_server_hwmgr_sdl_gamepad"
path = "src/lib.rs"

[dependencies]
buttplug_core = { version = "10.0.2", path = "../buttplug_core", default-features = false }
buttplug_server = { version = "10.0.2", path = "../buttplug_server", default-features = false }
buttplug_server_device_config = { version = "10.0.3", path = "../buttplug_server_device_config" }
futures = "0.3.32"
futures-util = "0.3.32"
log = "0.4.29"
tokio = { version = "1.50.0", features = ["sync", "time"] }
async-trait = "0.1.89"
tracing = "0.1.44"
thiserror = "2.0.18"
sdl2 = { version = "0.37", features = ["bundled", "static-link"] }
byteorder = "1.5.0"
tokio-util = "0.7.18"
strum = "0.28.0"
strum_macros = "0.28.0"
21 changes: 21 additions & 0 deletions crates/buttplug_server_hwmgr_sdl_gamepad/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Buttplug SDL2 Gamepad Hardware Manager
//
// Cross-platform gamepad rumble/haptics support via SDL2.
// Works on macOS (GCController backend), Windows (XInput/DirectInput),
// and Linux (evdev) — all from a single codebase.
//
// Copyright 2026 chiefautism. BSD-3-Clause license.

#[macro_use]
extern crate log;

#[macro_use]
extern crate strum_macros;

mod sdl_gamepad_comm_manager;
mod sdl_gamepad_hardware;

pub use sdl_gamepad_comm_manager::{
SdlGamepadCommunicationManager,
SdlGamepadCommunicationManagerBuilder,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Buttplug SDL2 Gamepad Communication Manager
//
// Scans for connected game controllers via SDL2's GameController API.
// SDL2 types are !Send, so scanning runs on a dedicated thread.

use super::sdl_gamepad_hardware::SdlGamepadHardwareConnector;
use async_trait::async_trait;
use buttplug_core::errors::ButtplugDeviceError;
use buttplug_server::device::hardware::communication::{
HardwareCommunicationManager,
HardwareCommunicationManagerBuilder,
HardwareCommunicationManagerEvent,
TimedRetryCommunicationManager,
TimedRetryCommunicationManagerImpl,
};
use tokio::sync::mpsc;

#[derive(Default, Clone)]
pub struct SdlGamepadCommunicationManagerBuilder {}

impl HardwareCommunicationManagerBuilder for SdlGamepadCommunicationManagerBuilder {
fn finish(
&mut self,
sender: mpsc::Sender<HardwareCommunicationManagerEvent>,
) -> Box<dyn HardwareCommunicationManager> {
Box::new(TimedRetryCommunicationManager::new(
SdlGamepadCommunicationManager::new(sender),
))
}
}

pub struct SdlGamepadCommunicationManager {
sender: mpsc::Sender<HardwareCommunicationManagerEvent>,
}

impl SdlGamepadCommunicationManager {
fn new(sender: mpsc::Sender<HardwareCommunicationManagerEvent>) -> Self {
Self { sender }
}
}

/// Info about a discovered gamepad, sent from the SDL scan thread.
struct GamepadInfo {
joystick_index: u32,
name: String,
}

#[async_trait]
impl TimedRetryCommunicationManagerImpl for SdlGamepadCommunicationManager {
fn name(&self) -> &'static str {
"SdlGamepadCommunicationManager"
}

async fn scan(&self) -> Result<(), ButtplugDeviceError> {
trace!("SDL Gamepad manager scanning for devices");

// SDL types are !Send, so we scan on a dedicated std thread and send results back.
let (tx, rx) = std::sync::mpsc::channel::<GamepadInfo>();

std::thread::spawn(move || {
let sdl = match sdl2::init() {
Ok(s) => s,
Err(e) => {
error!("SDL init failed: {e}");
return;
}
};
let gc = match sdl.game_controller() {
Ok(gc) => gc,
Err(e) => {
error!("SDL GameController init failed: {e}");
return;
}
};
let num = gc.num_joysticks().unwrap_or(0);
for i in 0..num {
if !gc.is_game_controller(i) {
continue;
}
let name = gc.name_for_index(i).unwrap_or_else(|_| format!("Gamepad {i}"));
let _ = tx.send(GamepadInfo {
joystick_index: i,
name,
});
}
});

// Collect results (thread exits quickly after enumeration)
// Small delay to let the thread finish
tokio::time::sleep(std::time::Duration::from_millis(500)).await;

while let Ok(info) = rx.try_recv() {
let address = format!("sdl-gamepad-{}", info.joystick_index);
debug!("SDL Gamepad found: {} (index {})", info.name, info.joystick_index);

let device_creator = Box::new(SdlGamepadHardwareConnector::new(
info.joystick_index,
info.name.clone(),
));

if self
.sender
.send(HardwareCommunicationManagerEvent::DeviceFound {
name: info.name,
address,
creator: device_creator,
})
.await
.is_err()
{
error!("Error sending device found from SDL Gamepad manager.");
break;
}
}

Ok(())
}

fn can_scan(&self) -> bool {
true
}
}
Loading