ctru/services/
ir_user.rs

1//! IR (Infrared) User Service.
2//!
3//! The ir:USER service allows you to communicate with IR devices such as the Circle Pad Pro.
4//!
5//! The Circle Pad Pro (CPP) is an accessory for the 3DS which adds a second Circle Pad and extra shoulder buttons.
6//! On New 3DS systems, the ir:USER service uses the built-in C-stick and new shoulder buttons to emulate the Circle Pad
7//! Pro. Many released games which support the second stick and extra shoulder buttons use this service to communicate
8//! so they can  support both Old 3DS + CPP and New 3DS.
9#![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
26/// The "ir:USER" service. This service is used to talk to IR devices such as
27/// the Circle Pad Pro.
28pub struct IrUser {
29    _service_reference: ServiceReference,
30}
31
32// We need to hold on to some extra service state, hence this struct.
33struct 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
42// ir:USER syscall command headers
43const 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
51// Misc constants
52const 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    /// Initialize the ir:USER service. The provided buffer sizes and packet
61    /// counts are used to calculate the size of shared memory used by the
62    /// service.
63    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                // Get the ir:USER service handle
73                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                // Calculate the shared memory size.
81                // Shared memory length must be a multiple of the page size.
82                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                // Allocate the shared memory
87                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                // Mark the memory as shared
93                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                // Initialize the ir:USER service with the shared memory
103                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                // Set up our service state
117                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                // Remove our service state from the global location
134                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                    // If we don't have any state, then we don't need to clean up.
139                    return;
140                };
141
142                (move || unsafe {
143                    // Close service and memory handles
144                    ResultCode(ctru_sys::svcCloseHandle(shared_mem.service_handle))?;
145                    ResultCode(ctru_sys::svcCloseHandle(shared_mem.shared_memory_handle))?;
146
147                    // Free shared memory
148                    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    /// Try to connect to the device with the provided ID.
165    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    /// Close the current IR connection.
176    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    /// Get an event handle that activates on connection status changes.
184    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    /// Get an event handle that activates when a packet is received.
194    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    /// Circle Pad Pro specific request.
203    ///
204    /// This will send a packet to the CPP requesting it to send back packets
205    /// with the current device input values.
206    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    /// Mark the last `packet_count` packets as processed, so their memory in
224    /// the receive buffer can be reused.
225    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    /// This will let you directly read the ir:USER shared memory via a callback.
233    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    /// Read and parse the ir:USER service status data from shared memory.
241    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    /// Read and parse the current packets received from the IR device.
265    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        // Find where the packets are, and how many
271        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        // Parse the packets
275        (0..valid_packet_count as usize)
276            .map(|i| {
277                // Get the packet info
278                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                // Find the packet data. The packet data may wrap around the buffer end, so
290                // `packet_data` is a function from packet byte offset to value.
291                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                // Find out how long the payload is (payload length is variable-length encoded)
300                let (payload_length, payload_offset) = if packet_data(2) & 0x40 != 0 {
301                    // Big payload
302                    (
303                        ((packet_data(2) as usize & 0x3F) << 8) + packet_data(3) as usize,
304                        4,
305                    )
306                } else {
307                    // Small payload
308                    ((packet_data(2) & 0x3F) as usize, 3)
309                };
310
311                // Check our payload length math against what the packet info contains
312                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                // IR packets start with a magic number, so double check it
321                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    /// Internal helper for calling ir:USER service methods.
342    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
358// Internal helper for rounding up a value to a multiple of another value.
359fn 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
367/// An enum which represents the different IR devices the 3DS can connect to via
368/// the ir:USER service.
369pub enum IrDeviceId {
370    /// Circle Pad Pro
371    CirclePadPro,
372    /// Other devices
373    // Pretty sure no other IDs are recognized, but just in case
374    Custom(u32),
375}
376
377impl IrDeviceId {
378    /// Get the ID of the device.
379    pub fn get_id(&self) -> u32 {
380        match *self {
381            IrDeviceId::CirclePadPro => 1,
382            IrDeviceId::Custom(id) => id,
383        }
384    }
385}
386
387/// This struct holds a parsed copy of the ir:USER service status (from shared memory).
388#[derive(Debug)]
389pub struct IrUserStatusInfo {
390    /// The result of the last receive operation.
391    pub recv_err_result: ctru_sys::Result,
392    /// The result of the last send operation.
393    pub send_err_result: ctru_sys::Result,
394    /// The current connection status.
395    pub connection_status: ConnectionStatus,
396    /// The status of the connection attempt.
397    pub trying_to_connect_status: u8,
398    /// The role of the device in the connection (value meaning is unknown).
399    pub connection_role: u8,
400    /// The machine ID of the device.
401    pub machine_id: u8,
402    /// Unknown field.
403    pub unknown_field_1: u8,
404    /// The network ID of the connection.
405    pub network_id: u8,
406    /// Unknown field.
407    pub unknown_field_2: u8,
408    /// Unknown field.
409    pub unknown_field_3: u8,
410}
411
412/// Connection status values for [`IrUserStatusInfo`].
413#[repr(u8)]
414#[derive(Debug, PartialEq, Eq)]
415pub enum ConnectionStatus {
416    /// Device is not connected
417    Disconnected = 0,
418    /// Waiting for device to connect
419    Connecting = 1,
420    /// Device is connected
421    Connected = 2,
422    /// Unknown connection status
423    Unknown(u8),
424}
425
426/// A packet of data sent/received to/from the IR device.
427#[derive(Debug)]
428pub struct IrUserPacket {
429    /// The magic number of the packet. Should always be 0xA5.
430    pub magic_number: u8,
431    /// The destination network ID.
432    pub destination_network_id: u8,
433    /// The length of the payload.
434    pub payload_length: usize,
435    /// The payload data.
436    pub payload: Vec<u8>,
437    /// The checksum of the packet.
438    pub checksum: u8,
439}
440
441/// Circle Pad Pro response packet holding the current device input signals and status.
442#[derive(Debug, Default)]
443pub struct CirclePadProInputResponse {
444    /// The X value of the C-stick.
445    pub c_stick_x: u16,
446    /// The Y value of the C-stick.
447    pub c_stick_y: u16,
448    /// The battery level of the Circle Pad Pro.
449    pub battery_level: u8,
450    /// Whether the ZL button is pressed.
451    pub zl_pressed: bool,
452    /// Whether the ZR button is pressed.
453    pub zr_pressed: bool,
454    /// Whether the R button is pressed.
455    pub r_pressed: bool,
456    /// Unknown field.
457    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}