Browse Source

Merge pull request #86 from rust3ds/feature/ir-user

Add an ir:USER service wrapper and Circle Pad Pro example
pull/159/head
Mark Drobnak 12 months ago committed by GitHub
parent
commit
157824ad8b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 269
      ctru-rs/examples/ir-user-circle-pad-pro.rs
  2. 34
      ctru-rs/src/console.rs
  3. 14
      ctru-rs/src/error.rs
  4. 1
      ctru-rs/src/lib.rs
  5. 15
      ctru-rs/src/sealed.rs
  6. 19
      ctru-rs/src/services/gfx.rs
  7. 496
      ctru-rs/src/services/ir_user.rs
  8. 2
      ctru-rs/src/services/mod.rs
  9. 76
      ctru-rs/src/services/svc.rs

269
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!();
}
}
}

34
ctru-rs/src/console.rs

@ -10,7 +10,7 @@ use std::default::Default;
use ctru_sys::{consoleClear, consoleInit, consoleSelect, consoleSetWindow, PrintConsole}; 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) }; static mut EMPTY_CONSOLE: PrintConsole = unsafe { const_zero::const_zero!(PrintConsole) };
@ -39,6 +39,10 @@ pub enum Dimension {
Height, Height,
} }
/// A [`Screen`] that can be used as a target for [`Console`].
pub trait ConsoleScreen: Screen + Swap + Flush {}
impl<S: Screen + Swap + Flush> ConsoleScreen for S {}
/// Virtual text console. /// Virtual text console.
/// ///
/// [`Console`] lets the application redirect `stdout` and `stderr` to a simple text displayer on the 3DS screen. /// [`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")] #[doc(alias = "PrintConsole")]
pub struct Console<'screen> { pub struct Console<'screen> {
context: Box<PrintConsole>, context: Box<PrintConsole>,
screen: RefMut<'screen, dyn Screen>, screen: RefMut<'screen, dyn ConsoleScreen>,
} }
impl<'screen> Console<'screen> { impl<'screen> Console<'screen> {
@ -102,7 +106,7 @@ impl<'screen> Console<'screen> {
/// # } /// # }
/// ``` /// ```
#[doc(alias = "consoleInit")] #[doc(alias = "consoleInit")]
pub fn new(screen: RefMut<'screen, dyn Screen>) -> Self { pub fn new<S: ConsoleScreen>(screen: RefMut<'screen, S>) -> Self {
let mut context = Box::<PrintConsole>::default(); let mut context = Box::<PrintConsole>::default();
unsafe { consoleInit(screen.as_raw(), context.as_mut()) }; 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<'_> { impl Drop for Console<'_> {
fn drop(&mut self) { fn drop(&mut self) {
unsafe { unsafe {

14
ctru-rs/src/error.rs

@ -94,6 +94,8 @@ pub enum Error {
/// Size of the requested data (in bytes). /// Size of the requested data (in bytes).
wanted: usize, wanted: usize,
}, },
/// An error that doesn't fit into the other categories.
Other(String),
} }
impl Error { impl Error {
@ -113,6 +115,14 @@ impl Error {
// Copy out of the error string, since it may be changed by other libc calls later // Copy out of the error string, since it may be changed by other libc calls later
Self::Libc(error_str.to_string_lossy().into()) 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<ctru_sys::Result> for Error { impl From<ctru_sys::Result> for Error {
@ -146,6 +156,7 @@ impl fmt::Debug for Error {
.field("provided", provided) .field("provided", provided)
.field("wanted", wanted) .field("wanted", wanted)
.finish(), .finish(),
Self::Other(err) => f.debug_tuple("Other").field(err).finish(),
} }
} }
} }
@ -168,7 +179,8 @@ impl fmt::Display for Error {
Self::OutputAlreadyRedirected => { Self::OutputAlreadyRedirected => {
write!(f, "output streams are already redirected to 3dslink") 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}"),
} }
} }
} }

1
ctru-rs/src/lib.rs

@ -64,6 +64,7 @@ pub mod linear;
pub mod mii; pub mod mii;
pub mod os; pub mod os;
pub mod prelude; pub mod prelude;
mod sealed;
pub mod services; pub mod services;
pub use crate::error::{Error, Result}; pub use crate::error::{Error, Result};

15
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<'_> {}

19
ctru-rs/src/services/gfx.rs

@ -9,28 +9,17 @@ use std::marker::PhantomData;
use std::sync::Mutex; use std::sync::Mutex;
use crate::error::Result; use crate::error::Result;
use crate::sealed::Sealed;
use crate::services::gspgpu::{self, FramebufferFormat}; use crate::services::gspgpu::{self, FramebufferFormat};
use crate::services::ServiceReference; 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. /// Trait to handle common functionality for all screens.
/// ///
/// This trait is implemented by the screen structs for working with frame buffers and /// 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 /// drawing to the screens. Graphics-related code can be made generic over this
/// trait to work with any of the given screens. /// trait to work with any of the given screens.
#[doc(alias = "gfxScreen_t")] #[doc(alias = "gfxScreen_t")]
pub trait Screen: private::Sealed { pub trait Screen: Sealed {
/// Returns the `libctru` value for the Screen kind. /// Returns the `libctru` value for the Screen kind.
fn as_raw(&self) -> ctru_sys::gfxScreen_t; 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. /// 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. /// 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. /// Swaps the video buffers.
/// ///
/// Even if double buffering is disabled, "swapping" the buffers has the side effect /// 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. /// A screen with buffers that can be flushed.
/// ///
/// This trait applies to any [`Screen`] that has data written to its frame buffer. /// 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. /// 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. /// Note that you must still call [`Swap::swap_buffers`] after this method for the buffer contents to be displayed.

496
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<Option<IrUserState>> = 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<Self> {
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<Handle> {
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<Handle> {
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<Vec<IrUserPacket>, 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<u32>,
expected_response_len: usize,
) -> crate::Result<Vec<u32>> {
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<u8>,
/// 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<Self, Self::Error> {
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,
})
}
}

2
ctru-rs/src/services/mod.rs

@ -19,11 +19,13 @@ pub mod fs;
pub mod gfx; pub mod gfx;
pub mod gspgpu; pub mod gspgpu;
pub mod hid; pub mod hid;
pub mod ir_user;
pub mod ndsp; pub mod ndsp;
pub mod ps; pub mod ps;
mod reference; mod reference;
pub mod soc; pub mod soc;
pub mod sslc; pub mod sslc;
pub mod svc;
cfg_if::cfg_if! { cfg_if::cfg_if! {
if #[cfg(all(feature = "romfs", romfs_exists))] { if #[cfg(all(feature = "romfs", romfs_exists))] {

76
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<u32>,
expected_response_len: usize,
) -> crate::Result<Vec<u32>>;
}
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<u32>,
expected_response_len: usize,
) -> crate::Result<Vec<u32>> {
// 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)
}
Loading…
Cancel
Save