Skip to main content

scdsu_core/
reader.rs

1//! This module provides a 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/// Background reader that continuously parses frames from a device
22pub struct Reader {
23    handle: thread::JoinHandle<()>,
24}
25
26impl Reader {
27    /// Spawn a thread that reads from `device` and sends parsed frames over the returned channel.
28    /// Returns immediately, use `Reader::join` to join the reader thread.
29    /// Returns `Self` and a mpsc Receiver for the UDP server
30    pub fn start(
31        running: Arc<atomic::AtomicBool>,
32        device: impl device::Device + std::marker::Send + 'static,
33    ) -> (Self, mpsc::Receiver<DSUFrame>) {
34        let (tx, rx) = mpsc::channel::<DSUFrame>();
35
36        let handle = thread::spawn(move || {
37            let mut frame_state = FrameState::new();
38
39            log::debug!("Reader thread started");
40
41            while running.load(READ_ATOMIC_BOOL_ORDERING) {
42                if !read_frame(&device, &mut frame_state, &tx) {
43                    break;
44                }
45            }
46
47            log::debug!(
48                "Reader thread finished after {} frames",
49                frame_state.total_frames
50            );
51        });
52
53        (Self { handle }, rx)
54    }
55
56    /// Join the reader's thread, consuming `self`.
57    pub fn join(self) -> Result<(), Box<dyn std::any::Any + Send>> {
58        self.handle.join()
59    }
60}
61
62struct FrameState {
63    pub frozen_count: usize,
64    pub total_frames: usize,
65    pub prev_frame: Option<DSUFrame>,
66    pub fail_count: usize,
67    pub last_init_attempt: Option<Instant>,
68}
69
70impl FrameState {
71    pub fn new() -> Self {
72        Self {
73            frozen_count: 0,
74            total_frames: 0,
75            prev_frame: None,
76            fail_count: 0,
77            last_init_attempt: None,
78        }
79    }
80}
81
82/// Read a frame, returning true if another should be read.
83fn read_frame<D>(device: &D, frame_state: &mut FrameState, tx: &mpsc::Sender<DSUFrame>) -> bool
84where
85    D: device::Device + std::marker::Send + 'static,
86{
87    match device.read_frame() {
88        Ok(frame) => {
89            frame_state.fail_count = 0;
90            frame_state.total_frames += 1;
91
92            // Check for frozen/stale IMU data
93            // This is observed behavior when Steam disables the IMU on Steam devices
94            let is_imu_frozen = frame_state
95                .prev_frame
96                .map(|prev| {
97                    frame.accel_x == prev.accel_x
98                        && frame.accel_y == prev.accel_y
99                        && frame.accel_z == prev.accel_z
100                        && frame.gyro_x == prev.gyro_x
101                        && frame.gyro_y == prev.gyro_y
102                        && frame.gyro_z == prev.gyro_z
103                })
104                .unwrap_or(false);
105
106            let mut frame_to_send = frame;
107
108            if is_imu_frozen {
109                frame_state.frozen_count += 1;
110
111                if frame_state.frozen_count == FROZEN_DETECT_THRESHOLD {
112                    log::warn!(
113                        "IMU data frozen ({} identical frames). Steam likely disabled the IMU.",
114                        frame_state.frozen_count
115                    );
116                }
117
118                // Periodically attempt to re-enable the IMU
119                if frame_state.frozen_count >= FROZEN_DETECT_THRESHOLD {
120                    let should_try = frame_state
121                        .last_init_attempt
122                        .map(|t| t.elapsed() >= REINIT_RETRY_INTERVAL)
123                        .unwrap_or(true);
124                    if should_try {
125                        frame_state.last_init_attempt = Some(Instant::now());
126                        if let Err(e) = device.initialize() {
127                            log::warn!("Failed to reinitialize device while IMU frozen: {e}");
128                        } else {
129                            log::info!("Reinitialized device while IMU was frozen.");
130                        }
131                    }
132                }
133
134                // Zero out motion data so clients don't drift on stale values
135                frame_to_send.accel_x = 0.0;
136                frame_to_send.accel_y = 0.0;
137                frame_to_send.accel_z = 0.0;
138                frame_to_send.gyro_x = 0.0;
139                frame_to_send.gyro_y = 0.0;
140                frame_to_send.gyro_z = 0.0;
141            } else {
142                frame_state.frozen_count = 0;
143                frame_state.last_init_attempt = None;
144            }
145
146            frame_state.prev_frame = Some(frame);
147
148            if tx.send(frame_to_send).is_err() {
149                log::debug!("Receiver has hung up, reader thread exiting");
150                return false;
151            }
152        }
153        Err(DeviceError::ShortRead(n, expected)) => {
154            log::trace!("Short read: {} bytes (expected {})", n, expected);
155            frame_state.fail_count += 1;
156        }
157        Err(DeviceError::InvalidReport(id)) => {
158            log::trace!("Ignoring invalid report (first byte: 0x{:02x})", id);
159            frame_state.fail_count = 0;
160        }
161        Err(e) => {
162            log::trace!("HID read error: {}", e);
163            frame_state.fail_count += 1;
164        }
165    }
166
167    if frame_state.fail_count >= DISCONNECT_THRESHOLD {
168        log::warn!(
169            "Controller appears disconnected ({} consecutive read failures). Exiting reader.",
170            frame_state.fail_count,
171        );
172        return false;
173    }
174
175    true
176}