diff --git a/ctru-rs/Cargo.toml b/ctru-rs/Cargo.toml index 4d6b363..b6f7ce5 100644 --- a/ctru-rs/Cargo.toml +++ b/ctru-rs/Cargo.toml @@ -24,6 +24,7 @@ shim-3ds = { git = "https://github.com/rust3ds/shim-3ds.git" } pthread-3ds = { git = "https://github.com/rust3ds/pthread-3ds.git" } libc = "0.2.121" bitflags = "2.3.3" +macaddr = "1.0.1" [build-dependencies] toml = "0.5" diff --git a/ctru-rs/examples/local-networking.rs b/ctru-rs/examples/local-networking.rs new file mode 100644 index 0000000..03f072f --- /dev/null +++ b/ctru-rs/examples/local-networking.rs @@ -0,0 +1,282 @@ +//! Local networking example. +//! +//! This example showcases local networking using the UDS module. + +use ctru::prelude::*; +use ctru::services::uds::*; + +fn main() { + let apt = Apt::new().unwrap(); + let mut hid = Hid::new().unwrap(); + let gfx = Gfx::new().unwrap(); + let console = Console::new(gfx.top_screen.borrow_mut()); + + println!("Local networking demo"); + + let mut uds = Uds::new(None).unwrap(); + + println!("UDS initialised"); + + enum State { + Initialised, + Scanning, + DrawList, + List, + Connect, + Connected, + Create, + Created, + } + + let mut state = State::Initialised; + + println!("Press A to start scanning or B to create a new network"); + + let mut networks = vec![]; + let mut selected_network = 0; + + let mut mode = ConnectionType::Client; + + let mut channel = 0; + let data_channel = 1; + + while apt.main_loop() { + gfx.wait_for_vblank(); + + hid.scan_input(); + if hid.keys_down().contains(KeyPad::START) { + break; + } + + match state { + State::Initialised => { + if hid.keys_down().contains(KeyPad::A) { + state = State::Scanning; + console.clear(); + } else if hid.keys_down().contains(KeyPad::B) { + state = State::Create; + console.clear(); + } + } + State::Scanning => { + println!("Scanning..."); + + let nwks = uds.scan(b"HBW\x10", None, None); + + match nwks { + Ok(n) => { + if n.is_empty() { + state = State::Initialised; + console.clear(); + println!("Scanned successfully; no networks found"); + println!("Press A to start scanning or B to create a new network"); + } else { + networks = n; + selected_network = 0; + state = State::DrawList; + } + } + Err(e) => { + state = State::Initialised; + console.clear(); + eprintln!("Error while scanning: {e}"); + println!("Press A to start scanning or B to create a new network"); + } + } + } + State::DrawList => { + console.clear(); + + println!( + "Scanned successfully; {} network{} found", + networks.len(), + if networks.len() == 1 { "" } else { "s" } + ); + + println!("D-Pad to select, A to connect as client, R + A to connect as spectator, B to create a new network"); + + for (index, n) in networks.iter().enumerate() { + println!( + "{} Username: {}", + if index == selected_network { ">" } else { " " }, + n.nodes[0].as_ref().unwrap().username + ); + } + + state = State::List; + } + State::List => { + if hid.keys_down().contains(KeyPad::UP) && selected_network > 0 { + selected_network -= 1; + state = State::DrawList; + } else if hid.keys_down().contains(KeyPad::DOWN) + && selected_network < networks.len() - 1 + { + selected_network += 1; + state = State::DrawList; + } else if hid.keys_down().contains(KeyPad::A) { + state = State::Connect; + mode = if hid.keys_held().contains(KeyPad::R) { + ConnectionType::Spectator + } else { + ConnectionType::Client + }; + } else if hid.keys_down().contains(KeyPad::B) { + state = State::Create; + } + } + State::Connect => { + let appdata = uds + .get_network_appdata(&networks[selected_network], None) + .unwrap(); + println!("App data: {:02X?}", appdata); + + if let Err(e) = uds.connect_network( + &networks[selected_network], + b"udsdemo passphrase c186093cd2652741\0", + mode, + data_channel, + ) { + console.clear(); + eprintln!("Error while connecting to network: {e}"); + state = State::Initialised; + println!("Press A to start scanning or B to create a new network"); + } else { + channel = uds.get_channel().unwrap(); + println!("Connected using channel {}", channel); + + let appdata = uds.get_appdata(None).unwrap(); + println!("App data: {:02X?}", appdata); + + if uds.wait_status_event(false, false).unwrap() { + println!("Connection status event signalled"); + let status = uds.get_connection_status().unwrap(); + println!("Status: {status:#02X?}"); + } + + println!("Press A to stop data transfer"); + state = State::Connected; + } + } + State::Connected => { + let packet = uds.pull_packet(); + + match packet { + Ok(p) => { + if let Some((pkt, node)) = p { + println!( + "{:02X}{:02X}{:02X}{:02X} from {:04X}", + pkt[0], pkt[1], pkt[2], pkt[3], node + ); + } + + if uds.wait_status_event(false, false).unwrap() { + println!("Connection status event signalled"); + let status = uds.get_connection_status().unwrap(); + println!("Status: {status:#02X?}"); + } + + if hid.keys_down().contains(KeyPad::A) { + uds.disconnect_network().unwrap(); + state = State::Initialised; + console.clear(); + println!("Press A to start scanning or B to create a new network"); + } else if !hid.keys_down().is_empty() || !hid.keys_up().is_empty() { + let transfer_data = hid.keys_held().bits(); + if mode != ConnectionType::Spectator { + uds.send_packet( + &transfer_data.to_le_bytes(), + ctru_sys::UDS_BROADCAST_NETWORKNODEID as _, + data_channel, + SendFlags::Default, + ) + .unwrap(); + } + } + } + Err(e) => { + uds.disconnect_network().unwrap(); + console.clear(); + eprintln!("Error while grabbing packet from network: {e:#?}"); + state = State::Initialised; + println!("Press A to start scanning or B to create a new network"); + } + } + } + State::Create => { + console.clear(); + println!("Creating network..."); + + match uds.create_network( + b"HBW\x10", + None, + None, + b"udsdemo passphrase c186093cd2652741\0", + data_channel, + ) { + Ok(_) => { + let appdata = [0x69u8, 0x8a, 0x05, 0x5c] + .into_iter() + .chain((*b"Test appdata.").into_iter()) + .chain(std::iter::repeat(0).take(3)) + .collect::>(); + + uds.set_appdata(&appdata).unwrap(); + + println!("Press A to stop data transfer"); + state = State::Created; + } + Err(e) => { + console.clear(); + eprintln!("Error while creating network: {e}"); + state = State::Initialised; + println!("Press A to start scanning or B to create a new network"); + } + } + } + State::Created => { + let packet = uds.pull_packet(); + + match packet { + Ok(p) => { + if let Some((pkt, node)) = p { + println!( + "{:02X}{:02X}{:02X}{:02X} from {:04X}", + pkt[0], pkt[1], pkt[2], pkt[3], node + ); + } + + if uds.wait_status_event(false, false).unwrap() { + println!("Connection status event signalled"); + let status = uds.get_connection_status().unwrap(); + println!("Status: {status:#02X?}"); + } + + if hid.keys_down().contains(KeyPad::A) { + uds.destroy_network().unwrap(); + state = State::Initialised; + console.clear(); + println!("Press A to start scanning or B to create a new network"); + } else if !hid.keys_down().is_empty() || !hid.keys_up().is_empty() { + let transfer_data = hid.keys_held().bits(); + uds.send_packet( + &transfer_data.to_le_bytes(), + ctru_sys::UDS_BROADCAST_NETWORKNODEID as _, + data_channel, + SendFlags::Default, + ) + .unwrap(); + } + } + Err(e) => { + uds.destroy_network().unwrap(); + console.clear(); + eprintln!("Error while grabbing packet from network: {e:#?}"); + state = State::Initialised; + println!("Press A to start scanning or B to create a new network"); + } + } + } + } + } +} diff --git a/ctru-rs/src/lib.rs b/ctru-rs/src/lib.rs index 2874839..3aa9313 100644 --- a/ctru-rs/src/lib.rs +++ b/ctru-rs/src/lib.rs @@ -21,6 +21,7 @@ #![feature(custom_test_frameworks)] #![feature(try_trait_v2)] #![feature(allocator_api)] +#![feature(new_uninit)] #![test_runner(test_runner::run_gdb)] // TODO: does this make sense to have configurable? #![doc( html_favicon_url = "https://user-images.githubusercontent.com/11131775/225929072-2fa1741c-93ae-4b47-9bdf-af70f3d59910.png" diff --git a/ctru-rs/src/services/ir_user.rs b/ctru-rs/src/services/ir_user.rs index 672fe20..7f1bd84 100644 --- a/ctru-rs/src/services/ir_user.rs +++ b/ctru-rs/src/services/ir_user.rs @@ -150,7 +150,7 @@ impl IrUser { shared_mem.shared_memory_layout, ); - Ok(()) + Ok::<_, Error>(()) })() .unwrap(); }, diff --git a/ctru-rs/src/services/mod.rs b/ctru-rs/src/services/mod.rs index 4fadeb3..e4ddb44 100644 --- a/ctru-rs/src/services/mod.rs +++ b/ctru-rs/src/services/mod.rs @@ -26,6 +26,7 @@ mod reference; pub mod soc; pub mod sslc; pub mod svc; +pub mod uds; cfg_if::cfg_if! { if #[cfg(all(feature = "romfs", romfs_exists))] { diff --git a/ctru-rs/src/services/uds.rs b/ctru-rs/src/services/uds.rs new file mode 100644 index 0000000..bf21bf3 --- /dev/null +++ b/ctru-rs/src/services/uds.rs @@ -0,0 +1,960 @@ +//! UDS (local networking) service. +//! +//! The UDS service is used to handle local networking, i.e. peer-to-peer networking used for local multiplayer. +//! This module also covers some functionality used in Download Play (dlp); there is a specific module for DLP, but it can also be implemented manually using UDS. +#![doc(alias = "network")] +#![doc(alias = "dlplay")] + +use std::ffi::CString; +use std::fmt::Debug; +use std::mem::MaybeUninit; +use std::ops::FromResidual; +use std::ptr::null; +use std::sync::Mutex; + +use crate::error::ResultCode; +use crate::services::ServiceReference; + +use bitflags::bitflags; +use macaddr::MacAddr6; + +bitflags! { + /// Flags used for sending packets to a network. + #[allow(missing_docs)] + #[derive(Copy, Clone, Debug, PartialEq, Eq)] + pub struct SendFlags: u8 { + /// According to libctru source, it's not really known what these do. + const Default = ctru_sys::UDS_SENDFLAG_Default as u8; + const Broadcast = ctru_sys::UDS_SENDFLAG_Broadcast as u8; + } +} + +/// Error enum for generic errors within the [`Uds`] service. +#[non_exhaustive] +#[derive(Debug)] +pub enum Error { + /// The provided username was too long. + UsernameTooLong, + /// The provided username contained a NULL byte. + UsernameContainsNull, + /// Not connected to a network. + NotConnected, + /// No context bound. + NoContext, + /// Cannot send data on a network as a spectator. + Spectator, + /// No network created. + NoNetwork, + /// The provided app data buffer was too large. + TooMuchAppData, + /// ctru-rs error + Lib(crate::Error), +} + +impl From for Error { + fn from(value: crate::Error) -> Self { + Error::Lib(value) + } +} + +impl FromResidual for Result { + fn from_residual(residual: crate::Error) -> Self { + Err(residual.into()) + } +} + +/// Possible types of connection to a network +/// +#[doc(alias = "udsConnectionType")] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u32)] +pub enum ConnectionType { + /// A normal client. Can push packets to the network. + Client = ctru_sys::UDSCONTYPE_Client, + /// A spectator. Cannot push packets to the network, + /// but doesn't need the passphrase to join. + Spectator = ctru_sys::UDSCONTYPE_Spectator, +} + +impl From for u8 { + fn from(value: ConnectionType) -> Self { + value as Self + } +} + +impl TryFrom for ConnectionType { + type Error = (); + + fn try_from(value: u8) -> std::result::Result { + match value as u32 { + ctru_sys::UDSCONTYPE_Client => Ok(Self::Client), + ctru_sys::UDSCONTYPE_Spectator => Ok(Self::Spectator), + _ => Err(()), + } + } +} + +/// Information about a network node. +/// Ported to Rust so that Debug can be derived on it. +#[allow(missing_docs)] +#[doc(alias = "udsNodeInfo")] +#[derive(Debug)] +pub struct NodeInfo { + pub uds_friendcodeseed: u64, + pub username: String, + pub unk_x1c: u16, + pub flag: u8, + pub pad_x1f: u8, + pub node_id: u16, + pub pad_x22: u16, + pub word_x24: u32, +} + +impl From for NodeInfo { + fn from(value: ctru_sys::udsNodeInfo) -> Self { + unsafe { + Self { + uds_friendcodeseed: value.uds_friendcodeseed, + username: String::from_utf16_lossy( + &value.__bindgen_anon_1.__bindgen_anon_1.username, + ), + unk_x1c: value.__bindgen_anon_1.__bindgen_anon_1.unk_x1c, + flag: value.__bindgen_anon_1.__bindgen_anon_1.flag, + pad_x1f: value.__bindgen_anon_1.__bindgen_anon_1.pad_x1f, + node_id: value.NetworkNodeID, + pad_x22: value.pad_x22, + word_x24: value.word_x24, + } + } + } +} + +/// Information returned from scanning for networks. +/// Ported to Rust so that Debug can be derived on it. +#[allow(missing_docs)] +#[doc(alias = "udsNetworkScanInfo")] +#[derive(Debug)] +pub struct NetworkScanInfo { + pub datareply_entry: ctru_sys::nwmBeaconDataReplyEntry, + pub network: ctru_sys::udsNetworkStruct, + pub nodes: [Option; 16], +} + +impl From for NetworkScanInfo { + fn from(value: ctru_sys::udsNetworkScanInfo) -> Self { + Self { + datareply_entry: value.datareply_entry, + network: value.network, + nodes: value.nodes.map(|n| { + if n.uds_friendcodeseed != 0 { + Some(n.into()) + } else { + None + } + }), + } + } +} + +/// Status of the service handle. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum ServiceStatus { + /// Not connected to or hosting a network. + Disconnected, + /// Connected to a network. + Client, + /// Hosting a network. + Server, +} + +/// Handle to the UDS service. +pub struct Uds { + _service_handler: ServiceReference, + context: Option, + network: Option, +} + +static UDS_ACTIVE: Mutex<()> = Mutex::new(()); + +impl Uds { + /// Size of one frame. + const RECV_FRAME_SIZE: usize = ctru_sys::UDS_DATAFRAME_MAXSIZE as usize; + + /// Size of receive buffer; max frame size * 8. + const RECV_BUF_SIZE: u32 = ctru_sys::UDS_DEFAULT_RECVBUFSIZE; + + /// Shared memory size; must be slightly larger + /// than `RECV_BUF_SIZE`. + const SHAREDMEM_SIZE: usize = 0x3000; + + /// Buffer used while scanning for networks. + /// This value is taken from the devkitPRO example. + const SCAN_BUF_SIZE: usize = 0x4000; + + /// The maximum number of nodes that can ever be connected + /// to a network (16). Can be further limited. + const MAX_NODES: u8 = ctru_sys::UDS_MAXNODES as u8; + + /// The maximum amount of app data any server can provide. + /// Limited by the size of a struct in libctru. + const MAX_APPDATA_SIZE: usize = 200; + + /// Retrieve the current status of the service. + pub fn service_status(&self) -> ServiceStatus { + match (self.context, self.network) { + (None, None) => ServiceStatus::Disconnected, + (Some(_), None) => ServiceStatus::Client, + (Some(_), Some(_)) => ServiceStatus::Server, + _ => unreachable!(), + } + } + + /// Initialise a new service handle. + /// No `new_with_buffer_size` function is provided, as there isn't really a + /// reason to use any size other than the default. + /// + /// # Errors + /// + /// This function will return an error if the [`Uds`] service is already being used, + /// or if the provided username is invalid (longer than 10 bytes or contains a NULL byte). + /// + /// # Example + /// + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// # + /// use ctru::services::uds::Uds; + /// + /// let uds = Uds::new(None)?; + /// # + /// # Ok(()) + /// # } + /// ``` + #[doc(alias = "udsInit")] + pub fn new(username: Option<&str>) -> Result { + if let Some(n) = username { + if n.len() > 10 { + return Err(Error::UsernameTooLong); + } + } + let cstr = username.map(CString::new); + let cstr = if let Some(conv) = cstr { + match conv { + Ok(c) => Some(c), + Err(_) => return Err(Error::UsernameContainsNull), + } + } else { + None + }; + let handler = ServiceReference::new( + &UDS_ACTIVE, + || { + let ptr = cstr.map(|c| c.as_ptr()).unwrap_or(null()); + + ResultCode(unsafe { ctru_sys::udsInit(Self::SHAREDMEM_SIZE, ptr) })?; + + Ok(()) + }, + || unsafe { + ctru_sys::udsExit(); + }, + )?; + + Ok(Self { + _service_handler: handler, + context: None, + network: None, + }) + } + + /// Scan the UDS service for all available beacons broadcasting with the given IDs. + /// + /// This function must be called to obtain network objects that can later be connected to. + /// + /// # Example + /// + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// # + /// use ctru::services::uds::Uds; + /// let uds = Uds::new(None)?; + /// + /// let networks = uds.scan(b"HBW\x10", None, None)?; + /// # + /// # Ok(()) + /// # } + /// ``` + #[doc(alias = "udsScanBeacons")] + pub fn scan( + &self, + comm_id: &[u8; 4], + additional_id: Option, + whitelist_macaddr: Option, + ) -> crate::Result> { + // normally, I construct this on the stack, but this example seems to have the stack size + // set too small for that, which causes a segfault. + let mut scan_buf = Box::<[u8; Self::SCAN_BUF_SIZE]>::new_zeroed(); + + let mut networks = MaybeUninit::uninit(); + let mut total_networks = MaybeUninit::uninit(); + + ResultCode(unsafe { + ctru_sys::udsScanBeacons( + scan_buf.as_mut_ptr().cast(), + Self::SCAN_BUF_SIZE, + networks.as_mut_ptr(), + total_networks.as_mut_ptr(), + u32::from_be_bytes(*comm_id), + additional_id.unwrap_or(0), + whitelist_macaddr + .map(|m| m.as_bytes().as_ptr()) + .unwrap_or(null()), + self.service_status() == ServiceStatus::Client, + ) + })?; + + unsafe { + scan_buf.assume_init_drop(); + } + + let networks = unsafe { networks.assume_init() }; + let total_networks = unsafe { total_networks.assume_init() }; + + let networks = if total_networks > 0 { + // Safety: `networks` is malloced in application memory with size = `total_networks` + unsafe { Vec::from_raw_parts(networks, total_networks, total_networks) } + .into_iter() + .map(<_ as Into>::into) + .collect() + } else { + vec![] + }; + + Ok(networks) + } + + /// Retrieve app data for a network which the service is not connected to. + /// + /// # Example + /// + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// # + /// use ctru::services::uds::Uds; + /// let uds = Uds::new(None)?; + /// + /// let networks = uds.scan(b"HBW\x10", None, None)?; + /// let appdata = uds.get_network_appdata(&networks[0], None)?; + /// # + /// # Ok(()) + /// # } + /// ``` + #[doc(alias = "udsGetNetworkStructApplicationData")] + pub fn get_network_appdata( + &self, + network: &NetworkScanInfo, + max_size: Option, + ) -> crate::Result> { + let mut appdata_buffer = vec![ + 0u8; + max_size + .unwrap_or(Self::MAX_APPDATA_SIZE) + .min(Self::MAX_APPDATA_SIZE) + ]; + + let mut actual_size = MaybeUninit::uninit(); + + ResultCode(unsafe { + ctru_sys::udsGetNetworkStructApplicationData( + &network.network as *const _, + appdata_buffer.as_mut_ptr().cast(), + appdata_buffer.len(), + actual_size.as_mut_ptr(), + ) + })?; + + let actual_size = unsafe { actual_size.assume_init() }; + + Ok(appdata_buffer[..actual_size].to_vec()) + } + + /// Retrieve app data for the currently connected network. + /// + /// # Errors + /// + /// This function will return an error if the service is not connected to a network. + /// See [`Uds::connect_network()`] to connect to a network. + /// + /// # Example + /// + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// # + /// use ctru::services::uds::{ConnectionType, Uds}; + /// let uds = Uds::new(None)?; + /// + /// let networks = uds.scan(b"HBW\x10", None, None)?; + /// uds.connect_network(&networks[0], b"udsdemo passphrase c186093cd2652741\0", ConnectionType::Client, 1)?; + /// let appdata = uds.get_appdata(None)?; + /// # + /// # Ok(()) + /// # } + /// ``` + #[doc(alias = "udsGetApplicationData")] + pub fn get_appdata(&self, max_size: Option) -> Result, Error> { + if self.service_status() == ServiceStatus::Disconnected { + return Err(Error::NotConnected); + } + + let mut appdata_buffer = vec![ + 0u8; + max_size + .unwrap_or(Self::MAX_APPDATA_SIZE) + .min(Self::MAX_APPDATA_SIZE) + ]; + + let mut actual_size = MaybeUninit::uninit(); + + ResultCode(unsafe { + ctru_sys::udsGetApplicationData( + appdata_buffer.as_mut_ptr().cast(), + appdata_buffer.len(), + actual_size.as_mut_ptr(), + ) + })?; + + let actual_size = unsafe { actual_size.assume_init() }; + + Ok(appdata_buffer[..actual_size].to_vec()) + } + + /// Connect to a network. + /// + /// # Example + /// + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// # + /// use ctru::services::uds::{ConnectionType, Uds}; + /// let mut uds = Uds::new(None)?; + /// + /// let networks = uds.scan(b"HBW\x10", None, None)?; + /// uds.connect_network(&networks[0], b"udsdemo passphrase c186093cd2652741\0", ConnectionType::Client, 1)?; + /// # + /// # Ok(()) + /// # } + /// ``` + #[doc(alias = "udsConnectNetwork")] + pub fn connect_network( + &mut self, + network: &NetworkScanInfo, + passphrase: &[u8], + connection_type: ConnectionType, + channel: u8, + ) -> crate::Result<()> { + let mut context = MaybeUninit::uninit(); + + ResultCode(unsafe { + ctru_sys::udsConnectNetwork( + &network.network as *const _, + passphrase.as_ptr().cast(), + passphrase.len(), + context.as_mut_ptr(), + ctru_sys::UDS_BROADCAST_NETWORKNODEID as u16, + connection_type as u32, + channel, + Self::RECV_BUF_SIZE, + ) + })?; + + let context = unsafe { context.assume_init() }; + + self.context.replace(context); + + Ok(()) + } + + /// Disconnect from a network. + /// + /// # Errors + /// + /// This function will return an error if the service is not connected to a network. + /// See [`Uds::connect_network()`] to connect to a network. + /// + /// # Example + /// + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// # + /// use ctru::services::uds::{ConnectionType, Uds}; + /// let mut uds = Uds::new(None)?; + /// + /// let networks = uds.scan(b"HBW\x10", None, None)?; + /// uds.connect_network(&networks[0], b"udsdemo passphrase c186093cd2652741\0", ConnectionType::Client, 1)?; + /// uds.disconnect_network()?; + /// # + /// # Ok(()) + /// # } + /// ``` + #[doc(alias = "udsDisconnectNetwork")] + pub fn disconnect_network(&mut self) -> Result<(), Error> { + if self.service_status() != ServiceStatus::Client { + return Err(Error::NotConnected); + } + + if self.context.is_some() { + self.unbind_context()?; + } + + ResultCode(unsafe { ctru_sys::udsDisconnectNetwork() })?; + + Ok(()) + } + + /// Unbind the connection context. + /// + /// Normally, there's no reason to call this function, + /// since [`Uds::disconnect_network()`] and [`Uds::destroy_network()`] both automatically unbind their contexts. + /// + /// # Errors + /// + /// This function will return an error if no context is currently bound (i.e. the service is neither connected to nor hosting a network). + /// See [`Uds::connect_network()`] to connect to a network or [`Uds::create_network()`] to create one. + /// + /// # Example + /// + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// # + /// use ctru::services::uds::{ConnectionType, Uds}; + /// let mut uds = Uds::new(None)?; + /// + /// let networks = uds.scan(b"HBW\x10", None, None)?; + /// uds.connect_network(&networks[0], b"udsdemo passphrase c186093cd2652741\0", ConnectionType::Client, 1)?; + /// uds.unbind_context()?; + /// # + /// # Ok(()) + /// # } + /// ``` + #[doc(alias = "udsUnbind")] + pub fn unbind_context(&mut self) -> Result<(), Error> { + if let Some(mut ctx) = self.context { + ResultCode(unsafe { ctru_sys::udsUnbind(&mut ctx as *mut _) })?; + } else { + return Err(Error::NoContext); + } + + self.context = None; + + Ok(()) + } + + /// Returns the Wi-Fi channel currently in use. + /// + /// # Errors + /// + /// This function will return an error if the service is currently neither connected to nor hosting a network. + /// See [`Uds::connect_network()`] to connect to a network or [`Uds::create_network()`] to create one. + /// + /// # Example + /// + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// # + /// use ctru::services::uds::{ConnectionType, Uds}; + /// let mut uds = Uds::new(None)?; + /// + /// let networks = uds.scan(b"HBW\x10", None, None)?; + /// uds.connect_network(&networks[0], b"udsdemo passphrase c186093cd2652741\0", ConnectionType::Client, 1)?; + /// let channel = uds.get_channel()?; + /// # + /// # Ok(()) + /// # } + /// ``` + #[doc(alias = "udsGetChannel")] + pub fn get_channel(&self) -> Result { + if self.service_status() == ServiceStatus::Disconnected { + return Err(Error::NotConnected); + } + + let mut channel = MaybeUninit::uninit(); + + ResultCode(unsafe { ctru_sys::udsGetChannel(channel.as_mut_ptr()) })?; + + let channel = unsafe { channel.assume_init() }; + + Ok(channel) + } + + /// Wait for a ConnectionStatus event to occur. + /// + /// If `next` is `true`, discard the current event (if any) and wait for the next one. + /// + /// If `wait` is `true`, block until an event is signalled, else return `false` if no event. + /// + /// Always returns `true`, unless `wait` is `false` and no event has been signalled. + /// + /// # Errors + /// + /// This function will return an error if the service is currently neither connected to nor hosting a network. + /// See [`Uds::connect_network()`] to connect to a network or [`Uds::create_network()`] to create one. + /// + /// # Example + /// + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// # + /// use ctru::services::uds::{ConnectionType, Uds}; + /// let mut uds = Uds::new(None)?; + /// + /// let networks = uds.scan(b"HBW\x10", None, None)?; + /// uds.connect_network(&networks[0], b"udsdemo passphrase c186093cd2652741\0", ConnectionType::Client, 1)?; + /// if uds.wait_status_event(false, false)? { + /// println!("Event signalled"); + /// } + /// # + /// # Ok(()) + /// # } + /// ``` + #[doc(alias = "udsWaitConnectionStatusEvent")] + pub fn wait_status_event(&self, next: bool, wait: bool) -> Result { + if self.service_status() == ServiceStatus::Disconnected { + return Err(Error::NotConnected); + } + + Ok(unsafe { ctru_sys::udsWaitConnectionStatusEvent(next, wait) }) + } + + /// Returns the current [`ctru_sys::udsConnectionStatus`] struct. + /// + /// TODO: should this return an error if not connected? + /// + /// # Example + /// + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// # + /// use ctru::services::uds::{ConnectionType, Uds}; + /// let mut uds = Uds::new(None)?; + /// + /// let networks = uds.scan(b"HBW\x10", None, None)?; + /// uds.connect_network(&networks[0], b"udsdemo passphrase c186093cd2652741\0", ConnectionType::Client, 1)?; + /// if uds.wait_status_event(false, false)? { + /// println!("Connection status event signalled"); + /// let status = uds.get_connection_status()?; + /// println!("Status: {status:#X?}"); + /// } + /// # + /// # Ok(()) + /// # } + /// ``` + #[doc(alias = "udsGetConnectionStatus")] + pub fn get_connection_status(&self) -> crate::Result { + let mut status = MaybeUninit::uninit(); + + ResultCode(unsafe { ctru_sys::udsGetConnectionStatus(status.as_mut_ptr()) })?; + + let status = unsafe { status.assume_init() }; + + Ok(status) + } + + /// Send a packet to the network. + /// + /// TODO: max size? + /// + /// # Errors + /// + /// This function will return an error if the service is currently neither connected to nor hosting a network. + /// See [`Uds::connect_network()`] to connect to a network or [`Uds::create_network()`] to create one. + /// It will also return an error if the service is currently connected to a network as a spectator, as spectators cannot send data, only receive it. + /// + /// # Example + /// + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// # + /// use ctru::services::uds::{ConnectionType, SendFlags, Uds}; + /// let mut uds = Uds::new(None)?; + /// + /// let networks = uds.scan(b"HBW\x10", None, None)?; + /// uds.connect_network(&networks[0], b"udsdemo passphrase c186093cd2652741\0", ConnectionType::Client, 1)?; + /// uds.send_packet(b"Hello, World!", ctru_sys::UDS_BROADCAST_NETWORKNODEID as u16, 1, SendFlags::Default)?; + /// # + /// # Ok(()) + /// # } + /// ``` + #[doc(alias = "udsSendTo")] + pub fn send_packet( + &self, + packet: &[u8], + to_nodes: u16, + channel: u8, + flags: SendFlags, + ) -> Result<(), Error> { + if self.service_status() == ServiceStatus::Disconnected { + return Err(Error::NotConnected); + } + + if self.context.unwrap().spectator { + return Err(Error::Spectator); + } + + let code = ResultCode(unsafe { + ctru_sys::udsSendTo( + to_nodes, + channel, + flags.bits(), + packet.as_ptr().cast(), + packet.len(), + ) + }); + + if code.0 + != ctru_sys::MAKERESULT( + ctru_sys::RL_STATUS as _, + ctru_sys::RS_OUTOFRESOURCE as _, + ctru_sys::RM_UDS as _, + ctru_sys::RD_BUSY as _, + ) + { + code?; + } + + Ok(()) + } + + /// Pull a packet from the network. + /// + /// # Errors + /// + /// This function will return an error if the service is currently neither connected to nor hosting a network. + /// See [`Uds::connect_network()`] to connect to a network or [`Uds::create_network()`] to create one. + /// + /// # Example + /// + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// # + /// use ctru::services::uds::{ConnectionType, SendFlags, Uds}; + /// let mut uds = Uds::new(None)?; + /// + /// let networks = uds.scan(b"HBW\x10", None, None)?; + /// uds.connect_network(&networks[0], b"udsdemo passphrase c186093cd2652741\0", ConnectionType::Client, 1)?; + /// let packet = uds.pull_packet()?; + /// # + /// # Ok(()) + /// # } + /// ``` + #[doc(alias = "udsPullPacket")] + pub fn pull_packet(&self) -> Result, u16)>, Error> { + if self.service_status() == ServiceStatus::Disconnected { + return Err(Error::NotConnected); + } + + let mut frame = MaybeUninit::<[u8; Self::RECV_FRAME_SIZE]>::zeroed(); + + let mut actual_size = MaybeUninit::uninit(); + let mut src_node_id = MaybeUninit::uninit(); + + ResultCode(unsafe { + ctru_sys::udsPullPacket( + &self.context.unwrap() as *const _, + frame.as_mut_ptr().cast(), + Self::RECV_FRAME_SIZE, + actual_size.as_mut_ptr(), + src_node_id.as_mut_ptr(), + ) + })?; + + let frame = unsafe { frame.assume_init() }; + let actual_size = unsafe { actual_size.assume_init() }; + let src_node_id = unsafe { src_node_id.assume_init() }; + + Ok(if actual_size == 0 { + None + } else { + Some((frame[..actual_size].to_vec(), src_node_id)) + }) + } + + /// Create a new network. + /// + /// # Errors + /// + /// This function will return an error if the [`Uds`] service is already being used. + /// + /// # Example + /// + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// # + /// use ctru::services::uds::{ConnectionType, SendFlags, Uds}; + /// let mut uds = Uds::new(None)?; + /// + /// uds.create_network(b"HBW\x10", None, None, b"udsdemo passphrase c186093cd2652741\0", 1)?; + /// # + /// # Ok(()) + /// # } + /// ``` + #[doc(alias = "udsCreateNetwork")] + pub fn create_network( + &mut self, + comm_id: &[u8; 4], + additional_id: Option, + max_nodes: Option, + passphrase: &[u8], + channel: u8, + ) -> crate::Result<()> { + let mut network = MaybeUninit::uninit(); + unsafe { + ctru_sys::udsGenerateDefaultNetworkStruct( + network.as_mut_ptr(), + u32::from_be_bytes(*comm_id), + additional_id.unwrap_or(0), + max_nodes.unwrap_or(Self::MAX_NODES).min(Self::MAX_NODES), + ) + }; + + let network = unsafe { network.assume_init() }; + + let mut context = MaybeUninit::uninit(); + + ResultCode(unsafe { + ctru_sys::udsCreateNetwork( + &network as *const _, + passphrase.as_ptr().cast(), + passphrase.len(), + context.as_mut_ptr(), + channel, + Self::RECV_BUF_SIZE, + ) + })?; + + let context = unsafe { context.assume_init() }; + + self.network.replace(network); + + self.context.replace(context); + + Ok(()) + } + + /// Destroy the current network. + /// + /// # Errors + /// + /// This function will return an error if no network has been created. + /// See [`Uds::create_network()`] to create a network. + /// + /// # Example + /// + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// # + /// use ctru::services::uds::{ConnectionType, SendFlags, Uds}; + /// let mut uds = Uds::new(None)?; + /// + /// uds.create_network(b"HBW\x10", None, None, b"udsdemo passphrase c186093cd2652741\0", 1)?; + /// uds.destroy_network()?; + /// # + /// # Ok(()) + /// # } + /// ``` + #[doc(alias = "udsDestroyNetwork")] + pub fn destroy_network(&mut self) -> Result<(), Error> { + if self.service_status() != ServiceStatus::Server { + return Err(Error::NoNetwork); + } + + // should always be true + if self.context.is_some() { + self.unbind_context()?; + } + + ResultCode(unsafe { ctru_sys::udsDestroyNetwork() })?; + + self.network = None; + + Ok(()) + } + + /// Set the app data for the currently hosted network. + /// + /// # Errors + /// + /// This function will return an error if no network has been created. + /// See [`Uds::create_network()`] to create a network. + /// This function will also return an error if the provided buffer is too large (see [`Uds::MAX_APPDATA_SIZE`]). + /// + /// # Example + /// + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// # + /// use ctru::services::uds::{ConnectionType, SendFlags, Uds}; + /// let mut uds = Uds::new(None)?; + /// + /// uds.create_network(b"HBW\x10", None, None, b"udsdemo passphrase c186093cd2652741\0", 1)?; + /// uds.set_appdata(b"Test appdata.\0")?; + /// # + /// # Ok(()) + /// # } + /// ``` + #[doc(alias = "udsSetApplicationData")] + pub fn set_appdata(&self, data: &[u8]) -> Result<(), Error> { + if self.service_status() != ServiceStatus::Server { + return Err(Error::NoNetwork); + } + + if data.len() > Self::MAX_APPDATA_SIZE { + return Err(Error::TooMuchAppData); + } + + ResultCode(unsafe { ctru_sys::udsSetApplicationData(data.as_ptr().cast(), data.len()) })?; + + Ok(()) + } +} + +impl Drop for Uds { + #[doc(alias = "udsExit")] + fn drop(&mut self) { + match self.service_status() { + ServiceStatus::Client => self.disconnect_network().unwrap(), + ServiceStatus::Server => self.destroy_network().unwrap(), + _ => {} + }; + // ctru_sys::udsExit() is called by the ServiceHandle + } +}