diff --git a/ctru-rs/examples/ir-user-circle-pad-pro.rs b/ctru-rs/examples/ir-user-circle-pad-pro.rs new file mode 100644 index 0000000..43cf0e2 --- /dev/null +++ b/ctru-rs/examples/ir-user-circle-pad-pro.rs @@ -0,0 +1,269 @@ +//! ir:USER Circle Pad Pro example. +//! +//! A demo of using the ir:USER service to connect to the Circle Pad Pro. + +use ctru::prelude::*; +use ctru::services::gfx::{Flush, Swap}; +use ctru::services::ir_user::{CirclePadProInputResponse, ConnectionStatus, IrDeviceId, IrUser}; +use ctru::services::svc::HandleExt; +use ctru_sys::Handle; +use std::time::Duration; + +// Configuration for this demo of the Circle Pad Pro (not general purpose ir:USER values). +const PACKET_INFO_SIZE: usize = 8; +const MAX_PACKET_SIZE: usize = 32; +const PACKET_COUNT: usize = 1; +const PACKET_BUFFER_SIZE: usize = PACKET_COUNT * (PACKET_INFO_SIZE + MAX_PACKET_SIZE); +const CPP_CONNECTION_POLLING_PERIOD_MS: u8 = 0x08; +const CPP_POLLING_PERIOD_MS: u8 = 0x32; + +// This export tells libctru to not initialize ir:rst when initializing HID. +// This is necessary on the New 3DS because ir:rst is mutually exclusive with ir:USER. +#[no_mangle] +unsafe extern "C" fn hidShouldUseIrrst() -> bool { + false +} + +fn main() { + let apt = Apt::new().unwrap(); + let gfx = Gfx::new().unwrap(); + let top_console = Console::new(gfx.top_screen.borrow_mut()); + let bottom_console = Console::new(gfx.bottom_screen.borrow_mut()); + let mut demo = CirclePadProDemo::new(top_console, bottom_console); + demo.print_status_info(); + + // Initialize HID after ir:USER because libctru also initializes ir:rst, + // which is mutually exclusive with ir:USER. Initializing HID before ir:USER + // on New 3DS causes ir:USER to not work. + let mut hid = Hid::new().unwrap(); + + println!("Press A to connect to the CPP, or Start to exit"); + + let mut is_connected = false; + while apt.main_loop() { + hid.scan_input(); + + // Check if we need to exit + if hid.keys_held().contains(KeyPad::START) { + break; + } + + // Check if we've received a packet from the circle pad pro + let packet_received = demo + .receive_packet_event + .wait_for_event(Duration::ZERO) + .is_ok(); + if packet_received { + demo.handle_packets(); + } + + // Check if we should start the connection + if hid.keys_down().contains(KeyPad::A) && !is_connected { + println!("Attempting to connect to the CPP"); + + match demo.connect_to_cpp(&mut hid) { + ConnectionResult::Connected => is_connected = true, + ConnectionResult::Canceled => break, + } + } + + gfx.wait_for_vblank(); + } +} + +struct CirclePadProDemo<'screen> { + top_console: Console<'screen>, + bottom_console: Console<'screen>, + ir_user: IrUser, + connection_status_event: Handle, + receive_packet_event: Handle, +} + +enum ConnectionResult { + Connected, + Canceled, +} + +impl<'screen> CirclePadProDemo<'screen> { + fn new(mut top_console: Console<'screen>, bottom_console: Console<'screen>) -> Self { + // Set up double buffering on top screen + top_console.set_double_buffering(true); + top_console.swap_buffers(); + + // Write messages to bottom screen (not double buffered) + bottom_console.select(); + println!("Welcome to the ir:USER / Circle Pad Pro Demo"); + + println!("Starting up ir:USER service"); + let ir_user = IrUser::init( + PACKET_BUFFER_SIZE, + PACKET_COUNT, + PACKET_BUFFER_SIZE, + PACKET_COUNT, + ) + .expect("Couldn't initialize ir:USER service"); + println!("ir:USER service initialized"); + + // Get event handles + let connection_status_event = ir_user + .get_connection_status_event() + .expect("Couldn't get ir:USER connection status event"); + let receive_packet_event = ir_user + .get_recv_event() + .expect("Couldn't get ir:USER recv event"); + + Self { + top_console, + bottom_console, + ir_user, + connection_status_event, + receive_packet_event, + } + } + + fn print_status_info(&mut self) { + self.top_console.select(); + self.top_console.clear(); + println!("{:#x?}", self.ir_user.get_status_info()); + self.top_console.flush_buffers(); + self.top_console.swap_buffers(); + self.bottom_console.select(); + } + + fn connect_to_cpp(&mut self, hid: &mut Hid) -> ConnectionResult { + // Connection loop + loop { + hid.scan_input(); + if hid.keys_held().contains(KeyPad::START) { + return ConnectionResult::Canceled; + } + + // Start the connection process + self.ir_user + .require_connection(IrDeviceId::CirclePadPro) + .expect("Couldn't initialize circle pad pro connection"); + + // Wait for the connection to establish + if let Err(e) = self + .connection_status_event + .wait_for_event(Duration::from_millis(100)) + { + if !e.is_timeout() { + panic!("Couldn't initialize circle pad pro connection: {e}"); + } + } + + self.print_status_info(); + if self.ir_user.get_status_info().connection_status == ConnectionStatus::Connected { + println!("Connected!"); + break; + } + + // If not connected (ex. timeout), disconnect so we can retry + self.ir_user + .disconnect() + .expect("Failed to disconnect circle pad pro connection"); + + // Wait for the disconnect to go through + if let Err(e) = self + .connection_status_event + .wait_for_event(Duration::from_millis(100)) + { + if !e.is_timeout() { + panic!("Couldn't initialize circle pad pro connection: {e}"); + } + } + } + + // Sending first packet retry loop + loop { + hid.scan_input(); + if hid.keys_held().contains(KeyPad::START) { + return ConnectionResult::Canceled; + } + + // Send a request for input to the CPP + if let Err(e) = self + .ir_user + .request_input_polling(CPP_CONNECTION_POLLING_PERIOD_MS) + { + println!("Error: {e:?}"); + } + self.print_status_info(); + + // Wait for the response + let recv_event_result = self + .receive_packet_event + .wait_for_event(Duration::from_millis(100)); + self.print_status_info(); + + if recv_event_result.is_ok() { + println!("Got first packet from CPP"); + self.handle_packets(); + break; + } + + // We didn't get a response in time, so loop and retry + } + + ConnectionResult::Connected + } + + fn handle_packets(&mut self) { + let packets = self + .ir_user + .get_packets() + .expect("Packets should be well formed"); + let packet_count = packets.len(); + let Some(last_packet) = packets.last() else { + return; + }; + let status_info = self.ir_user.get_status_info(); + let cpp_response = CirclePadProInputResponse::try_from(last_packet) + .expect("Failed to parse CPP response from IR packet"); + + // Write data to top screen + self.top_console.select(); + self.top_console.clear(); + println!("{:x?}", status_info); + + self.ir_user.process_shared_memory(|ir_mem| { + println!("\nReceiveBufferInfo:"); + print_buffer_as_hex(&ir_mem[0x10..0x20]); + + println!("\nReceiveBuffer:"); + print_buffer_as_hex(&ir_mem[0x20..0x20 + PACKET_BUFFER_SIZE]); + println!(); + }); + + println!("\nPacket count: {packet_count}"); + println!("{last_packet:02x?}"); + println!("\n{cpp_response:#02x?}"); + + // Flush output and switch back to bottom screen + self.top_console.flush_buffers(); + self.top_console.swap_buffers(); + self.bottom_console.select(); + + // Done handling the packets, release them + self.ir_user + .release_received_data(packet_count as u32) + .expect("Failed to release ir:USER packet"); + + // Remind the CPP that we're still listening + if let Err(e) = self.ir_user.request_input_polling(CPP_POLLING_PERIOD_MS) { + println!("Error: {e:?}"); + } + } +} + +fn print_buffer_as_hex(buffer: &[u8]) { + let mut counter = 0; + for byte in buffer { + print!("{byte:02x} "); + counter += 1; + if counter % 16 == 0 { + println!(); + } + } +} diff --git a/ctru-rs/src/console.rs b/ctru-rs/src/console.rs index 5a8c772..cb98840 100644 --- a/ctru-rs/src/console.rs +++ b/ctru-rs/src/console.rs @@ -10,7 +10,7 @@ use std::default::Default; use ctru_sys::{consoleClear, consoleInit, consoleSelect, consoleSetWindow, PrintConsole}; -use crate::services::gfx::Screen; +use crate::services::gfx::{Flush, Screen, Swap}; static mut EMPTY_CONSOLE: PrintConsole = unsafe { const_zero::const_zero!(PrintConsole) }; @@ -39,6 +39,10 @@ pub enum Dimension { Height, } +/// A [`Screen`] that can be used as a target for [`Console`]. +pub trait ConsoleScreen: Screen + Swap + Flush {} +impl ConsoleScreen for S {} + /// Virtual text console. /// /// [`Console`] lets the application redirect `stdout` and `stderr` to a simple text displayer on the 3DS screen. @@ -60,7 +64,7 @@ pub enum Dimension { #[doc(alias = "PrintConsole")] pub struct Console<'screen> { context: Box, - screen: RefMut<'screen, dyn Screen>, + screen: RefMut<'screen, dyn ConsoleScreen>, } impl<'screen> Console<'screen> { @@ -102,7 +106,7 @@ impl<'screen> Console<'screen> { /// # } /// ``` #[doc(alias = "consoleInit")] - pub fn new(screen: RefMut<'screen, dyn Screen>) -> Self { + pub fn new(screen: RefMut<'screen, S>) -> Self { let mut context = Box::::default(); unsafe { consoleInit(screen.as_raw(), context.as_mut()) }; @@ -324,6 +328,30 @@ impl<'screen> Console<'screen> { } } +impl Swap for Console<'_> { + /// Swaps the video buffers. Note: The console's cursor position is not reset, only the framebuffer is changed. + /// + /// Even if double buffering is disabled, "swapping" the buffers has the side effect + /// of committing any configuration changes to the buffers (e.g. [`TopScreen::set_wide_mode()`], + /// [`Screen::set_framebuffer_format()`], [`Swap::set_double_buffering()`]), so it should still be used. + /// + /// This should be called once per frame at most. + fn swap_buffers(&mut self) { + self.screen.swap_buffers(); + self.context.frameBuffer = self.screen.raw_framebuffer().ptr as *mut u16; + } + + fn set_double_buffering(&mut self, enabled: bool) { + self.screen.set_double_buffering(enabled); + } +} + +impl Flush for Console<'_> { + fn flush_buffers(&mut self) { + self.screen.flush_buffers(); + } +} + impl Drop for Console<'_> { fn drop(&mut self) { unsafe { diff --git a/ctru-rs/src/error.rs b/ctru-rs/src/error.rs index dc5a421..0be67b7 100644 --- a/ctru-rs/src/error.rs +++ b/ctru-rs/src/error.rs @@ -94,6 +94,8 @@ pub enum Error { /// Size of the requested data (in bytes). wanted: usize, }, + /// An error that doesn't fit into the other categories. + Other(String), } impl Error { @@ -113,6 +115,14 @@ impl Error { // Copy out of the error string, since it may be changed by other libc calls later Self::Libc(error_str.to_string_lossy().into()) } + + /// Check if the error is a timeout. + pub fn is_timeout(&self) -> bool { + match *self { + Error::Os(code) => R_DESCRIPTION(code) == ctru_sys::RD_TIMEOUT as ctru_sys::Result, + _ => false, + } + } } impl From for Error { @@ -146,6 +156,7 @@ impl fmt::Debug for Error { .field("provided", provided) .field("wanted", wanted) .finish(), + Self::Other(err) => f.debug_tuple("Other").field(err).finish(), } } } @@ -168,7 +179,8 @@ impl fmt::Display for Error { Self::OutputAlreadyRedirected => { write!(f, "output streams are already redirected to 3dslink") } - Self::BufferTooShort{provided, wanted} => write!(f, "the provided buffer's length is too short (length = {provided}) to hold the wanted data (size = {wanted})") + Self::BufferTooShort{provided, wanted} => write!(f, "the provided buffer's length is too short (length = {provided}) to hold the wanted data (size = {wanted})"), + Self::Other(err) => write!(f, "{err}"), } } } diff --git a/ctru-rs/src/lib.rs b/ctru-rs/src/lib.rs index e8c6483..2874839 100644 --- a/ctru-rs/src/lib.rs +++ b/ctru-rs/src/lib.rs @@ -64,6 +64,7 @@ pub mod linear; pub mod mii; pub mod os; pub mod prelude; +mod sealed; pub mod services; pub use crate::error::{Error, Result}; diff --git a/ctru-rs/src/sealed.rs b/ctru-rs/src/sealed.rs new file mode 100644 index 0000000..34d5fee --- /dev/null +++ b/ctru-rs/src/sealed.rs @@ -0,0 +1,15 @@ +//! This is a private module to prevent users from implementing certain traits. +//! This is done by requiring a `Sealed` trait implementation, which can only be +//! done in this crate. + +use crate::console::Console; +use crate::services::gfx::{BottomScreen, TopScreen, TopScreen3D, TopScreenLeft, TopScreenRight}; + +pub trait Sealed {} + +impl Sealed for TopScreen {} +impl Sealed for TopScreen3D<'_> {} +impl Sealed for TopScreenLeft {} +impl Sealed for TopScreenRight {} +impl Sealed for BottomScreen {} +impl Sealed for Console<'_> {} diff --git a/ctru-rs/src/services/gfx.rs b/ctru-rs/src/services/gfx.rs index 6eb04f6..7a6ec6e 100644 --- a/ctru-rs/src/services/gfx.rs +++ b/ctru-rs/src/services/gfx.rs @@ -9,28 +9,17 @@ use std::marker::PhantomData; use std::sync::Mutex; use crate::error::Result; +use crate::sealed::Sealed; use crate::services::gspgpu::{self, FramebufferFormat}; use crate::services::ServiceReference; -mod private { - use super::{BottomScreen, TopScreen, TopScreen3D, TopScreenLeft, TopScreenRight}; - - pub trait Sealed {} - - impl Sealed for TopScreen {} - impl Sealed for TopScreen3D<'_> {} - impl Sealed for TopScreenLeft {} - impl Sealed for TopScreenRight {} - impl Sealed for BottomScreen {} -} - /// Trait to handle common functionality for all screens. /// /// This trait is implemented by the screen structs for working with frame buffers and /// drawing to the screens. Graphics-related code can be made generic over this /// trait to work with any of the given screens. #[doc(alias = "gfxScreen_t")] -pub trait Screen: private::Sealed { +pub trait Screen: Sealed { /// Returns the `libctru` value for the Screen kind. fn as_raw(&self) -> ctru_sys::gfxScreen_t; @@ -99,7 +88,7 @@ pub struct TopScreen3D<'screen> { /// Trait for screens that can have its frame buffers swapped, when double buffering is enabled. /// /// This trait applies to all [`Screen`]s that have swappable frame buffers. -pub trait Swap: private::Sealed { +pub trait Swap: Sealed { /// Swaps the video buffers. /// /// Even if double buffering is disabled, "swapping" the buffers has the side effect @@ -160,7 +149,7 @@ impl Swap for BottomScreen { /// A screen with buffers that can be flushed. /// /// This trait applies to any [`Screen`] that has data written to its frame buffer. -pub trait Flush: private::Sealed { +pub trait Flush: Sealed { /// Flushes the video buffer(s) for this screen. /// /// Note that you must still call [`Swap::swap_buffers`] after this method for the buffer contents to be displayed. diff --git a/ctru-rs/src/services/ir_user.rs b/ctru-rs/src/services/ir_user.rs new file mode 100644 index 0000000..672fe20 --- /dev/null +++ b/ctru-rs/src/services/ir_user.rs @@ -0,0 +1,496 @@ +//! IR (Infrared) User Service. +//! +//! The ir:USER service allows you to communicate with IR devices such as the Circle Pad Pro. +//! +//! The Circle Pad Pro (CPP) is an accessory for the 3DS which adds a second Circle Pad and extra shoulder buttons. +//! On New 3DS systems, the ir:USER service uses the built-in C-stick and new shoulder buttons to emulate the Circle Pad +//! Pro. Many released games which support the second stick and extra shoulder buttons use this service to communicate +//! so they can support both Old 3DS + CPP and New 3DS. +#![doc(alias = "input")] +#![doc(alias = "controller")] +#![doc(alias = "gamepad")] + +use crate::error::ResultCode; +use crate::services::svc::{make_ipc_header, HandleExt}; +use crate::services::ServiceReference; +use crate::Error; +use ctru_sys::{Handle, MEMPERM_READ, MEMPERM_READWRITE}; +use std::alloc::Layout; +use std::ffi::CString; +use std::ptr::slice_from_raw_parts; +use std::sync::Mutex; + +static IR_USER_ACTIVE: Mutex<()> = Mutex::new(()); +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, + shared_memory: &'static [u8], + shared_memory_layout: Layout, + recv_buffer_size: usize, + recv_packet_count: usize, +} + +// ir:USER syscall command headers +const REQUIRE_CONNECTION_COMMAND_HEADER: u32 = make_ipc_header(6, 1, 0); +const DISCONNECT_COMMAND_HEADER: u32 = make_ipc_header(9, 0, 0); +const GET_RECEIVE_EVENT_COMMAND_HEADER: u32 = make_ipc_header(10, 0, 0); +const GET_CONNECTION_STATUS_EVENT_COMMAND_HEADER: u32 = make_ipc_header(12, 0, 0); +const SEND_IR_NOP_COMMAND_HEADER: u32 = make_ipc_header(13, 1, 2); +const INITIALIZE_IRNOP_SHARED_COMMAND_HEADER: u32 = make_ipc_header(24, 6, 2); +const RELEASE_RECEIVED_DATA_COMMAND_HEADER: u32 = make_ipc_header(25, 1, 0); + +// 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: usize = 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, + send_buffer_size: usize, + send_packet_count: usize, + ) -> crate::Result { + let service_reference = ServiceReference::new( + &IR_USER_ACTIVE, + || unsafe { + // Get the ir:USER service handle + let mut service_handle = Handle::default(); + let service_name = CString::new("ir:USER").unwrap(); + ResultCode(ctru_sys::srvGetServiceHandle( + &mut service_handle, + service_name.as_ptr(), + ))?; + + // Calculate the shared memory size. + // Shared memory length must be a multiple of the page size. + let minimum_shared_memory_len = + 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, 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); + + // Mark the memory as shared + let mut shared_memory_handle = Handle::default(); + ResultCode(ctru_sys::svcCreateMemoryBlock( + &mut shared_memory_handle, + shared_memory_ptr as u32, + shared_memory_len as u32, + MEMPERM_READ, + MEMPERM_READWRITE, + ))?; + + // Initialize the ir:USER service with the shared memory + let request = vec![ + INITIALIZE_IRNOP_SHARED_COMMAND_HEADER, + shared_memory_len as u32, + recv_buffer_size as u32, + recv_packet_count as u32, + send_buffer_size as u32, + send_packet_count as u32, + IR_BITRATE, + 0, + shared_memory_handle, + ]; + service_handle.send_service_request(request, 2)?; + + // Set up our service state + let user_state = IrUserState { + service_handle, + shared_memory_handle, + shared_memory, + shared_memory_layout, + recv_buffer_size, + recv_packet_count, + }; + let mut ir_user_state = IR_USER_STATE + .lock() + .map_err(|e| Error::Other(format!("Failed to write to IR_USER_STATE: {e}")))?; + *ir_user_state = Some(user_state); + + Ok(()) + }, + || { + // Remove our service state from the global location + let mut shared_mem_guard = IR_USER_STATE + .lock() + .expect("Failed to write to IR_USER_STATE"); + let Some(shared_mem) = shared_mem_guard.take() else { + // If we don't have any state, then we don't need to clean up. + return; + }; + + (move || unsafe { + // Close service and memory handles + ResultCode(ctru_sys::svcCloseHandle(shared_mem.service_handle))?; + ResultCode(ctru_sys::svcCloseHandle(shared_mem.shared_memory_handle))?; + + // Free shared memory + std::alloc::dealloc( + shared_mem.shared_memory.as_ptr() as *mut u8, + shared_mem.shared_memory_layout, + ); + + Ok(()) + })() + .unwrap(); + }, + )?; + + Ok(IrUser { + _service_reference: service_reference, + }) + } + + /// Try to connect to the device with the provided ID. + pub fn require_connection(&mut self, device_id: IrDeviceId) -> crate::Result<()> { + unsafe { + self.send_service_request( + vec![REQUIRE_CONNECTION_COMMAND_HEADER, device_id.get_id()], + 2, + )?; + } + Ok(()) + } + + /// Close the current IR connection. + pub fn disconnect(&mut self) -> crate::Result<()> { + unsafe { + 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 = unsafe { + self.send_service_request(vec![GET_CONNECTION_STATUS_EVENT_COMMAND_HEADER], 4) + }?; + let status_event = response[3] as Handle; + + Ok(status_event) + } + + /// Get an event handle that activates when a packet is received. + pub fn get_recv_event(&self) -> crate::Result { + let response = + unsafe { self.send_service_request(vec![GET_RECEIVE_EVENT_COMMAND_HEADER], 4) }?; + let recv_event = response[3] as Handle; + + Ok(recv_event) + } + + /// 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(&mut self, period_ms: u8) -> crate::Result<()> { + let ir_request: [u8; 3] = [1, period_ms, (period_ms + 2) << 2]; + unsafe { + self.send_service_request( + vec![ + SEND_IR_NOP_COMMAND_HEADER, + ir_request.len() as u32, + 2 + (ir_request.len() << 14) as u32, + ir_request.as_ptr() as u32, + ], + 2, + )?; + } + + Ok(()) + } + + /// Mark the last `packet_count` packets as processed, so their memory in + /// the receive buffer can be reused. + pub fn release_received_data(&mut self, packet_count: u32) -> crate::Result<()> { + unsafe { + 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(); + + 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; + + IrUserStatusInfo { + recv_err_result: i32::from_ne_bytes(shared_mem[0..4].try_into().unwrap()), + send_err_result: i32::from_ne_bytes(shared_mem[4..8].try_into().unwrap()), + connection_status: match shared_mem[8] { + 0 => ConnectionStatus::Disconnected, + 1 => ConnectionStatus::Connecting, + 2 => ConnectionStatus::Connected, + n => ConnectionStatus::Unknown(n), + }, + trying_to_connect_status: shared_mem[9], + connection_role: shared_mem[10], + machine_id: shared_mem[11], + unknown_field_1: shared_mem[12], + network_id: shared_mem[13], + unknown_field_2: shared_mem[14], + unknown_field_3: shared_mem[15], + } + } + + /// Read and parse the current packets received from the IR device. + pub fn get_packets(&self) -> Result, String> { + 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..0x14].try_into().unwrap()); + let valid_packet_count = u32::from_ne_bytes(shared_mem[0x18..0x1c].try_into().unwrap()); + + // Parse the packets + (0..valid_packet_count as usize) + .map(|i| { + // Get the packet info + let packet_index = (i + start_index as usize) % user_state.recv_packet_count; + let packet_info_offset = + SHARED_MEM_RECV_BUFFER_OFFSET + (packet_index * PACKET_INFO_SIZE); + 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..4].try_into().unwrap()) as usize; + let data_length = + u32::from_ne_bytes(packet_info[4..8].try_into().unwrap()) as usize; + + // 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 data_buffer_offset = offset_to_data_buffer + idx; + 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 + ( + ((packet_data(2) as usize & 0x3F) << 8) + packet_data(3) as usize, + 4, + ) + } else { + // Small payload + ((packet_data(2) & 0x3F) as usize, 3) + }; + + // Check our payload length math against what the packet info contains + if data_length != payload_offset + payload_length + 1 { + return Err(format!( + "Invalid payload length (expected {}, got {})", + data_length, + payload_offset + payload_length + 1 + )); + } + + // IR packets start with a magic number, so double check it + let magic_number = packet_data(0); + if magic_number != 0xA5 { + return Err(format!( + "Invalid magic number in packet: {magic_number:#x}, expected 0xA5" + )); + } + + Ok(IrUserPacket { + magic_number: packet_data(0), + destination_network_id: packet_data(1), + payload_length, + payload: (payload_offset..payload_offset + payload_length) + .map(packet_data) + .collect(), + checksum: packet_data(payload_offset + payload_length), + }) + }) + .collect() + } + + /// Internal helper for calling ir:USER service methods. + unsafe fn send_service_request( + &self, + request: Vec, + expected_response_len: usize, + ) -> crate::Result> { + let mut shared_mem_guard = IR_USER_STATE.lock().unwrap(); + let shared_mem = shared_mem_guard.as_mut().unwrap(); + + shared_mem + .service_handle + .send_service_request(request, expected_response_len) + } +} + +// 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 + } +} + +/// An enum which represents the different IR devices the 3DS can connect to via +/// the ir:USER service. +pub enum IrDeviceId { + /// Circle Pad Pro + CirclePadPro, + /// Other devices + // Pretty sure no other IDs are recognized, but just in case + Custom(u32), +} + +impl IrDeviceId { + /// Get the ID of the device. + pub fn get_id(&self) -> u32 { + match *self { + IrDeviceId::CirclePadPro => 1, + IrDeviceId::Custom(id) => id, + } + } +} + +/// This struct holds a parsed copy of the ir:USER service status (from shared memory). +#[derive(Debug)] +pub struct IrUserStatusInfo { + /// The result of the last receive operation. + pub recv_err_result: ctru_sys::Result, + /// The result of the last send operation. + pub send_err_result: ctru_sys::Result, + /// The current connection status. + pub connection_status: ConnectionStatus, + /// The status of the connection attempt. + pub trying_to_connect_status: u8, + /// The role of the device in the connection (value meaning is unknown). + pub connection_role: u8, + /// The machine ID of the device. + pub machine_id: u8, + /// Unknown field. + pub unknown_field_1: u8, + /// The network ID of the connection. + pub network_id: u8, + /// Unknown field. + pub unknown_field_2: u8, + /// Unknown field. + pub unknown_field_3: u8, +} + +/// Connection status values for [`IrUserStatusInfo`]. +#[repr(u8)] +#[derive(Debug, PartialEq, Eq)] +pub enum ConnectionStatus { + /// Device is not connected + Disconnected = 0, + /// Waiting for device to connect + Connecting = 1, + /// Device is connected + Connected = 2, + /// Unknown connection status + Unknown(u8), +} + +/// A packet of data sent/received to/from the IR device. +#[derive(Debug)] +pub struct IrUserPacket { + /// The magic number of the packet. Should always be 0xA5. + pub magic_number: u8, + /// The destination network ID. + pub destination_network_id: u8, + /// The length of the payload. + pub payload_length: usize, + /// The payload data. + pub payload: Vec, + /// The checksum of the packet. + pub checksum: u8, +} + +/// Circle Pad Pro response packet holding the current device input signals and status. +#[derive(Debug, Default)] +pub struct CirclePadProInputResponse { + /// The X value of the C-stick. + pub c_stick_x: u16, + /// The Y value of the C-stick. + pub c_stick_y: u16, + /// The battery level of the Circle Pad Pro. + pub battery_level: u8, + /// Whether the ZL button is pressed. + pub zl_pressed: bool, + /// Whether the ZR button is pressed. + pub zr_pressed: bool, + /// Whether the R button is pressed. + pub r_pressed: bool, + /// Unknown field. + pub unknown_field: u8, +} + +impl TryFrom<&IrUserPacket> for CirclePadProInputResponse { + type Error = String; + + fn try_from(packet: &IrUserPacket) -> Result { + if packet.payload.len() != 6 { + return Err(format!( + "Invalid payload length (expected 6 bytes, got {})", + packet.payload.len() + )); + } + + let response_id = packet.payload[0]; + if response_id != CIRCLE_PAD_PRO_INPUT_RESPONSE_PACKET_ID { + return Err(format!( + "Invalid response ID (expected {CIRCLE_PAD_PRO_INPUT_RESPONSE_PACKET_ID}, got {:#x}", + packet.payload[0] + )); + } + + let c_stick_x = packet.payload[1] as u16 + (((packet.payload[2] & 0x0F) as u16) << 8); + let c_stick_y = + (((packet.payload[2] & 0xF0) as u16) >> 4) + ((packet.payload[3] as u16) << 4); + let battery_level = packet.payload[4] & 0x1F; + let zl_pressed = packet.payload[4] & 0x20 == 0; + let zr_pressed = packet.payload[4] & 0x40 == 0; + let r_pressed = packet.payload[4] & 0x80 == 0; + let unknown_field = packet.payload[5]; + + Ok(CirclePadProInputResponse { + c_stick_x, + c_stick_y, + battery_level, + zl_pressed, + zr_pressed, + r_pressed, + unknown_field, + }) + } +} diff --git a/ctru-rs/src/services/mod.rs b/ctru-rs/src/services/mod.rs index 8aadf87..4fadeb3 100644 --- a/ctru-rs/src/services/mod.rs +++ b/ctru-rs/src/services/mod.rs @@ -19,11 +19,13 @@ pub mod fs; pub mod gfx; pub mod gspgpu; pub mod hid; +pub mod ir_user; pub mod ndsp; pub mod ps; mod reference; pub mod soc; pub mod sslc; +pub mod svc; cfg_if::cfg_if! { if #[cfg(all(feature = "romfs", romfs_exists))] { diff --git a/ctru-rs/src/services/svc.rs b/ctru-rs/src/services/svc.rs new file mode 100644 index 0000000..11530f0 --- /dev/null +++ b/ctru-rs/src/services/svc.rs @@ -0,0 +1,76 @@ +//! Syscall APIs +//! +//! Not all APIs are wrapped in this module, since a lot are fundamentally unsafe. +//! Most APIs should be used directly from `ctru-sys`. + +use crate::error::ResultCode; +use ctru_sys::Handle; +use std::time::Duration; + +/// Extension trait for [Handle] +pub trait HandleExt { + /// 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. + fn wait_for_event(self, timeout: Duration) -> crate::Result<()>; + + /// Send a service request to the handle. + /// The request vector must contain the command header and any parameters. + /// The request vector is overwritten with the response and returned. + /// The error in the response is checked and returned as a `Result::Err` if the operation failed. + /// + /// # Safety + /// This function is unsafe because it directly accesses the thread command buffer. + /// If the request vector or the expected response length is incorrect, or the handle is not a service that accepts + /// requests, the function may cause undefined behavior. + unsafe fn send_service_request( + self, + request: Vec, + expected_response_len: usize, + ) -> crate::Result>; +} + +impl HandleExt for Handle { + fn wait_for_event(self, timeout: Duration) -> crate::Result<()> { + let timeout = i64::try_from(timeout.as_nanos()).map_err(|e| { + crate::Error::Other(format!( + "Failed to convert timeout to 64-bit nanoseconds: {}", + e + )) + })?; + unsafe { + ResultCode(ctru_sys::svcWaitSynchronization(self, timeout))?; + } + Ok(()) + } + + unsafe fn send_service_request( + self, + mut request: Vec, + expected_response_len: usize, + ) -> crate::Result> { + // Copy over the request + let cmd_buffer_ptr = unsafe { ctru_sys::getThreadCommandBuffer() }; + std::ptr::copy_nonoverlapping(request.as_ptr(), cmd_buffer_ptr, request.len()); + + // Send the request + ResultCode(ctru_sys::svcSendSyncRequest(self))?; + + // Handle the result returned by the service + let result = unsafe { std::ptr::read(cmd_buffer_ptr.add(1)) }; + ResultCode(result as ctru_sys::Result)?; + + // Copy back the response + request.clear(); + request.resize(expected_response_len, 0); + std::ptr::copy_nonoverlapping(cmd_buffer_ptr, request.as_mut_ptr(), expected_response_len); + + Ok(request) + } +} + +/// Creates a command header to be used for IPC. This is a const fn version of [`ctru_sys::IPC_MakeHeader`]. +pub const fn make_ipc_header(command_id: u16, normal_params: u8, translate_params: u8) -> u32 { + ((command_id as u32) << 16) + | (((normal_params as u32) & 0x3F) << 6) + | ((translate_params as u32) & 0x3F) +}