Skip to main content

scdsu_core/
reader.rs

1//! Provides a background reader for reading [`DSUFrame`](crate::dsu::DSUFrame) data from devices.
2
3use std::sync::{Arc, atomic, mpsc};
4use std::thread;
5use std::time::{Duration, Instant};
6
7use crate::READ_ATOMIC_BOOL_ORDERING;
8use crate::devices::Device;
9use crate::dsu::DSUFrame;
10use crate::errors::DeviceError;
11
12/// Number of identical IMU frames before we consider the IMU frozen.
13/// At 100 Hz this is 1 second
14const FROZEN_DETECT_THRESHOLD: usize = 100;
15/// Retry interval for re-initializing the device when the IMU is frozen
16const REINIT_RETRY_INTERVAL: Duration = Duration::from_secs(1);
17/// Number of consecutive failed reads before assuming disconnect.
18/// At 100Hz this is ~1 second of no data.
19const DISCONNECT_THRESHOLD: usize = 100;
20
21/// Spawn a thread that reads from `device` and sends parsed frames over the returned channel.
22///
23/// The reader thread will exit when `running` is set to false.
24/// Returns a [`JoinHandle`](std::thread::JoinHandle) and a mpsc Receiver for receiving frame data.
25pub fn spawn_reader(
26    running: Arc<atomic::AtomicBool>,
27    device: impl Device + std::marker::Send + 'static,
28) -> (std::thread::JoinHandle<()>, mpsc::Receiver<DSUFrame>) {
29    let (tx, rx) = mpsc::channel::<DSUFrame>();
30
31    let handle = thread::spawn(move || {
32        let mut frame_state = FrameState::new();
33
34        log::debug!("Reader thread started");
35
36        while running.load(READ_ATOMIC_BOOL_ORDERING) {
37            if !read_frame(&device, &mut frame_state, &tx) {
38                break;
39            }
40        }
41
42        log::debug!(
43            "Reader thread finished after {} frames",
44            frame_state.total_frames
45        );
46    });
47
48    (handle, rx)
49}
50
51struct FrameState {
52    pub frozen_count: usize,
53    pub total_frames: usize,
54    pub prev_frame: Option<DSUFrame>,
55    pub fail_count: usize,
56    pub last_init_attempt: Option<Instant>,
57}
58
59impl FrameState {
60    pub fn new() -> Self {
61        Self {
62            frozen_count: 0,
63            total_frames: 0,
64            prev_frame: None,
65            fail_count: 0,
66            last_init_attempt: None,
67        }
68    }
69}
70
71/// Read a frame, returning true if another should be read.
72fn read_frame<D>(device: &D, frame_state: &mut FrameState, tx: &mpsc::Sender<DSUFrame>) -> bool
73where
74    D: Device + std::marker::Send + 'static,
75{
76    match device.read_frame() {
77        Ok(frame) => {
78            frame_state.fail_count = 0;
79            frame_state.total_frames += 1;
80
81            // Check for frozen/stale IMU data
82            // This is observed behavior when Steam disables the IMU on Steam devices
83            let is_imu_frozen = frame_state
84                .prev_frame
85                .map(|prev| {
86                    frame.accel_x == prev.accel_x
87                        && frame.accel_y == prev.accel_y
88                        && frame.accel_z == prev.accel_z
89                        && frame.gyro_x == prev.gyro_x
90                        && frame.gyro_y == prev.gyro_y
91                        && frame.gyro_z == prev.gyro_z
92                })
93                .unwrap_or(false);
94
95            let mut frame_to_send = frame;
96
97            if is_imu_frozen {
98                frame_state.frozen_count += 1;
99
100                if frame_state.frozen_count == FROZEN_DETECT_THRESHOLD {
101                    log::warn!(
102                        "IMU data frozen ({} identical frames). Steam likely disabled the IMU.",
103                        frame_state.frozen_count
104                    );
105                }
106
107                // Periodically attempt to re-enable the IMU
108                if frame_state.frozen_count >= FROZEN_DETECT_THRESHOLD {
109                    let should_try = frame_state
110                        .last_init_attempt
111                        .map(|t| t.elapsed() >= REINIT_RETRY_INTERVAL)
112                        .unwrap_or(true);
113                    if should_try {
114                        frame_state.last_init_attempt = Some(Instant::now());
115                        if let Err(e) = device.initialize() {
116                            log::warn!("Failed to reinitialize device while IMU frozen: {e}");
117                        } else {
118                            log::info!("Reinitialized device while IMU was frozen.");
119                        }
120                    }
121                }
122
123                // Zero out motion data so clients don't drift on stale values
124                frame_to_send.accel_x = 0.0;
125                frame_to_send.accel_y = 0.0;
126                frame_to_send.accel_z = 0.0;
127                frame_to_send.gyro_x = 0.0;
128                frame_to_send.gyro_y = 0.0;
129                frame_to_send.gyro_z = 0.0;
130            } else {
131                frame_state.frozen_count = 0;
132                frame_state.last_init_attempt = None;
133            }
134
135            frame_state.prev_frame = Some(frame);
136
137            if tx.send(frame_to_send).is_err() {
138                log::debug!("Receiver has hung up, reader thread exiting");
139                return false;
140            }
141        }
142        Err(DeviceError::ShortRead(n, expected)) => {
143            log::trace!("Short read: {} bytes (expected {})", n, expected);
144            frame_state.fail_count += 1;
145        }
146        Err(DeviceError::InvalidReport(id)) => {
147            log::trace!("Ignoring invalid report (first byte: 0x{:02x})", id);
148            frame_state.fail_count = 0;
149        }
150        Err(e) => {
151            log::trace!("HID read error: {}", e);
152            frame_state.fail_count += 1;
153        }
154    }
155
156    if frame_state.fail_count >= DISCONNECT_THRESHOLD {
157        log::warn!(
158            "Controller appears disconnected ({} consecutive read failures). Exiting reader.",
159            frame_state.fail_count,
160        );
161        return false;
162    }
163
164    true
165}