Skip to main content

scdsu_core/
lib.rs

1//! Core library for `steam-controller-dsu`.
2//!
3//! This library crate provides the ability to run CemuHook (DSU) server which supplies controller
4//! input state over a UDP connection to various video game console emulators. The main focus is supporting
5//! Valve Steam Controller families.
6
7pub mod devices;
8pub mod dsu;
9pub mod errors;
10pub mod reader;
11pub mod server;
12
13pub use server::ServerConfig;
14
15use std::sync::Arc;
16use std::sync::atomic;
17use std::time::Duration;
18
19use crate::devices::{Device, DeviceFamily};
20use crate::errors::{DeviceError, ServerError};
21
22pub(crate) const READ_ATOMIC_BOOL_ORDERING: atomic::Ordering = atomic::Ordering::Relaxed;
23const CONTROLLER_OPEN_RETRY_DELAY_SEC: u64 = 5;
24
25/// Run the server loop until `running` is `false`.
26///
27/// Accepts an [`AtomicBool`](std::sync::atomic::AtomicBool) within an `Arc<>` for signaling when
28/// the server should shut down (set to `false`).
29///
30/// Accepts `config` and `device_config` for specifying [`ServerConfig`](server::ServerConfig) and
31/// [`DeviceConfig`](devices::DeviceConfig).
32///
33/// `family` specifies the [`DeviceFamily`](devices::DeviceFamily) to find and pass to
34/// [`spawn_reader`](reader::spawn_reader) for feeding the server device frames.
35pub fn run_server(
36    running: Arc<atomic::AtomicBool>,
37    config: server::ServerConfig,
38    device_config: devices::DeviceConfig,
39    family: DeviceFamily,
40) -> Result<(), ServerError> {
41    let mut api = hidapi::HidApi::new()?;
42
43    loop {
44        if !running.load(READ_ATOMIC_BOOL_ORDERING) {
45            return Ok(());
46        }
47
48        if let Err(e) = api.refresh_devices() {
49            log::warn!("Failed to refresh HID device list: {e}");
50        }
51
52        let Some(device) = open_controller_with_retry(
53            running.clone(),
54            &mut api,
55            device_config.clone(),
56            config.device_path.as_deref(),
57            family,
58        ) else {
59            // Interrupted by signal
60            return Ok(());
61        };
62
63        log::info!("Controller opened. Initializing...");
64        if let Err(e) = device.initialize() {
65            log::error!("Failed to initialize device: {e}");
66            sleep_interruptible(&running, Duration::from_secs(3));
67            continue;
68        }
69        log::info!(
70            "Device initialized. Starting CemuHook server on {}:{} ...",
71            config.bind_addr,
72            config.port
73        );
74
75        // Start the device reader and cemuhook udp server
76        //
77
78        let (reader_handle, rx) = reader::spawn_reader(running.clone(), device);
79
80        let udp_server = server::Server::new(running.clone(), config.clone())?;
81
82        match udp_server.run(rx) {
83            Ok((recv_result, send_result)) => {
84                if let Err(e) = recv_result {
85                    log::error!("UDP server receive loop error: {e}");
86                }
87                if let Err(err) = send_result {
88                    log::error!("UDP server send thread panicked: {err:?}");
89                }
90            }
91            Err(e) => {
92                log::error!("Failed to start the UDP server: {e}");
93            }
94        }
95
96        if let Err(err) = reader_handle.join() {
97            log::error!("Reader thread panicked: {err:?}");
98        }
99
100        if !running.load(READ_ATOMIC_BOOL_ORDERING) {
101            return Ok(());
102        }
103
104        log::info!("Server shut down. Waiting 3 seconds before reconnect...");
105        sleep_interruptible(&running, Duration::from_secs(3));
106    }
107}
108
109/// Runs a debug loop, dumping DSU-compatible frames to stdout for debugging purposes. Like
110/// [`run_server`], it runs until `running` is false.
111///
112/// Accepts an [`AtomicBool`](std::sync::atomic::AtomicBool) within an `Arc<>` for signaling when
113/// the server should shut down. Optionally accepts a `device_path` and `device_config`.
114///
115/// `family` specifies the [`DeviceFamily`](devices::DeviceFamily) to find and pass to
116/// [`spawn_reader`](reader::spawn_reader) for feeding the server device frames.
117pub fn run_debug_dump(
118    running: Arc<atomic::AtomicBool>,
119    device_path: Option<&str>,
120    device_config: Option<devices::DeviceConfig>,
121    family: DeviceFamily,
122) -> Result<(), DeviceError> {
123    let api = hidapi::HidApi::new()?;
124    let device_config = device_config.unwrap_or_default();
125
126    let device = open_device(family, device_config, &api, device_path)?;
127
128    log::info!("Controller opened. Running initialization...");
129    device.initialize()?;
130    log::info!("Initialized. Dumping frames...");
131
132    let (reader_handle, rx) = reader::spawn_reader(running.clone(), device);
133
134    while running.load(READ_ATOMIC_BOOL_ORDERING) {
135        match rx.recv() {
136            Ok(frame) => {
137                let buttons_pressed: Vec<&str> = [
138                    ("A", frame.a),
139                    ("B", frame.b),
140                    ("X", frame.x),
141                    ("Y", frame.y),
142                    ("L1", frame.l1),
143                    ("R1", frame.r1),
144                    ("L2", frame.l2),
145                    ("R2", frame.r2),
146                    ("L3", frame.l3),
147                    ("R3", frame.r3),
148                    ("Options", frame.options),
149                    ("Share", frame.share),
150                    ("Home", frame.home),
151                    ("QAM", frame.touch),
152                ]
153                .iter()
154                .filter(|(_, p)| *p)
155                .map(|(n, _)| *n)
156                .collect();
157
158                let dpad_pressed: Vec<&str> = [
159                    ("Up", frame.dpad_up),
160                    ("Down", frame.dpad_down),
161                    ("Left", frame.dpad_left),
162                    ("Right", frame.dpad_right),
163                ]
164                .iter()
165                .filter(|(_, p)| *p)
166                .map(|(n, _)| *n)
167                .collect();
168
169                let buttons_str = if buttons_pressed.is_empty() {
170                    "none".to_string()
171                } else {
172                    buttons_pressed.join(" ")
173                };
174                let dpad_str = if dpad_pressed.is_empty() {
175                    "none".to_string()
176                } else {
177                    dpad_pressed.join(" ")
178                };
179
180                println!(
181                    "Buttons: {buttons_str}\n\
182                     DPad:    {dpad_str}\n\
183                     Sticks:  L({:4},{:4})  R({:4},{:4})\n\
184                     Triggers: L2={:3}  R2={:3}\n\
185                     Accel:   ({:7.3},{:7.3},{:7.3}) g\n\
186                     Gyro:    ({:8.1},{:8.1},{:8.1}) dps",
187                    frame.left_stick_x,
188                    frame.left_stick_y,
189                    frame.right_stick_x,
190                    frame.right_stick_y,
191                    frame.analog_l2,
192                    frame.analog_r2,
193                    frame.accel_x,
194                    frame.accel_y,
195                    frame.accel_z,
196                    frame.gyro_x,
197                    frame.gyro_y,
198                    frame.gyro_z
199                );
200            }
201            Err(e) => {
202                log::error!("Recv error: {e}");
203                break;
204            }
205        }
206    }
207
208    drop(rx);
209    if let Err(err) = reader_handle.join() {
210        log::error!("Reader thread panicked: {err:?}");
211    }
212
213    log::info!("Debug dump finished.");
214    Ok(())
215}
216
217/// Open a controller of type `family` with unlimited retries.
218///
219/// Returns `None` if interrupted.
220fn open_controller_with_retry(
221    running: Arc<atomic::AtomicBool>,
222    api: &mut hidapi::HidApi,
223    device_config: devices::DeviceConfig,
224    device_path: Option<&str>,
225    family: DeviceFamily,
226) -> Option<Box<dyn devices::Device + Send>> {
227    loop {
228        if !running.load(READ_ATOMIC_BOOL_ORDERING) {
229            return None;
230        }
231
232        // Refresh devices in case any were dis/reconnected
233        if let Err(err) = api.refresh_devices() {
234            log::error!("HidApi failed to refresh_devices: {err:?}");
235            return None;
236        }
237
238        match open_device(family, device_config.clone(), api, device_path) {
239            Ok(d) => return Some(d),
240            Err(e) => {
241                log::warn!(
242                    "Failed to open controller: {e}. Retrying in {} seconds...",
243                    CONTROLLER_OPEN_RETRY_DELAY_SEC
244                );
245                sleep_interruptible(
246                    &running,
247                    Duration::from_secs(CONTROLLER_OPEN_RETRY_DELAY_SEC),
248                );
249            }
250        }
251    }
252}
253
254fn open_device(
255    family: DeviceFamily,
256    config: devices::DeviceConfig,
257    api: &hidapi::HidApi,
258    device_path: Option<&str>,
259) -> Result<Box<dyn devices::Device + Send>, DeviceError> {
260    match family {
261        DeviceFamily::Triton => Ok(Box::new(devices::triton::Triton::find(
262            config,
263            api,
264            device_path,
265        )?)),
266        DeviceFamily::Legacy => Ok(Box::new(devices::legacy::LegacySteamController::find(
267            config,
268            api,
269            device_path,
270        )?)),
271    }
272}
273
274/// Sleep in 100 ms increments while `running`.
275pub(crate) fn sleep_interruptible(running: &atomic::AtomicBool, total: Duration) {
276    let start = std::time::Instant::now();
277    while start.elapsed() < total {
278        if !running.load(READ_ATOMIC_BOOL_ORDERING) {
279            return;
280        }
281        std::thread::sleep(Duration::from_millis(100).min(total - start.elapsed()));
282    }
283}