Skip to main content

scdsu_core/
lib.rs

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