Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an ir:USER service wrapper and Circle Pad Pro example #86

Merged
merged 46 commits into from
Jan 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
87bbc04
Add a rough ir:USER service wrapper and example
AzureMarker Dec 29, 2022
6c2b184
Got connection working and packets flowing
AzureMarker Jan 2, 2023
ebcb260
Improve packet handling and parse packets
AzureMarker Jan 2, 2023
d1deb60
Fix some result codes not getting recognized as errors
AzureMarker Jan 3, 2023
d798f56
Clean up some of the code and warnings
AzureMarker Jan 3, 2023
5a357dc
Implement ir:USER service shutdown, and do more code cleanup
AzureMarker Jan 3, 2023
39b6bc9
Tidy up the ir:USER code/example and fix a bug in the service code
AzureMarker Jan 3, 2023
8af2c96
More ir:USER service/example tidying and documentation
AzureMarker Jan 5, 2023
9aa866d
Fix some type issues in ir_user service
AzureMarker Jan 14, 2023
d859308
Refactor the ir-user example
AzureMarker Jan 14, 2023
da348b2
Update "known good" nightly version in CI
AzureMarker Jan 14, 2023
6229905
Add a srv module and move wait_for_event to it
AzureMarker Jan 28, 2023
a832cb9
Merge branch 'master' into feature/ir-user
AzureMarker Jan 28, 2023
7f63384
Initialize HID after ir:USER to try and fix New 3DS case
AzureMarker Jan 29, 2023
eb8628d
Make wait_for_event a method on Handle
AzureMarker Jan 29, 2023
86b1820
Derive Default for CirclePadProInputResponse
AzureMarker Jan 29, 2023
c55244d
Merge branch 'master' into feature/ir-user
AzureMarker May 5, 2023
0659df3
Update ir-user example after merge
AzureMarker May 5, 2023
6899f70
Merge branch 'master' of https://github.com/rust3ds/ctru-rs into feat…
Meziu Nov 22, 2023
7e99463
Remove panic handler
Meziu Nov 22, 2023
d5ae641
Fmt
Meziu Nov 22, 2023
47f1f78
Update PR to build with the latest versions
Meziu Nov 22, 2023
44a061a
Merge branch 'master' into feature/ir-user
AzureMarker Dec 23, 2023
e09ad16
Clarify a comment
AzureMarker Dec 23, 2023
87f33f1
Improve some service start/stop error handling
AzureMarker Dec 23, 2023
b4fa7bd
Improve packet parser error handling
AzureMarker Dec 23, 2023
dd9f84f
Improve safety of send_service_request
AzureMarker Dec 23, 2023
051ab26
Add enum for connection status
AzureMarker Dec 23, 2023
09361e5
Try using double buffering to reduce flickering (actually increased :/)
AzureMarker Dec 24, 2023
08b4c37
Fixed file-explorer example (Console usage)
AzureMarker Dec 24, 2023
02126e9
Add missing documentation to ir_user.rs
AzureMarker Dec 24, 2023
ef1c913
Rename ir-user example to ir-user-circle-pad-pro
AzureMarker Dec 24, 2023
1f45da1
Fix double buffering in example via impl Swap and Flush for Console
AzureMarker Dec 24, 2023
964a3d9
Remove Console::with_screen
AzureMarker Dec 26, 2023
323538b
Handle timeout int conversion better
AzureMarker Dec 26, 2023
daf6134
Add missing docs to Error::is_timeout
AzureMarker Dec 26, 2023
c6d8d88
Remove Console S generic by making ConsoleScreen trait alias
AzureMarker Dec 26, 2023
208a917
Add srv::make_ipc_header and use in ir_user
AzureMarker Dec 26, 2023
86c59ee
Simplify some of the syscall code and fix &mut unsafety
AzureMarker Dec 26, 2023
df50702
Rename srv module to svc and add Handle::send_service_request
AzureMarker Dec 26, 2023
979fd50
Simplify some of the packet parsing
AzureMarker Dec 26, 2023
fe0bb51
Update example file documentation
AzureMarker Jan 2, 2024
de1604b
Add ir:rst disable in CPP demo to improve New 3DS compatibility
AzureMarker Jan 2, 2024
1496d47
Move Sealed to separate file
AzureMarker Jan 2, 2024
5233237
Make more IrUser functions take &mut self
AzureMarker Jan 2, 2024
06d26fc
Revert one &mut self
AzureMarker Jan 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
AzureMarker marked this conversation as resolved.
Show resolved Hide resolved

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;
AzureMarker marked this conversation as resolved.
Show resolved Hide resolved

// 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.
AzureMarker marked this conversation as resolved.
Show resolved Hide resolved
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>,
}
AzureMarker marked this conversation as resolved.
Show resolved Hide resolved

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
Loading