1#![doc(alias = "input")]
10#![doc(alias = "controller")]
11#![doc(alias = "gamepad")]
12
13use crate::Error;
14use crate::error::ResultCode;
15use crate::services::ServiceReference;
16use crate::services::svc::{HandleExt, make_ipc_header};
17use ctru_sys::{Handle, MEMPERM_READ, MEMPERM_READWRITE};
18use std::alloc::Layout;
19use std::ffi::CString;
20use std::ptr::slice_from_raw_parts;
21use std::sync::Mutex;
22
23static IR_USER_ACTIVE: Mutex<()> = Mutex::new(());
24static IR_USER_STATE: Mutex<Option<IrUserState>> = Mutex::new(None);
25
26pub struct IrUser {
29 _service_reference: ServiceReference,
30}
31
32struct IrUserState {
34 service_handle: Handle,
35 shared_memory_handle: Handle,
36 shared_memory: &'static [u8],
37 shared_memory_layout: Layout,
38 recv_buffer_size: usize,
39 recv_packet_count: usize,
40}
41
42const REQUIRE_CONNECTION_COMMAND_HEADER: u32 = make_ipc_header(6, 1, 0);
44const DISCONNECT_COMMAND_HEADER: u32 = make_ipc_header(9, 0, 0);
45const GET_RECEIVE_EVENT_COMMAND_HEADER: u32 = make_ipc_header(10, 0, 0);
46const GET_CONNECTION_STATUS_EVENT_COMMAND_HEADER: u32 = make_ipc_header(12, 0, 0);
47const SEND_IR_NOP_COMMAND_HEADER: u32 = make_ipc_header(13, 1, 2);
48const INITIALIZE_IRNOP_SHARED_COMMAND_HEADER: u32 = make_ipc_header(24, 6, 2);
49const RELEASE_RECEIVED_DATA_COMMAND_HEADER: u32 = make_ipc_header(25, 1, 0);
50
51const SHARED_MEM_INFO_SECTIONS_SIZE: usize = 0x30;
53const SHARED_MEM_RECV_BUFFER_OFFSET: usize = 0x20;
54const PAGE_SIZE: usize = 0x1000;
55const IR_BITRATE: u32 = 4;
56const PACKET_INFO_SIZE: usize = 8;
57const CIRCLE_PAD_PRO_INPUT_RESPONSE_PACKET_ID: u8 = 0x10;
58
59impl IrUser {
60 pub fn init(
64 recv_buffer_size: usize,
65 recv_packet_count: usize,
66 send_buffer_size: usize,
67 send_packet_count: usize,
68 ) -> crate::Result<Self> {
69 let service_reference = ServiceReference::new(
70 &IR_USER_ACTIVE,
71 || unsafe {
72 let mut service_handle = Handle::default();
74 let service_name = CString::new("ir:USER").unwrap();
75 ResultCode(ctru_sys::srvGetServiceHandle(
76 &mut service_handle,
77 service_name.as_ptr(),
78 ))?;
79
80 let minimum_shared_memory_len =
83 SHARED_MEM_INFO_SECTIONS_SIZE + recv_buffer_size + send_buffer_size;
84 let shared_memory_len = round_up(minimum_shared_memory_len, PAGE_SIZE);
85
86 let shared_memory_layout =
88 Layout::from_size_align(shared_memory_len, PAGE_SIZE).unwrap();
89 let shared_memory_ptr = std::alloc::alloc_zeroed(shared_memory_layout);
90 let shared_memory = &*slice_from_raw_parts(shared_memory_ptr, shared_memory_len);
91
92 let mut shared_memory_handle = Handle::default();
94 ResultCode(ctru_sys::svcCreateMemoryBlock(
95 &mut shared_memory_handle,
96 shared_memory_ptr as u32,
97 shared_memory_len as u32,
98 MEMPERM_READ,
99 MEMPERM_READWRITE,
100 ))?;
101
102 let request = vec![
104 INITIALIZE_IRNOP_SHARED_COMMAND_HEADER,
105 shared_memory_len as u32,
106 recv_buffer_size as u32,
107 recv_packet_count as u32,
108 send_buffer_size as u32,
109 send_packet_count as u32,
110 IR_BITRATE,
111 0,
112 shared_memory_handle,
113 ];
114 service_handle.send_service_request(request, 2)?;
115
116 let user_state = IrUserState {
118 service_handle,
119 shared_memory_handle,
120 shared_memory,
121 shared_memory_layout,
122 recv_buffer_size,
123 recv_packet_count,
124 };
125 let mut ir_user_state = IR_USER_STATE
126 .lock()
127 .map_err(|e| Error::Other(format!("Failed to write to IR_USER_STATE: {e}")))?;
128 *ir_user_state = Some(user_state);
129
130 Ok(())
131 },
132 || {
133 let mut shared_mem_guard = IR_USER_STATE
135 .lock()
136 .expect("Failed to write to IR_USER_STATE");
137 let Some(shared_mem) = shared_mem_guard.take() else {
138 return;
140 };
141
142 (move || unsafe {
143 ResultCode(ctru_sys::svcCloseHandle(shared_mem.service_handle))?;
145 ResultCode(ctru_sys::svcCloseHandle(shared_mem.shared_memory_handle))?;
146
147 std::alloc::dealloc(
149 shared_mem.shared_memory.as_ptr() as *mut u8,
150 shared_mem.shared_memory_layout,
151 );
152
153 Ok::<_, Error>(())
154 })()
155 .unwrap();
156 },
157 )?;
158
159 Ok(IrUser {
160 _service_reference: service_reference,
161 })
162 }
163
164 pub fn require_connection(&mut self, device_id: IrDeviceId) -> crate::Result<()> {
166 unsafe {
167 self.send_service_request(
168 vec![REQUIRE_CONNECTION_COMMAND_HEADER, device_id.get_id()],
169 2,
170 )?;
171 }
172 Ok(())
173 }
174
175 pub fn disconnect(&mut self) -> crate::Result<()> {
177 unsafe {
178 self.send_service_request(vec![DISCONNECT_COMMAND_HEADER], 2)?;
179 }
180 Ok(())
181 }
182
183 pub fn get_connection_status_event(&self) -> crate::Result<Handle> {
185 let response = unsafe {
186 self.send_service_request(vec![GET_CONNECTION_STATUS_EVENT_COMMAND_HEADER], 4)
187 }?;
188 let status_event = response[3] as Handle;
189
190 Ok(status_event)
191 }
192
193 pub fn get_recv_event(&self) -> crate::Result<Handle> {
195 let response =
196 unsafe { self.send_service_request(vec![GET_RECEIVE_EVENT_COMMAND_HEADER], 4) }?;
197 let recv_event = response[3] as Handle;
198
199 Ok(recv_event)
200 }
201
202 pub fn request_input_polling(&mut self, period_ms: u8) -> crate::Result<()> {
207 let ir_request: [u8; 3] = [1, period_ms, (period_ms + 2) << 2];
208 unsafe {
209 self.send_service_request(
210 vec![
211 SEND_IR_NOP_COMMAND_HEADER,
212 ir_request.len() as u32,
213 2 + (ir_request.len() << 14) as u32,
214 ir_request.as_ptr() as u32,
215 ],
216 2,
217 )?;
218 }
219
220 Ok(())
221 }
222
223 pub fn release_received_data(&mut self, packet_count: u32) -> crate::Result<()> {
226 unsafe {
227 self.send_service_request(vec![RELEASE_RECEIVED_DATA_COMMAND_HEADER, packet_count], 2)?;
228 }
229 Ok(())
230 }
231
232 pub fn process_shared_memory(&self, process_fn: impl FnOnce(&[u8])) {
234 let shared_mem_guard = IR_USER_STATE.lock().unwrap();
235 let shared_mem = shared_mem_guard.as_ref().unwrap();
236
237 process_fn(shared_mem.shared_memory);
238 }
239
240 pub fn get_status_info(&self) -> IrUserStatusInfo {
242 let shared_mem_guard = IR_USER_STATE.lock().unwrap();
243 let shared_mem = shared_mem_guard.as_ref().unwrap().shared_memory;
244
245 IrUserStatusInfo {
246 recv_err_result: i32::from_ne_bytes(shared_mem[0..4].try_into().unwrap()),
247 send_err_result: i32::from_ne_bytes(shared_mem[4..8].try_into().unwrap()),
248 connection_status: match shared_mem[8] {
249 0 => ConnectionStatus::Disconnected,
250 1 => ConnectionStatus::Connecting,
251 2 => ConnectionStatus::Connected,
252 n => ConnectionStatus::Unknown(n),
253 },
254 trying_to_connect_status: shared_mem[9],
255 connection_role: shared_mem[10],
256 machine_id: shared_mem[11],
257 unknown_field_1: shared_mem[12],
258 network_id: shared_mem[13],
259 unknown_field_2: shared_mem[14],
260 unknown_field_3: shared_mem[15],
261 }
262 }
263
264 pub fn get_packets(&self) -> Result<Vec<IrUserPacket>, String> {
266 let shared_mem_guard = IR_USER_STATE.lock().unwrap();
267 let user_state = shared_mem_guard.as_ref().unwrap();
268 let shared_mem = user_state.shared_memory;
269
270 let start_index = u32::from_ne_bytes(shared_mem[0x10..0x14].try_into().unwrap());
272 let valid_packet_count = u32::from_ne_bytes(shared_mem[0x18..0x1c].try_into().unwrap());
273
274 (0..valid_packet_count as usize)
276 .map(|i| {
277 let packet_index = (i + start_index as usize) % user_state.recv_packet_count;
279 let packet_info_offset =
280 SHARED_MEM_RECV_BUFFER_OFFSET + (packet_index * PACKET_INFO_SIZE);
281 let packet_info =
282 &shared_mem[packet_info_offset..packet_info_offset + PACKET_INFO_SIZE];
283
284 let offset_to_data_buffer =
285 u32::from_ne_bytes(packet_info[0..4].try_into().unwrap()) as usize;
286 let data_length =
287 u32::from_ne_bytes(packet_info[4..8].try_into().unwrap()) as usize;
288
289 let packet_info_section_size = user_state.recv_packet_count * PACKET_INFO_SIZE;
292 let header_size = SHARED_MEM_RECV_BUFFER_OFFSET + packet_info_section_size;
293 let data_buffer_size = user_state.recv_buffer_size - packet_info_section_size;
294 let packet_data = |idx| -> u8 {
295 let data_buffer_offset = offset_to_data_buffer + idx;
296 shared_mem[header_size + data_buffer_offset % data_buffer_size]
297 };
298
299 let (payload_length, payload_offset) = if packet_data(2) & 0x40 != 0 {
301 (
303 ((packet_data(2) as usize & 0x3F) << 8) + packet_data(3) as usize,
304 4,
305 )
306 } else {
307 ((packet_data(2) & 0x3F) as usize, 3)
309 };
310
311 if data_length != payload_offset + payload_length + 1 {
313 return Err(format!(
314 "Invalid payload length (expected {}, got {})",
315 data_length,
316 payload_offset + payload_length + 1
317 ));
318 }
319
320 let magic_number = packet_data(0);
322 if magic_number != 0xA5 {
323 return Err(format!(
324 "Invalid magic number in packet: {magic_number:#x}, expected 0xA5"
325 ));
326 }
327
328 Ok(IrUserPacket {
329 magic_number: packet_data(0),
330 destination_network_id: packet_data(1),
331 payload_length,
332 payload: (payload_offset..payload_offset + payload_length)
333 .map(packet_data)
334 .collect(),
335 checksum: packet_data(payload_offset + payload_length),
336 })
337 })
338 .collect()
339 }
340
341 unsafe fn send_service_request(
343 &self,
344 request: Vec<u32>,
345 expected_response_len: usize,
346 ) -> crate::Result<Vec<u32>> {
347 let mut shared_mem_guard = IR_USER_STATE.lock().unwrap();
348 let shared_mem = shared_mem_guard.as_mut().unwrap();
349
350 unsafe {
351 shared_mem
352 .service_handle
353 .send_service_request(request, expected_response_len)
354 }
355 }
356}
357
358fn round_up(value: usize, multiple: usize) -> usize {
360 if !value.is_multiple_of(multiple) {
361 (value / multiple) * multiple + multiple
362 } else {
363 (value / multiple) * multiple
364 }
365}
366
367pub enum IrDeviceId {
370 CirclePadPro,
372 Custom(u32),
375}
376
377impl IrDeviceId {
378 pub fn get_id(&self) -> u32 {
380 match *self {
381 IrDeviceId::CirclePadPro => 1,
382 IrDeviceId::Custom(id) => id,
383 }
384 }
385}
386
387#[derive(Debug)]
389pub struct IrUserStatusInfo {
390 pub recv_err_result: ctru_sys::Result,
392 pub send_err_result: ctru_sys::Result,
394 pub connection_status: ConnectionStatus,
396 pub trying_to_connect_status: u8,
398 pub connection_role: u8,
400 pub machine_id: u8,
402 pub unknown_field_1: u8,
404 pub network_id: u8,
406 pub unknown_field_2: u8,
408 pub unknown_field_3: u8,
410}
411
412#[repr(u8)]
414#[derive(Debug, PartialEq, Eq)]
415pub enum ConnectionStatus {
416 Disconnected = 0,
418 Connecting = 1,
420 Connected = 2,
422 Unknown(u8),
424}
425
426#[derive(Debug)]
428pub struct IrUserPacket {
429 pub magic_number: u8,
431 pub destination_network_id: u8,
433 pub payload_length: usize,
435 pub payload: Vec<u8>,
437 pub checksum: u8,
439}
440
441#[derive(Debug, Default)]
443pub struct CirclePadProInputResponse {
444 pub c_stick_x: u16,
446 pub c_stick_y: u16,
448 pub battery_level: u8,
450 pub zl_pressed: bool,
452 pub zr_pressed: bool,
454 pub r_pressed: bool,
456 pub unknown_field: u8,
458}
459
460impl TryFrom<&IrUserPacket> for CirclePadProInputResponse {
461 type Error = String;
462
463 fn try_from(packet: &IrUserPacket) -> Result<Self, Self::Error> {
464 if packet.payload.len() != 6 {
465 return Err(format!(
466 "Invalid payload length (expected 6 bytes, got {})",
467 packet.payload.len()
468 ));
469 }
470
471 let response_id = packet.payload[0];
472 if response_id != CIRCLE_PAD_PRO_INPUT_RESPONSE_PACKET_ID {
473 return Err(format!(
474 "Invalid response ID (expected {CIRCLE_PAD_PRO_INPUT_RESPONSE_PACKET_ID}, got {:#x}",
475 packet.payload[0]
476 ));
477 }
478
479 let c_stick_x = packet.payload[1] as u16 + (((packet.payload[2] & 0x0F) as u16) << 8);
480 let c_stick_y =
481 (((packet.payload[2] & 0xF0) as u16) >> 4) + ((packet.payload[3] as u16) << 4);
482 let battery_level = packet.payload[4] & 0x1F;
483 let zl_pressed = packet.payload[4] & 0x20 == 0;
484 let zr_pressed = packet.payload[4] & 0x40 == 0;
485 let r_pressed = packet.payload[4] & 0x80 == 0;
486 let unknown_field = packet.payload[5];
487
488 Ok(CirclePadProInputResponse {
489 c_stick_x,
490 c_stick_y,
491 battery_level,
492 zl_pressed,
493 zr_pressed,
494 r_pressed,
495 unknown_field,
496 })
497 }
498}