Skip to main content

scdsu_core/
lib.rs

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