Skip to main content

scdsu_core/
dsu.rs

1//! Provides functionality for working with DSU protocol data.
2
3/// DSU frame representing all controller data sent over the CemuHook protocol.
4/// DSU protocol reference can be found [`here`](https://v1993.github.io/cemuhook-protocol/).
5#[derive(Debug, Clone, Copy, PartialEq)]
6pub struct DSUFrame {
7    pub dpad_left: bool,
8    pub dpad_down: bool,
9    pub dpad_right: bool,
10    pub dpad_up: bool,
11    pub options: bool,
12    pub r3: bool,
13    pub l3: bool,
14    pub share: bool,
15    pub y: bool,
16    pub b: bool,
17    pub a: bool,
18    pub x: bool,
19    pub r1: bool,
20    pub l1: bool,
21    pub r2: bool,
22    pub l2: bool,
23    pub home: bool,
24    pub touch: bool,
25    pub left_stick_x: u8,
26    pub left_stick_y: u8,
27    pub right_stick_x: u8,
28    pub right_stick_y: u8,
29    pub analog_r2: u8,
30    pub analog_l2: u8,
31    pub raw_accel_x: f32,
32    pub raw_accel_y: f32,
33    pub raw_accel_z: f32,
34    pub raw_gyro_x: f32,
35    pub raw_gyro_y: f32,
36    pub raw_gyro_z: f32,
37    pub accel_x: f32,
38    pub accel_y: f32,
39    pub accel_z: f32,
40    pub gyro_x: f32,
41    pub gyro_y: f32,
42    pub gyro_z: f32,
43}
44
45/// Write the common CemuHook packet header into `buf`
46///
47/// `buf` must be at least 16 bytes
48/// The CRC32 field (bytes 8..12) is zeroed so the caller can compute it after
49/// filling the payload.
50fn write_header(buf: &mut [u8], payload_len: u16, client_id: u32) {
51    buf[0..4].copy_from_slice(b"DSUS");
52    buf[4..6].copy_from_slice(&1001u16.to_le_bytes());
53    buf[6..8].copy_from_slice(&payload_len.to_le_bytes());
54    buf[8..12].fill(0); // crc32 placeholder
55    buf[12..16].copy_from_slice(&client_id.to_le_bytes());
56}
57
58/// CRC32 used by the CemuHook protocol.
59/// Matches the algorithm from SteamDeckGyroDSU.
60fn crc32(data: &[u8]) -> u32 {
61    let mut crc: u32 = 0xFFFFFFFF;
62    for &byte in data {
63        crc ^= byte as u32;
64        for _ in 0..8 {
65            crc = if crc & 1 != 0 {
66                (crc >> 1) ^ 0xEDB8_8320
67            } else {
68                crc >> 1
69            };
70        }
71    }
72    !crc
73}
74
75/// Build a CemuHook protocol-version response packet into `buf`.
76/// `buf` must be at least 22 bytes.
77pub fn write_version_response(buf: &mut [u8], client_id: u32) {
78    write_header(buf, 2, client_id);
79    buf[16..20].copy_from_slice(&0x100000u32.to_le_bytes());
80    buf[20..22].copy_from_slice(&1001u16.to_le_bytes());
81
82    let c = crc32(&buf[..22]);
83    buf[8..12].copy_from_slice(&c.to_le_bytes());
84}
85
86/// Build a CemuHook controller-info response packet into `buf`.
87/// `buf` must be at least 32 bytes.
88pub fn write_info_response(buf: &mut [u8], slot: u8, client_id: u32, connected: bool) {
89    buf.fill(0);
90    write_header(buf, 16, client_id); // payload length = 32 - 16
91    buf[16..20].copy_from_slice(&0x100001u32.to_le_bytes());
92
93    // SharedResponse
94    buf[20] = slot;
95    if connected {
96        buf[21] = 2; // slotState = connected
97        buf[22] = 2; // deviceModel = full gyro
98        buf[23] = 1; // connection = USB
99    }
100    // Info response: byte 31 is a zero byte (not a connected flag).
101
102    let c = crc32(&buf[..32]);
103    buf[8..12].copy_from_slice(&c.to_le_bytes());
104}
105
106/// Build a CemuHook data-event packet (100 bytes) from a `DSUFrame`.
107pub fn write_data_event(
108    buf: &mut [u8; 100],
109    frame: &DSUFrame,
110    packet_num: u32,
111    client_id: u32,
112    slot: u8,
113    timestamp_us: u64,
114    invert_pitch: bool,
115) {
116    buf.fill(0);
117
118    write_header(buf, 84, client_id); // 100 - 16 = 84
119    buf[16..20].copy_from_slice(&0x100002u32.to_le_bytes());
120
121    // SharedResponse (11 bytes, offset 20)
122    buf[20] = slot; // slot requested by client
123    buf[21] = 2; // slotState = connected
124    buf[22] = 2; // deviceModel = full gyro
125    buf[23] = 1; // connection = USB
126    // mac1/mac2/battery already zero
127    buf[31] = 1; // connected
128
129    // packetNumber (offset 32)
130    buf[32..36].copy_from_slice(&packet_num.to_le_bytes());
131
132    // Buttons (offset 36)
133    buf[36] = get_bitmask(&[
134        (frame.dpad_left, 7),
135        (frame.dpad_down, 6),
136        (frame.dpad_right, 5),
137        (frame.dpad_up, 4),
138        (frame.options, 3),
139        (frame.r3, 2),
140        (frame.l3, 1),
141        (frame.share, 0),
142    ]);
143    buf[37] = get_bitmask(&[
144        (frame.y, 7),
145        (frame.b, 6),
146        (frame.a, 5),
147        (frame.x, 4),
148        (frame.r1, 3),
149        (frame.l1, 2),
150        (frame.r2, 1),
151        (frame.l2, 0),
152    ]);
153    buf[38] = u8::from(frame.home);
154    buf[39] = u8::from(frame.touch);
155
156    // Sticks (offset 40)
157    buf[40] = frame.left_stick_x;
158    buf[41] = frame.left_stick_y;
159    buf[42] = frame.right_stick_x;
160    buf[43] = frame.right_stick_y;
161
162    // Analog buttons (offset 44)
163    // Cemu reads these analog values even for digital buttons.
164    buf[44] = if frame.dpad_left { u8::MAX } else { 0 };
165    buf[45] = if frame.dpad_down { u8::MAX } else { 0 };
166    buf[46] = if frame.dpad_right { u8::MAX } else { 0 };
167    buf[47] = if frame.dpad_up { u8::MAX } else { 0 };
168    buf[48] = if frame.y { u8::MAX } else { 0 };
169    buf[49] = if frame.b { u8::MAX } else { 0 };
170    buf[50] = if frame.a { u8::MAX } else { 0 };
171    buf[51] = if frame.x { u8::MAX } else { 0 };
172    buf[52] = if frame.r1 { u8::MAX } else { 0 };
173    buf[53] = if frame.l1 { u8::MAX } else { 0 };
174    buf[54] = frame.analog_r2;
175    buf[55] = frame.analog_l2;
176
177    // Touch data (bytes 56-67) are already zeroed.
178
179    // MotionData timestamp (offset 68)
180    buf[68..76].copy_from_slice(&timestamp_us.to_le_bytes());
181
182    // Accelerometer in g (offset 76)
183    let acc_x = frame.accel_x;
184    let acc_y = if invert_pitch {
185        -frame.accel_y
186    } else {
187        frame.accel_y
188    };
189    let acc_z = frame.accel_z;
190
191    buf[76..80].copy_from_slice(&acc_x.to_le_bytes());
192    buf[80..84].copy_from_slice(&acc_y.to_le_bytes());
193    buf[84..88].copy_from_slice(&acc_z.to_le_bytes());
194
195    // Gyroscope in deg/s (offset 88)
196    let pitch = if invert_pitch {
197        -frame.gyro_x
198    } else {
199        frame.gyro_x
200    };
201
202    // when gravity reference is flipped with invert_pitch, this needs to be flipped too
203    let yaw = if invert_pitch {
204        -frame.gyro_y
205    } else {
206        frame.gyro_y
207    };
208
209    let roll = frame.gyro_z;
210
211    buf[88..92].copy_from_slice(&pitch.to_le_bytes());
212    buf[92..96].copy_from_slice(&yaw.to_le_bytes());
213    buf[96..100].copy_from_slice(&roll.to_le_bytes());
214
215    let c = crc32(&buf[..100]);
216    buf[8..12].copy_from_slice(&c.to_le_bytes());
217}
218
219/// Get a DSU button bitmask from a slice of bool and bit position pairs
220fn get_bitmask(bits: &[(bool, u8)]) -> u8 {
221    let mut mask = 0u8;
222    for &(on, pos) in bits {
223        if on {
224            mask |= 1u8 << pos;
225        }
226    }
227    mask
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    #[test]
235    fn test_get_bitmask_empty() {
236        assert_eq!(get_bitmask(&[]), 0);
237    }
238
239    #[test]
240    fn test_get_bitmask_single_bit() {
241        assert_eq!(get_bitmask(&[(true, 0)]), 0b00000001);
242        assert_eq!(get_bitmask(&[(true, 7)]), 0b10000000);
243    }
244
245    #[test]
246    fn test_get_bitmask_multiple_bits() {
247        assert_eq!(get_bitmask(&[(true, 0), (true, 1), (true, 2)]), 0b00000111);
248        assert_eq!(get_bitmask(&[(true, 0), (true, 7)]), 0b10000001);
249    }
250
251    #[test]
252    fn test_write_version_response() {
253        let mut buf = [0u8; 22];
254        write_version_response(&mut buf, 0x12345678);
255
256        assert_eq!(&buf[0..4], b"DSUS");
257        assert_eq!(u16::from_le_bytes([buf[4], buf[5]]), 1001);
258        assert_eq!(u16::from_le_bytes([buf[6], buf[7]]), 2);
259        assert_eq!(
260            u32::from_le_bytes([buf[12], buf[13], buf[14], buf[15]]),
261            0x12345678
262        );
263        assert_eq!(
264            u32::from_le_bytes([buf[16], buf[17], buf[18], buf[19]]),
265            0x100000
266        );
267        assert_eq!(u16::from_le_bytes([buf[20], buf[21]]), 1001);
268    }
269
270    #[test]
271    fn test_write_info_response_connected() {
272        let mut buf = [0u8; 32];
273        write_info_response(&mut buf, 1, 0xABCD_EF01, true);
274
275        assert_eq!(&buf[0..4], b"DSUS");
276        assert_eq!(u16::from_le_bytes([buf[6], buf[7]]), 16);
277        assert_eq!(
278            u32::from_le_bytes([buf[12], buf[13], buf[14], buf[15]]),
279            0xABCD_EF01
280        );
281        assert_eq!(buf[20], 1);
282        assert_eq!(buf[21], 2);
283        assert_eq!(buf[22], 2);
284        assert_eq!(buf[23], 1);
285    }
286
287    #[test]
288    fn test_write_info_response_disconnected() {
289        let mut buf = [0u8; 32];
290        write_info_response(&mut buf, 0, 0, false);
291
292        assert_eq!(&buf[0..4], b"DSUS");
293        assert_eq!(buf[20], 0);
294        assert_eq!(buf[21], 0);
295        assert_eq!(buf[22], 0);
296        assert_eq!(buf[23], 0);
297    }
298
299    fn create_test_frame() -> DSUFrame {
300        DSUFrame {
301            dpad_left: true,
302            dpad_down: false,
303            dpad_right: true,
304            dpad_up: false,
305            options: true,
306            r3: false,
307            l3: true,
308            share: false,
309            y: true,
310            b: false,
311            a: true,
312            x: false,
313            r1: true,
314            l1: false,
315            r2: true,
316            l2: false,
317            home: true,
318            touch: false,
319            left_stick_x: 128,
320            left_stick_y: 64,
321            right_stick_x: 200,
322            right_stick_y: 50,
323            analog_r2: 200,
324            analog_l2: 100,
325            raw_accel_x: 1.0,
326            raw_accel_y: 0.5,
327            raw_accel_z: -0.5,
328            raw_gyro_x: 10.0,
329            raw_gyro_y: -5.0,
330            raw_gyro_z: 2.5,
331            accel_x: 1.0,
332            accel_y: 0.5,
333            accel_z: -0.5,
334            gyro_x: 10.0,
335            gyro_y: -5.0,
336            gyro_z: 2.5,
337        }
338    }
339
340    #[test]
341    fn test_write_data_event_basic() {
342        let mut buf = [0u8; 100];
343        let frame = create_test_frame();
344        write_data_event(&mut buf, &frame, 12345, 0xDEAD_BEEF, 0, 1_000_000, false);
345
346        assert_eq!(&buf[0..4], b"DSUS");
347        assert_eq!(u16::from_le_bytes([buf[6], buf[7]]), 84);
348        assert_eq!(
349            u32::from_le_bytes([buf[12], buf[13], buf[14], buf[15]]),
350            0xDEAD_BEEF
351        );
352        assert_eq!(buf[20], 0);
353        assert_eq!(
354            u32::from_le_bytes([buf[32], buf[33], buf[34], buf[35]]),
355            12345
356        );
357    }
358
359    #[test]
360    fn test_write_data_event_buttons() {
361        let mut buf = [0u8; 100];
362        let frame = create_test_frame();
363        write_data_event(&mut buf, &frame, 0, 0, 0, 0, false);
364
365        assert_eq!(buf[36], 0b1010_1010);
366        assert_eq!(buf[37], 0b1010_1010);
367        assert_eq!(buf[38], 1);
368        assert_eq!(buf[39], 0);
369    }
370
371    #[test]
372    fn test_write_data_event_sticks() {
373        let mut buf = [0u8; 100];
374        let frame = create_test_frame();
375        write_data_event(&mut buf, &frame, 0, 0, 0, 0, false);
376
377        assert_eq!(buf[40], 128);
378        assert_eq!(buf[41], 64);
379        assert_eq!(buf[42], 200);
380        assert_eq!(buf[43], 50);
381    }
382
383    #[test]
384    fn test_write_data_event_analog_buttons() {
385        let mut buf = [0u8; 100];
386        let frame = create_test_frame();
387        write_data_event(&mut buf, &frame, 0, 0, 0, 0, false);
388
389        assert_eq!(buf[44], 255);
390        assert_eq!(buf[45], 0);
391        assert_eq!(buf[46], 255);
392        assert_eq!(buf[47], 0);
393        assert_eq!(buf[48], 255);
394        assert_eq!(buf[49], 0);
395        assert_eq!(buf[50], 255);
396        assert_eq!(buf[51], 0);
397        assert_eq!(buf[52], 255);
398        assert_eq!(buf[53], 0);
399        assert_eq!(buf[54], 200);
400        assert_eq!(buf[55], 100);
401    }
402
403    #[test]
404    fn test_write_data_event_timestamp() {
405        let mut buf = [0u8; 100];
406        let frame = create_test_frame();
407        write_data_event(&mut buf, &frame, 0, 0, 0, 9_876_543_210, false);
408
409        assert_eq!(
410            u64::from_le_bytes(buf[68..76].try_into().unwrap()),
411            9_876_543_210
412        );
413    }
414
415    #[test]
416    fn test_write_data_event_accel_normal() {
417        let mut buf = [0u8; 100];
418        let frame = create_test_frame();
419        write_data_event(&mut buf, &frame, 0, 0, 0, 0, false);
420
421        assert_eq!(f32::from_le_bytes(buf[76..80].try_into().unwrap()), 1.0);
422        assert_eq!(f32::from_le_bytes(buf[80..84].try_into().unwrap()), 0.5);
423        assert_eq!(f32::from_le_bytes(buf[84..88].try_into().unwrap()), -0.5);
424    }
425
426    #[test]
427    fn test_write_data_event_accel_inverted() {
428        let mut buf = [0u8; 100];
429        let frame = create_test_frame();
430        write_data_event(&mut buf, &frame, 0, 0, 0, 0, true);
431
432        assert_eq!(f32::from_le_bytes(buf[76..80].try_into().unwrap()), 1.0);
433        assert_eq!(f32::from_le_bytes(buf[80..84].try_into().unwrap()), -0.5);
434        assert_eq!(f32::from_le_bytes(buf[84..88].try_into().unwrap()), -0.5);
435    }
436
437    #[test]
438    fn test_write_data_event_gyro_normal() {
439        let mut buf = [0u8; 100];
440        let frame = create_test_frame();
441        write_data_event(&mut buf, &frame, 0, 0, 0, 0, false);
442
443        assert_eq!(f32::from_le_bytes(buf[88..92].try_into().unwrap()), 10.0);
444        assert_eq!(f32::from_le_bytes(buf[92..96].try_into().unwrap()), -5.0);
445        assert_eq!(f32::from_le_bytes(buf[96..100].try_into().unwrap()), 2.5);
446    }
447
448    #[test]
449    fn test_write_data_event_gyro_inverted() {
450        let mut buf = [0u8; 100];
451        let frame = create_test_frame();
452        write_data_event(&mut buf, &frame, 0, 0, 0, 0, true);
453
454        assert_eq!(f32::from_le_bytes(buf[88..92].try_into().unwrap()), -10.0);
455        assert_eq!(f32::from_le_bytes(buf[92..96].try_into().unwrap()), 5.0);
456        assert_eq!(f32::from_le_bytes(buf[96..100].try_into().unwrap()), 2.5);
457    }
458}