Skip to main content

scdsu_core/
dsu.rs

1/// DSU frame representing all controller data sent over the CemuHook protocol.
2/// DSU protocol reference can be found [`here`](https://v1993.github.io/cemuhook-protocol/).
3#[derive(Debug, Clone, Copy, PartialEq)]
4pub struct DSUFrame {
5    pub dpad_left: bool,
6    pub dpad_down: bool,
7    pub dpad_right: bool,
8    pub dpad_up: bool,
9    pub options: bool,
10    pub r3: bool,
11    pub l3: bool,
12    pub share: bool,
13    pub y: bool,
14    pub b: bool,
15    pub a: bool,
16    pub x: bool,
17    pub r1: bool,
18    pub l1: bool,
19    pub r2: bool,
20    pub l2: bool,
21    pub home: bool,
22    pub touch: bool,
23    pub left_stick_x: u8,
24    pub left_stick_y: u8,
25    pub right_stick_x: u8,
26    pub right_stick_y: u8,
27    pub analog_r2: u8,
28    pub analog_l2: u8,
29    pub accel_x: f32,
30    pub accel_y: f32,
31    pub accel_z: f32,
32    pub gyro_x: f32,
33    pub gyro_y: f32,
34    pub gyro_z: f32,
35}
36
37/// Write the common CemuHook packet header into `buf`
38///
39/// `buf` must be at least 16 bytes
40/// The CRC32 field (bytes 8..12) is zeroed so the caller can compute it after
41/// filling the payload.
42fn write_header(buf: &mut [u8], payload_len: u16, client_id: u32) {
43    buf[0..4].copy_from_slice(b"DSUS");
44    buf[4..6].copy_from_slice(&1001u16.to_le_bytes());
45    buf[6..8].copy_from_slice(&payload_len.to_le_bytes());
46    buf[8..12].fill(0); // crc32 placeholder
47    buf[12..16].copy_from_slice(&client_id.to_le_bytes());
48}
49
50/// CRC32 used by the CemuHook protocol.
51/// Matches the algorithm from SteamDeckGyroDSU.
52fn crc32(data: &[u8]) -> u32 {
53    let mut crc: u32 = 0xFFFFFFFF;
54    for &byte in data {
55        crc ^= byte as u32;
56        for _ in 0..8 {
57            crc = if crc & 1 != 0 {
58                (crc >> 1) ^ 0xEDB8_8320
59            } else {
60                crc >> 1
61            };
62        }
63    }
64    !crc
65}
66
67/// Build a CemuHook protocol-version response packet into `buf`.
68/// `buf` must be at least 22 bytes.
69pub fn write_version_response(buf: &mut [u8], client_id: u32) {
70    write_header(buf, 2, client_id);
71    buf[16..20].copy_from_slice(&0x100000u32.to_le_bytes());
72    buf[20..22].copy_from_slice(&1001u16.to_le_bytes());
73
74    let c = crc32(&buf[..22]);
75    buf[8..12].copy_from_slice(&c.to_le_bytes());
76}
77
78/// Build a CemuHook controller-info response packet into `buf`.
79/// `buf` must be at least 32 bytes.
80pub fn write_info_response(buf: &mut [u8], slot: u8, client_id: u32, connected: bool) {
81    buf.fill(0);
82    write_header(buf, 16, client_id); // payload length = 32 - 16
83    buf[16..20].copy_from_slice(&0x100001u32.to_le_bytes());
84
85    // SharedResponse
86    buf[20] = slot;
87    if connected {
88        buf[21] = 2; // slotState = connected
89        buf[22] = 2; // deviceModel = full gyro
90        buf[23] = 1; // connection = USB
91    }
92    // Info response: byte 31 is a zero byte (not a connected flag).
93
94    let c = crc32(&buf[..32]);
95    buf[8..12].copy_from_slice(&c.to_le_bytes());
96}
97
98/// Build a CemuHook data-event packet (100 bytes) from a `DSUFrame`.
99pub fn write_data_event(
100    buf: &mut [u8; 100],
101    frame: &DSUFrame,
102    packet_num: u32,
103    client_id: u32,
104    slot: u8,
105    timestamp_us: u64,
106    invert_pitch: bool,
107) {
108    buf.fill(0);
109
110    write_header(buf, 84, client_id); // 100 - 16 = 84
111    buf[16..20].copy_from_slice(&0x100002u32.to_le_bytes());
112
113    // SharedResponse (11 bytes, offset 20)
114    buf[20] = slot; // slot requested by client
115    buf[21] = 2; // slotState = connected
116    buf[22] = 2; // deviceModel = full gyro
117    buf[23] = 1; // connection = USB
118    // mac1/mac2/battery already zero
119    buf[31] = 1; // connected
120
121    // packetNumber (offset 32)
122    buf[32..36].copy_from_slice(&packet_num.to_le_bytes());
123
124    // Buttons (offset 36)
125    buf[36] = get_bitmask(&[
126        (frame.dpad_left, 7),
127        (frame.dpad_down, 6),
128        (frame.dpad_right, 5),
129        (frame.dpad_up, 4),
130        (frame.options, 3),
131        (frame.r3, 2),
132        (frame.l3, 1),
133        (frame.share, 0),
134    ]);
135    buf[37] = get_bitmask(&[
136        (frame.y, 7),
137        (frame.b, 6),
138        (frame.a, 5),
139        (frame.x, 4),
140        (frame.r1, 3),
141        (frame.l1, 2),
142        (frame.r2, 1),
143        (frame.l2, 0),
144    ]);
145    buf[38] = u8::from(frame.home);
146    buf[39] = u8::from(frame.touch);
147
148    // Sticks (offset 40)
149    buf[40] = frame.left_stick_x;
150    buf[41] = frame.left_stick_y;
151    buf[42] = frame.right_stick_x;
152    buf[43] = frame.right_stick_y;
153
154    // Analog buttons (offset 44)
155    // Cemu reads these analog values even for digital buttons.
156    buf[44] = if frame.dpad_left { u8::MAX } else { 0 };
157    buf[45] = if frame.dpad_down { u8::MAX } else { 0 };
158    buf[46] = if frame.dpad_right { u8::MAX } else { 0 };
159    buf[47] = if frame.dpad_up { u8::MAX } else { 0 };
160    buf[48] = if frame.y { u8::MAX } else { 0 };
161    buf[49] = if frame.b { u8::MAX } else { 0 };
162    buf[50] = if frame.a { u8::MAX } else { 0 };
163    buf[51] = if frame.x { u8::MAX } else { 0 };
164    buf[52] = if frame.r1 { u8::MAX } else { 0 };
165    buf[53] = if frame.l1 { u8::MAX } else { 0 };
166    buf[54] = frame.analog_r2;
167    buf[55] = frame.analog_l2;
168
169    // Touch data (bytes 56-67) are already zeroed.
170
171    // MotionData timestamp (offset 68)
172    buf[68..76].copy_from_slice(&timestamp_us.to_le_bytes());
173
174    // Accelerometer in g (offset 76)
175    let acc_x = frame.accel_x;
176    let acc_y = if invert_pitch {
177        -frame.accel_y
178    } else {
179        frame.accel_y
180    };
181    let acc_z = frame.accel_z;
182
183    buf[76..80].copy_from_slice(&acc_x.to_le_bytes());
184    buf[80..84].copy_from_slice(&acc_y.to_le_bytes());
185    buf[84..88].copy_from_slice(&acc_z.to_le_bytes());
186
187    // Gyroscope in deg/s (offset 88)
188    let pitch = if invert_pitch {
189        -frame.gyro_x
190    } else {
191        frame.gyro_x
192    };
193
194    // when gravity reference is flipped with invert_pitch, this needs to be flipped too
195    let yaw = if invert_pitch {
196        -frame.gyro_y
197    } else {
198        frame.gyro_y
199    };
200
201    let roll = frame.gyro_z;
202
203    buf[88..92].copy_from_slice(&pitch.to_le_bytes());
204    buf[92..96].copy_from_slice(&yaw.to_le_bytes());
205    buf[96..100].copy_from_slice(&roll.to_le_bytes());
206
207    let c = crc32(&buf[..100]);
208    buf[8..12].copy_from_slice(&c.to_le_bytes());
209}
210
211/// Get a DSU button bitmask from a slice of bool and bit position pairs
212fn get_bitmask(bits: &[(bool, u8)]) -> u8 {
213    let mut mask = 0u8;
214    for &(on, pos) in bits {
215        if on {
216            mask |= 1u8 << pos;
217        }
218    }
219    mask
220}