diff --git a/ctru-rs/examples/ir-user.rs b/ctru-rs/examples/ir-user.rs index d0c1abd..2b04260 100644 --- a/ctru-rs/examples/ir-user.rs +++ b/ctru-rs/examples/ir-user.rs @@ -1,3 +1,5 @@ +//! A demo of using the ir:USER service to connect to the Circle Pad Pro. + use ctru::prelude::*; use ctru::services::ir_user::{CirclePadProInputResponse, IrDeviceId, IrUser}; use std::io::Write; @@ -37,6 +39,7 @@ fn main() { bottom_console.select(); }; + // Get event handles let conn_status_event = ir_user .get_connection_status_event() .expect("Couldn't get ir:USER connection status event"); @@ -49,6 +52,7 @@ fn main() { 'main_loop: while apt.main_loop() { hid.scan_input(); + // Check if we need to exit if hid.keys_held().contains(KeyPad::KEY_START) { break; } @@ -59,6 +63,7 @@ fn main() { handle_packet(&ir_user, &top_console, &bottom_console); } + // Check if we should start the connection if hid.keys_down().contains(KeyPad::KEY_A) && !is_connected { println!("Attempting to connect to the CPP"); @@ -69,6 +74,7 @@ fn main() { break 'main_loop; } + // Start the connection process ir_user .require_connection(IrDeviceId::CirclePadPro) .expect("Couldn't initialize circle pad pro connection"); @@ -83,12 +89,12 @@ fn main() { } print_status_info(); - let status_info = ir_user.get_status_info(); - if status_info.connection_status == 2 { + if ir_user.get_status_info().connection_status == 2 { println!("Connected!"); break; } + // If not connected (ex. timeout), disconnect so we can retry ir_user .disconnect() .expect("Failed to disconnect circle pad pro connection"); @@ -110,11 +116,13 @@ fn main() { break 'main_loop; } - if let Err(e) = ir_user.start_polling_input(CPP_CONNECTION_POLLING_PERIOD_MS) { + // Send a request for input to the CPP + if let Err(e) = ir_user.request_input_polling(CPP_CONNECTION_POLLING_PERIOD_MS) { println!("Error: {e:?}"); } print_status_info(); + // Wait for the response let recv_event_result = IrUser::wait_for_event(recv_event, Duration::from_millis(100)); print_status_info(); @@ -124,6 +132,8 @@ fn main() { handle_packet(&ir_user, &top_console, &bottom_console); break; } + + // We didn't get a response in time, so loop and retry } is_connected = true; @@ -172,7 +182,7 @@ fn handle_packet(ir_user: &IrUser, top_console: &Console, bottom_console: &Conso .expect("Failed to release ir:USER packet"); // Remind the CPP that we're still listening - if let Err(e) = ir_user.start_polling_input(CPP_POLLING_PERIOD_MS) { + if let Err(e) = ir_user.request_input_polling(CPP_POLLING_PERIOD_MS) { println!("Error: {e:?}"); } } diff --git a/ctru-rs/src/services/ir_user.rs b/ctru-rs/src/services/ir_user.rs index a171812..303490a 100644 --- a/ctru-rs/src/services/ir_user.rs +++ b/ctru-rs/src/services/ir_user.rs @@ -1,5 +1,6 @@ use crate::error::ResultCode; use crate::services::ServiceReference; +use crate::Error; use ctru_sys::{Handle, MEMPERM_READ, MEMPERM_READWRITE}; use std::alloc::Layout; use std::cmp::max; @@ -11,10 +12,13 @@ use std::time::Duration; static IR_USER_ACTIVE: Mutex = Mutex::new(0); static IR_USER_STATE: Mutex> = Mutex::new(None); +/// The "ir:USER" service. This service is used to talk to IR devices such as +/// the Circle Pad Pro. pub struct IrUser { _service_reference: ServiceReference, } +// We need to hold on to some extra service state, hence this struct. struct IrUserState { service_handle: Handle, shared_memory_handle: Handle, @@ -24,6 +28,7 @@ struct IrUserState { recv_packet_count: usize, } +// ir:USER syscall command headers const REQUIRE_CONNECTION_COMMAND_HEADER: u32 = 0x00060040; const DISCONNECT_COMMAND_HEADER: u32 = 0x00090000; const GET_RECEIVE_EVENT_COMMAND_HEADER: u32 = 0x000A0000; @@ -32,7 +37,18 @@ const SEND_IR_NOP_COMMAND_HEADER: u32 = 0x000D0042; const INITIALIZE_IRNOP_SHARED_COMMAND_HEADER: u32 = 0x00180182; const RELEASE_RECEIVED_DATA_COMMAND_HEADER: u32 = 0x00190040; +// Misc constants +const SHARED_MEM_INFO_SECTIONS_SIZE: usize = 0x30; +const SHARED_MEM_RECV_BUFFER_OFFSET: usize = 0x20; +const PAGE_SIZE: usize = 0x1000; +const IR_BITRATE: u32 = 4; +const PACKET_INFO_SIZE: u32 = 8; +const CIRCLE_PAD_PRO_INPUT_RESPONSE_PACKET_ID: u8 = 0x10; + impl IrUser { + /// Initialize the ir:USER service. The provided buffer sizes and packet + /// counts are used to calculate the size of shared memory used by the + /// service. pub fn init( recv_buffer_size: usize, recv_packet_count: usize, @@ -53,19 +69,13 @@ impl IrUser { // Calculate the shared memory size. // Shared memory length must be page aligned. - let info_sections_size = 0x30; let minimum_shared_memory_len = - info_sections_size + recv_buffer_size + send_buffer_size; - let shared_memory_len = if minimum_shared_memory_len % 0x1000 != 0 { - (minimum_shared_memory_len / 0x1000) * 0x1000 + 0x1000 - } else { - (minimum_shared_memory_len / 0x1000) * 0x1000 - }; - assert_eq!(shared_memory_len % 0x1000, 0); + SHARED_MEM_INFO_SECTIONS_SIZE + recv_buffer_size + send_buffer_size; + let shared_memory_len = round_up(minimum_shared_memory_len, PAGE_SIZE); // Allocate the shared memory let shared_memory_layout = - Layout::from_size_align(shared_memory_len, 0x1000).unwrap(); + Layout::from_size_align(shared_memory_len, PAGE_SIZE).unwrap(); let shared_memory_ptr = std::alloc::alloc_zeroed(shared_memory_layout); let shared_memory = &*slice_from_raw_parts(shared_memory_ptr, shared_memory_len); @@ -87,7 +97,7 @@ impl IrUser { recv_packet_count: recv_packet_count as u32, send_packet_buffer_len: send_buffer_size as u32, send_packet_count: send_packet_count as u32, - bit_rate: 4, + bit_rate: IR_BITRATE, shared_memory_handle, })?; @@ -131,6 +141,7 @@ impl IrUser { }) } + /// Try to connect to the device with the provided ID. pub fn require_connection(&self, device_id: IrDeviceId) -> crate::Result<()> { self.send_service_request( vec![REQUIRE_CONNECTION_COMMAND_HEADER, device_id.get_id()], @@ -139,11 +150,13 @@ impl IrUser { Ok(()) } + /// Close the current IR connection. pub fn disconnect(&self) -> crate::Result<()> { self.send_service_request(vec![DISCONNECT_COMMAND_HEADER], 2)?; Ok(()) } + /// Get an event handle that activates on connection status changes. pub fn get_connection_status_event(&self) -> crate::Result { let response = self.send_service_request(vec![GET_CONNECTION_STATUS_EVENT_COMMAND_HEADER], 4)?; @@ -152,6 +165,7 @@ impl IrUser { Ok(status_event) } + /// Get an event handle that activates when a packet is received. pub fn get_recv_event(&self) -> crate::Result { let response = self.send_service_request(vec![GET_RECEIVE_EVENT_COMMAND_HEADER], 4)?; let recv_event = response[3] as Handle; @@ -159,6 +173,8 @@ impl IrUser { Ok(recv_event) } + /// Wait for an event to fire. If the timeout is reached, an error is returned. You can use + /// [`Error::is_timeout`] to check if the error is due to a timeout. pub fn wait_for_event(event: Handle, timeout: Duration) -> crate::Result<()> { unsafe { ResultCode(ctru_sys::svcWaitSynchronization( @@ -169,7 +185,11 @@ impl IrUser { Ok(()) } - pub fn start_polling_input(&self, period_ms: u8) -> crate::Result<()> { + /// Circle Pad Pro specific request. + /// + /// This will send a packet to the CPP requesting it to send back packets + /// with the current device input values. + pub fn request_input_polling(&self, period_ms: u8) -> crate::Result<()> { let ir_request: [u8; 3] = [1, period_ms, (period_ms + 2) << 2]; self.send_service_request( vec![ @@ -184,11 +204,14 @@ impl IrUser { Ok(()) } + /// Mark the last `packet_count` packets as processed, so their memory in + /// the receive buffer can be reused. pub fn release_received_data(&self, packet_count: u32) -> crate::Result<()> { self.send_service_request(vec![RELEASE_RECEIVED_DATA_COMMAND_HEADER, packet_count], 2)?; Ok(()) } + /// This will let you directly read the ir:USER shared memory via a callback. pub fn process_shared_memory(&self, process_fn: impl FnOnce(&[u8])) { let shared_mem_guard = IR_USER_STATE.lock().unwrap(); let shared_mem = shared_mem_guard.as_ref().unwrap(); @@ -196,6 +219,7 @@ impl IrUser { process_fn(shared_mem.shared_memory); } + /// Read and parse the ir:USER service status data from shared memory. pub fn get_status_info(&self) -> IrUserStatusInfo { let shared_mem_guard = IR_USER_STATE.lock().unwrap(); let shared_mem = shared_mem_guard.as_ref().unwrap().shared_memory; @@ -224,11 +248,13 @@ impl IrUser { } } + /// Read and parse the current packets received from the IR device. pub fn get_packets(&self) -> Vec { let shared_mem_guard = IR_USER_STATE.lock().unwrap(); let user_state = shared_mem_guard.as_ref().unwrap(); let shared_mem = user_state.shared_memory; + // Find where the packets are, and how many let start_index = u32::from_ne_bytes([ shared_mem[0x10], shared_mem[0x11], @@ -242,11 +268,15 @@ impl IrUser { shared_mem[0x1b], ]); + // Parse the packets (0..valid_packet_count) .map(|i| { + // Get the packet info let packet_index = (i + start_index) % user_state.recv_packet_count as u32; - let packet_info_offset = 0x20 + (packet_index * 8) as usize; - let packet_info = &shared_mem[packet_info_offset..packet_info_offset + 8]; + let packet_info_offset = + SHARED_MEM_RECV_BUFFER_OFFSET + (packet_index * PACKET_INFO_SIZE) as usize; + let packet_info = + &shared_mem[packet_info_offset..packet_info_offset + PACKET_INFO_SIZE]; let offset_to_data_buffer = u32::from_ne_bytes([ packet_info[0], @@ -261,14 +291,17 @@ impl IrUser { packet_info[7], ]) as usize; - let packet_info_section_size = user_state.recv_packet_count * 8; + // Find the packet data. The packet data may wrap around the buffer end, so + // `packet_data` is a function from packet byte offset to value. + let packet_info_section_size = user_state.recv_packet_count * PACKET_INFO_SIZE; + let header_size = SHARED_MEM_RECV_BUFFER_OFFSET + packet_info_section_size; + let data_buffer_size = user_state.recv_buffer_size - packet_info_section_size; let packet_data = |idx| -> u8 { - let header_size = 0x20 + packet_info_section_size; let data_buffer_offset = offset_to_data_buffer + idx; - let data_buffer_size = user_state.recv_buffer_size - packet_info_section_size; shared_mem[header_size + data_buffer_offset % data_buffer_size] }; + // Find out how long the payload is (payload length is variable-length encoded) let (payload_length, payload_offset) = if packet_data(2) & 0x40 != 0 { // Big payload ( @@ -280,8 +313,10 @@ impl IrUser { ((packet_data(2) & 0x3F) as usize, 3) }; + // Check our payload length math against what the packet info contains assert_eq!(data_length, payload_offset + payload_length + 1); + // IR packets start with a magic number, so double check it let magic_number = packet_data(0); assert_eq!(magic_number, 0xA5); @@ -298,6 +333,7 @@ impl IrUser { .collect() } + /// Internal helper for calling ir:USER service methods. fn send_service_request( &self, mut request: Vec, @@ -331,6 +367,15 @@ impl IrUser { } } +// Internal helper for rounding up a value to a multiple of another value. +fn round_up(value: usize, multiple: usize) -> usize { + if value % multiple != 0 { + (value / multiple) * multiple + multiple + } else { + (value / multiple) * multiple + } +} + struct InitializeIrnopSharedParams { ir_user_handle: Handle, shared_memory_len: u32, @@ -342,6 +387,7 @@ struct InitializeIrnopSharedParams { shared_memory_handle: Handle, } +/// Internal helper for initializing the ir:USER service unsafe fn initialize_irnop_shared(params: InitializeIrnopSharedParams) -> crate::Result<()> { let cmd_buffer = &mut *(slice_from_raw_parts_mut(ctru_sys::getThreadCommandBuffer(), 9)); cmd_buffer[0] = INITIALIZE_IRNOP_SHARED_COMMAND_HEADER; @@ -363,6 +409,8 @@ unsafe fn initialize_irnop_shared(params: InitializeIrnopSharedParams) -> crate: Ok(()) } +/// An enum which represents the different IR devices the 3DS can connect to via +/// the ir:USER service. pub enum IrDeviceId { CirclePadPro, // Pretty sure no other IDs are recognized, but just in case @@ -370,6 +418,7 @@ pub enum IrDeviceId { } impl IrDeviceId { + /// Get the ID of the device. pub fn get_id(&self) -> u32 { match *self { IrDeviceId::CirclePadPro => 1, @@ -378,6 +427,7 @@ impl IrDeviceId { } } +/// This struct holds a parsed copy of the ir:USER service status (from shared memory). #[derive(Debug)] pub struct IrUserStatusInfo { pub recv_err_result: ctru_sys::Result, @@ -392,6 +442,7 @@ pub struct IrUserStatusInfo { pub unknown_field_3: u8, } +/// A packet of data sent/received to/from the IR device. #[derive(Debug)] pub struct IrUserPacket { pub magic_number: u8, @@ -401,6 +452,7 @@ pub struct IrUserPacket { pub checksum: u8, } +/// Circle Pad Pro response packet holding the current device input signals and status. #[derive(Debug)] pub struct CirclePadProInputResponse { pub c_stick_x: u16, @@ -424,7 +476,7 @@ impl TryFrom<&IrUserPacket> for CirclePadProInputResponse { } let response_id = packet.payload[0]; - if response_id != 0x10 { + if response_id != CIRCLE_PAD_PRO_INPUT_RESPONSE_PACKET_ID { return Err(format!( "Invalid response ID (expected 0x10, got {:#x}", packet.payload[0]