Skip to content

Commit

Permalink
Merge pull request #86 from rust3ds/feature/ir-user
Browse files Browse the repository at this point in the history
Add an ir:USER service wrapper and Circle Pad Pro example
  • Loading branch information
AzureMarker authored Jan 2, 2024
2 parents 4c0ebcd + 06d26fc commit 157824a
Show file tree
Hide file tree
Showing 9 changed files with 907 additions and 19 deletions.
269 changes: 269 additions & 0 deletions ctru-rs/examples/ir-user-circle-pad-pro.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 31 additions & 3 deletions ctru-rs/src/console.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) };

Expand Down Expand Up @@ -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<S: Screen + Swap + Flush> ConsoleScreen for S {}

/// Virtual text console.
///
/// [`Console`] lets the application redirect `stdout` and `stderr` to a simple text displayer on the 3DS screen.
Expand All @@ -60,7 +64,7 @@ pub enum Dimension {
#[doc(alias = "PrintConsole")]
pub struct Console<'screen> {
context: Box<PrintConsole>,
screen: RefMut<'screen, dyn Screen>,
screen: RefMut<'screen, dyn ConsoleScreen>,
}

impl<'screen> Console<'screen> {
Expand Down Expand Up @@ -102,7 +106,7 @@ impl<'screen> Console<'screen> {
/// # }
/// ```
#[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();

unsafe { consoleInit(screen.as_raw(), context.as_mut()) };
Expand Down Expand Up @@ -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 {
Expand Down
14 changes: 13 additions & 1 deletion ctru-rs/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<ctru_sys::Result> for Error {
Expand Down Expand Up @@ -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(),
}
}
}
Expand All @@ -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}"),
}
}
}
Expand Down
1 change: 1 addition & 0 deletions ctru-rs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Loading

0 comments on commit 157824a

Please sign in to comment.