From f065d0da3dfd647cb19abf1fefc9d7128b414b1c Mon Sep 17 00:00:00 2001 From: Jhynjhiruu Date: Thu, 8 Feb 2024 01:18:30 +0000 Subject: [PATCH 01/12] Implement UDS service with example --- ctru-rs/Cargo.toml | 1 + ctru-rs/examples/local-networking.rs | 282 ++++++++ ctru-rs/src/lib.rs | 1 + ctru-rs/src/services/ir_user.rs | 2 +- ctru-rs/src/services/mod.rs | 1 + ctru-rs/src/services/uds.rs | 960 +++++++++++++++++++++++++++ 6 files changed, 1246 insertions(+), 1 deletion(-) create mode 100644 ctru-rs/examples/local-networking.rs create mode 100644 ctru-rs/src/services/uds.rs 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 + } +} From 4fd4ef08cb35e3fec13582129a67d9b9c7d88251 Mon Sep 17 00:00:00 2001 From: Jhynjhiruu Date: Thu, 8 Feb 2024 21:26:18 +0000 Subject: [PATCH 02/12] Fix some suggestions (and hopefully rust-analyzer) --- ctru-rs/src/services/uds.rs | 78 +++++++++++++++++++++++++++++-------- 1 file changed, 62 insertions(+), 16 deletions(-) diff --git a/ctru-rs/src/services/uds.rs b/ctru-rs/src/services/uds.rs index bf21bf3..566dba8 100644 --- a/ctru-rs/src/services/uds.rs +++ b/ctru-rs/src/services/uds.rs @@ -95,19 +95,19 @@ impl TryFrom for ConnectionType { } /// Information about a network node. -/// Ported to Rust so that Debug can be derived on it. #[allow(missing_docs)] #[doc(alias = "udsNodeInfo")] #[derive(Debug)] +// Ported to Rust so that Debug can be derived on it. pub struct NodeInfo { pub uds_friendcodeseed: u64, pub username: String, - pub unk_x1c: u16, + unk_x1c: u16, pub flag: u8, - pub pad_x1f: u8, + pad_x1f: u8, pub node_id: u16, - pub pad_x22: u16, - pub word_x24: u32, + pad_x22: u16, + word_x24: u32, } impl From for NodeInfo { @@ -130,13 +130,16 @@ impl From for NodeInfo { } /// 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)] +// Ported to Rust so that Debug can be derived on it. pub struct NetworkScanInfo { + /// NWM output structure. pub datareply_entry: ctru_sys::nwmBeaconDataReplyEntry, + /// Information about the network. pub network: ctru_sys::udsNetworkStruct, + /// All nodes on the network (first node is the server, + /// max 16, `None` means no node connected). pub nodes: [Option; 16], } @@ -156,6 +159,42 @@ impl From for NetworkScanInfo { } } +/// Status of the connection. +#[doc(alias = "udsConnectionStatus")] +#[derive(Debug)] +pub struct ConnectionStatus { + // TODO: is this in some kind of readable format? + pub status: u32, + unk_x4: u32, + /// Network node ID for the current device. + pub cur_node_id: u16, + unk_xa: u16, + unk_xc: [u32; 8], + /// Number of nodes connected to the network. + pub total_nodes: u8, + /// Maximum nodes allowed on this network. + pub max_nodes: u8, + /// Bitmask for which of the 16 possible nodes are connected + /// to this network; bit 0 is the server, bit 1 is the first + /// original client, etc. + pub node_bitmask: u16, +} + +impl From for ConnectionStatus { + fn from(value: ctru_sys::udsConnectionStatus) -> Self { + Self { + status: value.status, + unk_x4: value.unk_x4, + cur_node_id: value.cur_NetworkNodeID, + unk_xa: value.unk_xa, + unk_xc: value.unk_xc, + total_nodes: value.total_nodes, + max_nodes: value.max_nodes, + node_bitmask: value.node_bitmask, + } + } +} + /// Status of the service handle. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum ServiceStatus { @@ -172,6 +211,7 @@ pub struct Uds { _service_handler: ServiceReference, context: Option, network: Option, + scan_buf: Box<[u8; Self::SCAN_BUF_SIZE]>, } static UDS_ACTIVE: Mutex<()> = Mutex::new(()); @@ -266,6 +306,7 @@ impl Uds { _service_handler: handler, context: None, network: None, + scan_buf: Box::new([0; Self::SCAN_BUF_SIZE]), }) } @@ -290,14 +331,12 @@ impl Uds { /// ``` #[doc(alias = "udsScanBeacons")] pub fn scan( - &self, + &mut 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(); + self.scan_buf.fill(0); let mut networks = MaybeUninit::uninit(); let mut total_networks = MaybeUninit::uninit(); @@ -381,7 +420,10 @@ impl Uds { let actual_size = unsafe { actual_size.assume_init() }; - Ok(appdata_buffer[..actual_size].to_vec()) + appdata_buffer.truncate(actual_size); + appdata_buffer.shrink_to_fit(); + + Ok(appdata_buffer) } /// Retrieve app data for the currently connected network. @@ -433,7 +475,10 @@ impl Uds { let actual_size = unsafe { actual_size.assume_init() }; - Ok(appdata_buffer[..actual_size].to_vec()) + appdata_buffer.truncate(actual_size); + appdata_buffer.shrink_to_fit(); + + Ok(appdata_buffer) } /// Connect to a network. @@ -643,7 +688,7 @@ impl Uds { Ok(unsafe { ctru_sys::udsWaitConnectionStatusEvent(next, wait) }) } - /// Returns the current [`ctru_sys::udsConnectionStatus`] struct. + /// Returns the current [`ConnectionStatus`] struct. /// /// TODO: should this return an error if not connected? /// @@ -669,14 +714,14 @@ impl Uds { /// # } /// ``` #[doc(alias = "udsGetConnectionStatus")] - pub fn get_connection_status(&self) -> crate::Result { + 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) + Ok(status.into()) } /// Send a packet to the network. @@ -798,6 +843,7 @@ impl Uds { Ok(if actual_size == 0 { None } else { + // TODO: to_vec() first, then truncate() and shrink_to_fit()? Some((frame[..actual_size].to_vec(), src_node_id)) }) } From bcb2be8cf79666063c35215c75c4c127c9ebeb9a Mon Sep 17 00:00:00 2001 From: Jhynjhiruu Date: Thu, 8 Feb 2024 21:37:52 +0000 Subject: [PATCH 03/12] I hate not having rust-analyzer working --- ctru-rs/src/services/uds.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/ctru-rs/src/services/uds.rs b/ctru-rs/src/services/uds.rs index 566dba8..8b6caf0 100644 --- a/ctru-rs/src/services/uds.rs +++ b/ctru-rs/src/services/uds.rs @@ -343,7 +343,7 @@ impl Uds { ResultCode(unsafe { ctru_sys::udsScanBeacons( - scan_buf.as_mut_ptr().cast(), + self.scan_buf.as_mut_ptr().cast(), Self::SCAN_BUF_SIZE, networks.as_mut_ptr(), total_networks.as_mut_ptr(), @@ -356,10 +356,6 @@ impl Uds { ) })?; - unsafe { - scan_buf.assume_init_drop(); - } - let networks = unsafe { networks.assume_init() }; let total_networks = unsafe { total_networks.assume_init() }; From 1cdf4737f2f94f5c74d28a7f4a863e459b3247e5 Mon Sep 17 00:00:00 2001 From: Jhynjhiruu Date: Thu, 8 Feb 2024 21:59:16 +0000 Subject: [PATCH 04/12] Explain the `username` parameter on `Uds::new()` --- ctru-rs/src/services/uds.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ctru-rs/src/services/uds.rs b/ctru-rs/src/services/uds.rs index 8b6caf0..ae4decc 100644 --- a/ctru-rs/src/services/uds.rs +++ b/ctru-rs/src/services/uds.rs @@ -252,6 +252,9 @@ impl Uds { /// 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. + /// + /// The `username` parameter should be a max 10-byte (not 10 code point!) UTF-8 string, converted to UTF-16 internally. + /// Pass `None` to use the 3DS's configured username. /// /// # Errors /// From 528702348675b4f36ecdc067fb85f08ce24ef3e7 Mon Sep 17 00:00:00 2001 From: Jhynjhiruu Date: Sat, 10 Feb 2024 14:42:56 +0000 Subject: [PATCH 05/12] Add more documentation, add NetworkNodeID enum --- ctru-rs/examples/local-networking.rs | 8 +-- ctru-rs/src/services/uds.rs | 75 +++++++++++++++++++++++----- 2 files changed, 66 insertions(+), 17 deletions(-) diff --git a/ctru-rs/examples/local-networking.rs b/ctru-rs/examples/local-networking.rs index 03f072f..75f2d0e 100644 --- a/ctru-rs/examples/local-networking.rs +++ b/ctru-rs/examples/local-networking.rs @@ -165,7 +165,7 @@ fn main() { Ok(p) => { if let Some((pkt, node)) = p { println!( - "{:02X}{:02X}{:02X}{:02X} from {:04X}", + "{:02X}{:02X}{:02X}{:02X} from {:?}", pkt[0], pkt[1], pkt[2], pkt[3], node ); } @@ -186,7 +186,7 @@ fn main() { if mode != ConnectionType::Spectator { uds.send_packet( &transfer_data.to_le_bytes(), - ctru_sys::UDS_BROADCAST_NETWORKNODEID as _, + NodeID::Broadcast, data_channel, SendFlags::Default, ) @@ -241,7 +241,7 @@ fn main() { Ok(p) => { if let Some((pkt, node)) = p { println!( - "{:02X}{:02X}{:02X}{:02X} from {:04X}", + "{:02X}{:02X}{:02X}{:02X} from {:?}", pkt[0], pkt[1], pkt[2], pkt[3], node ); } @@ -261,7 +261,7 @@ fn main() { let transfer_data = hid.keys_held().bits(); uds.send_packet( &transfer_data.to_le_bytes(), - ctru_sys::UDS_BROADCAST_NETWORKNODEID as _, + NodeID::Broadcast, data_channel, SendFlags::Default, ) diff --git a/ctru-rs/src/services/uds.rs b/ctru-rs/src/services/uds.rs index ae4decc..33f6dc3 100644 --- a/ctru-rs/src/services/uds.rs +++ b/ctru-rs/src/services/uds.rs @@ -20,11 +20,11 @@ use macaddr::MacAddr6; bitflags! { /// Flags used for sending packets to a network. - #[allow(missing_docs)] - #[derive(Copy, Clone, Debug, PartialEq, Eq)] + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct SendFlags: u8 { - /// According to libctru source, it's not really known what these do. + /// Unknown function according to `libctru`. const Default = ctru_sys::UDS_SENDFLAG_Default as u8; + /// Broadcast the data frame even when sending to a non-broadcast address. const Broadcast = ctru_sys::UDS_SENDFLAG_Broadcast as u8; } } @@ -94,6 +94,43 @@ impl TryFrom for ConnectionType { } } +/// ID for a node on the network. +#[doc(alias = "NetworkNodeID")] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NodeID { + /// No node ID set (not connected to a network). + None, + /// A normal node on the network, counting from 1 (the host) to 16, inclusive. + Node(u8), + /// Broadcast to all nodes + Broadcast, +} + +impl From for u16 { + fn from(value: NodeID) -> Self { + match value { + NodeID::None => 0, + NodeID::Node(node) => node as u16, + NodeID::Broadcast => ctru_sys::UDS_BROADCAST_NETWORKNODEID as u16, + } + } +} + +impl TryFrom for NodeID { + type Error = (); + + fn try_from(value: u16) -> std::result::Result { + match value as u32 { + 0 => Ok(Self::None), + ctru_sys::UDS_HOST_NETWORKNODEID..=ctru_sys::UDS_MAXNODES => { + Ok(Self::Node(value as u8)) + } + ctru_sys::UDS_BROADCAST_NETWORKNODEID => Ok(Self::Broadcast), + _ => Err(()), + } + } +} + /// Information about a network node. #[allow(missing_docs)] #[doc(alias = "udsNodeInfo")] @@ -105,7 +142,7 @@ pub struct NodeInfo { unk_x1c: u16, pub flag: u8, pad_x1f: u8, - pub node_id: u16, + pub node_id: NodeID, pad_x22: u16, word_x24: u32, } @@ -121,7 +158,10 @@ impl From for NodeInfo { 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, + node_id: value + .NetworkNodeID + .try_into() + .expect("UDS service should always provide a valid NetworkNodeID"), pad_x22: value.pad_x22, word_x24: value.word_x24, } @@ -163,11 +203,12 @@ impl From for NetworkScanInfo { #[doc(alias = "udsConnectionStatus")] #[derive(Debug)] pub struct ConnectionStatus { + /// Raw status information // TODO: is this in some kind of readable format? pub status: u32, unk_x4: u32, /// Network node ID for the current device. - pub cur_node_id: u16, + pub cur_node_id: NodeID, unk_xa: u16, unk_xc: [u32; 8], /// Number of nodes connected to the network. @@ -185,7 +226,10 @@ impl From for ConnectionStatus { Self { status: value.status, unk_x4: value.unk_x4, - cur_node_id: value.cur_NetworkNodeID, + cur_node_id: value + .cur_NetworkNodeID + .try_into() + .expect("UDS service should always provide a valid NetworkNodeID"), unk_xa: value.unk_xa, unk_xc: value.unk_xc, total_nodes: value.total_nodes, @@ -196,7 +240,7 @@ impl From for ConnectionStatus { } /// Status of the service handle. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ServiceStatus { /// Not connected to or hosting a network. Disconnected, @@ -252,7 +296,7 @@ impl Uds { /// 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. - /// + /// /// The `username` parameter should be a max 10-byte (not 10 code point!) UTF-8 string, converted to UTF-16 internally. /// Pass `None` to use the 3DS's configured username. /// @@ -754,7 +798,7 @@ impl Uds { pub fn send_packet( &self, packet: &[u8], - to_nodes: u16, + address: NodeID, channel: u8, flags: SendFlags, ) -> Result<(), Error> { @@ -768,7 +812,7 @@ impl Uds { let code = ResultCode(unsafe { ctru_sys::udsSendTo( - to_nodes, + address.into(), channel, flags.bits(), packet.as_ptr().cast(), @@ -815,7 +859,7 @@ impl Uds { /// # } /// ``` #[doc(alias = "udsPullPacket")] - pub fn pull_packet(&self) -> Result, u16)>, Error> { + pub fn pull_packet(&self) -> Result, NodeID)>, Error> { if self.service_status() == ServiceStatus::Disconnected { return Err(Error::NotConnected); } @@ -843,7 +887,12 @@ impl Uds { None } else { // TODO: to_vec() first, then truncate() and shrink_to_fit()? - Some((frame[..actual_size].to_vec(), src_node_id)) + Some(( + frame[..actual_size].to_vec(), + src_node_id + .try_into() + .expect("UDS service should always provide a valid NetworkNodeID"), + )) }) } From 5feb9066901137db3dd40305ca43f3ac19ddd931 Mon Sep 17 00:00:00 2001 From: Jhynjhiruu Date: Sat, 10 Feb 2024 15:38:57 +0000 Subject: [PATCH 06/12] Implement remaining UDS functions (mostly untested) --- ctru-rs/examples/local-networking.rs | 77 +++++---- ctru-rs/src/services/uds.rs | 235 ++++++++++++++++++++++++++- 2 files changed, 275 insertions(+), 37 deletions(-) diff --git a/ctru-rs/examples/local-networking.rs b/ctru-rs/examples/local-networking.rs index 75f2d0e..0138659 100644 --- a/ctru-rs/examples/local-networking.rs +++ b/ctru-rs/examples/local-networking.rs @@ -5,7 +5,30 @@ use ctru::prelude::*; use ctru::services::uds::*; -fn main() { +fn handle_status_event(uds: &Uds, prev_node_mask: u16) -> ctru::Result { + println!("Connection status event signalled"); + let status = uds.get_connection_status()?; + println!("Status: {status:#02X?}"); + let left = prev_node_mask & (status.node_bitmask ^ prev_node_mask); + let joined = status.node_bitmask & (status.node_bitmask ^ prev_node_mask); + for i in 0..16 { + if left & (1 << i) != 0 { + println!("Node {} disconnected", i + 1); + } + } + for i in 0..16 { + if joined & (1 << i) != 0 { + println!( + "Node {} connected: {:?}", + i + 1, + uds.get_node_info(NodeID::Node(i + 1)) + ); + } + } + Ok(status.node_bitmask) +} + +fn main() -> Result<(), Error> { let apt = Apt::new().unwrap(); let mut hid = Hid::new().unwrap(); let gfx = Gfx::new().unwrap(); @@ -40,6 +63,8 @@ fn main() { let mut channel = 0; let data_channel = 1; + let mut prev_node_mask = 0; + while apt.main_loop() { gfx.wait_for_vblank(); @@ -53,9 +78,11 @@ fn main() { if hid.keys_down().contains(KeyPad::A) { state = State::Scanning; console.clear(); + prev_node_mask = 0; } else if hid.keys_down().contains(KeyPad::B) { state = State::Create; console.clear(); + prev_node_mask = 0; } } State::Scanning => { @@ -126,9 +153,7 @@ fn main() { } } State::Connect => { - let appdata = uds - .get_network_appdata(&networks[selected_network], None) - .unwrap(); + let appdata = uds.get_network_appdata(&networks[selected_network], None)?; println!("App data: {:02X?}", appdata); if let Err(e) = uds.connect_network( @@ -142,16 +167,14 @@ fn main() { state = State::Initialised; println!("Press A to start scanning or B to create a new network"); } else { - channel = uds.get_channel().unwrap(); + channel = uds.get_channel()?; println!("Connected using channel {}", channel); - let appdata = uds.get_appdata(None).unwrap(); + let appdata = uds.get_appdata(None)?; 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?}"); + if uds.wait_status_event(false, false)? { + prev_node_mask = handle_status_event(&uds, prev_node_mask)?; } println!("Press A to stop data transfer"); @@ -170,14 +193,12 @@ fn main() { ); } - 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 uds.wait_status_event(false, false)? { + prev_node_mask = handle_status_event(&uds, prev_node_mask)?; } if hid.keys_down().contains(KeyPad::A) { - uds.disconnect_network().unwrap(); + uds.disconnect_network()?; state = State::Initialised; console.clear(); println!("Press A to start scanning or B to create a new network"); @@ -189,15 +210,14 @@ fn main() { NodeID::Broadcast, data_channel, SendFlags::Default, - ) - .unwrap(); + )?; } } } Err(e) => { - uds.disconnect_network().unwrap(); + uds.disconnect_network()?; console.clear(); - eprintln!("Error while grabbing packet from network: {e:#?}"); + eprintln!("Error while grabbing packet from network: {e}"); state = State::Initialised; println!("Press A to start scanning or B to create a new network"); } @@ -221,7 +241,7 @@ fn main() { .chain(std::iter::repeat(0).take(3)) .collect::>(); - uds.set_appdata(&appdata).unwrap(); + uds.set_appdata(&appdata)?; println!("Press A to stop data transfer"); state = State::Created; @@ -246,14 +266,12 @@ fn main() { ); } - 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 uds.wait_status_event(false, false)? { + prev_node_mask = handle_status_event(&uds, prev_node_mask)?; } if hid.keys_down().contains(KeyPad::A) { - uds.destroy_network().unwrap(); + uds.destroy_network()?; state = State::Initialised; console.clear(); println!("Press A to start scanning or B to create a new network"); @@ -264,14 +282,13 @@ fn main() { NodeID::Broadcast, data_channel, SendFlags::Default, - ) - .unwrap(); + )?; } } Err(e) => { - uds.destroy_network().unwrap(); + uds.destroy_network()?; console.clear(); - eprintln!("Error while grabbing packet from network: {e:#?}"); + eprintln!("Error while grabbing packet from network: {e}"); state = State::Initialised; println!("Press A to start scanning or B to create a new network"); } @@ -279,4 +296,6 @@ fn main() { } } } + + Ok(()) } diff --git a/ctru-rs/src/services/uds.rs b/ctru-rs/src/services/uds.rs index 33f6dc3..98fd630 100644 --- a/ctru-rs/src/services/uds.rs +++ b/ctru-rs/src/services/uds.rs @@ -5,8 +5,9 @@ #![doc(alias = "network")] #![doc(alias = "dlplay")] +use std::error::Error as StdError; use std::ffi::CString; -use std::fmt::Debug; +use std::fmt::{Debug, Display}; use std::mem::MaybeUninit; use std::ops::FromResidual; use std::ptr::null; @@ -47,6 +48,8 @@ pub enum Error { NoNetwork, /// The provided app data buffer was too large. TooMuchAppData, + /// The provided node ID does not reference a specific node. + NotANode, /// ctru-rs error Lib(crate::Error), } @@ -63,6 +66,29 @@ impl FromResidual for Result { } } +impl Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::UsernameTooLong => + "provided username was too long (max 10 bytes, not code points)".into(), + Self::UsernameContainsNull => "provided username contained a NULL byte".into(), + Self::NotConnected => "not connected to a network".into(), + Self::NoContext => "no context bound".into(), + Self::Spectator => "cannot send data on a network as a spectator".into(), + Self::NoNetwork => "not hosting a network".into(), + Self::TooMuchAppData => "provided too much app data (max 200 bytes)".into(), + Self::NotANode => "provided node ID was non-specific".into(), + Self::Lib(e) => format!("ctru-rs error: {e}"), + } + ) + } +} + +impl StdError for Error {} + /// Possible types of connection to a network /// #[doc(alias = "udsConnectionType")] @@ -558,7 +584,7 @@ impl Uds { passphrase.as_ptr().cast(), passphrase.len(), context.as_mut_ptr(), - ctru_sys::UDS_BROADCAST_NETWORKNODEID as u16, + NodeID::Broadcast.into(), connection_type as u32, channel, Self::RECV_BUF_SIZE, @@ -784,12 +810,12 @@ impl Uds { /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # - /// use ctru::services::uds::{ConnectionType, SendFlags, Uds}; + /// use ctru::services::uds::{ConnectionType, NodeID, 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)?; + /// uds.send_packet(b"Hello, World!", NodeID::Broadcast, 1, SendFlags::Default)?; /// # /// # Ok(()) /// # } @@ -848,7 +874,7 @@ impl Uds { /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # - /// use ctru::services::uds::{ConnectionType, SendFlags, Uds}; + /// use ctru::services::uds::{ConnectionType, Uds}; /// let mut uds = Uds::new(None)?; /// /// let networks = uds.scan(b"HBW\x10", None, None)?; @@ -909,7 +935,7 @@ impl Uds { /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # - /// use ctru::services::uds::{ConnectionType, SendFlags, Uds}; + /// use ctru::services::uds::Uds; /// let mut uds = Uds::new(None)?; /// /// uds.create_network(b"HBW\x10", None, None, b"udsdemo passphrase c186093cd2652741\0", 1)?; @@ -974,7 +1000,7 @@ impl Uds { /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # - /// use ctru::services::uds::{ConnectionType, SendFlags, Uds}; + /// use ctru::services::uds::Uds; /// let mut uds = Uds::new(None)?; /// /// uds.create_network(b"HBW\x10", None, None, b"udsdemo passphrase c186093cd2652741\0", 1)?; @@ -1016,7 +1042,7 @@ impl Uds { /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # - /// use ctru::services::uds::{ConnectionType, SendFlags, Uds}; + /// use ctru::services::uds::Uds; /// let mut uds = Uds::new(None)?; /// /// uds.create_network(b"HBW\x10", None, None, b"udsdemo passphrase c186093cd2652741\0", 1)?; @@ -1039,6 +1065,199 @@ impl Uds { Ok(()) } + + /// Wait for a bind 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_data_available(false, false)? { + /// println!("Data available"); + /// } + /// # + /// # Ok(()) + /// # } + /// ``` + #[doc(alias = "udsWaitConnectionStatusEvent")] + pub fn wait_data_available(&self, next: bool, wait: bool) -> Result { + if self.service_status() == ServiceStatus::Disconnected { + return Err(Error::NotConnected); + } + + Ok(unsafe { + ctru_sys::udsWaitDataAvailable(&self.context.unwrap() as *const _, next, wait) + }) + } + + /// Eject a client from the 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::{NodeID, Uds}; + /// let mut uds = Uds::new(None)?; + /// + /// uds.create_network(b"HBW\x10", None, None, b"udsdemo passphrase c186093cd2652741\0", 1)?; + /// uds.eject_client(NodeID::Node(2))?; + /// # + /// # Ok(()) + /// # } + /// ``` + #[doc(alias = "udsEjectClient")] + pub fn eject_client(&self, address: NodeID) -> Result<(), Error> { + if self.service_status() != ServiceStatus::Server { + return Err(Error::NoNetwork); + } + + ResultCode(unsafe { ctru_sys::udsEjectClient(address.into()) })?; + + Ok(()) + } + + /// Allow or disallow spectators on the network. + /// + /// Disallowing spectators will disconnect all spectators currently observing the 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::Uds; + /// let mut uds = Uds::new(None)?; + /// + /// uds.create_network(b"HBW\x10", None, None, b"udsdemo passphrase c186093cd2652741\0", 1)?; + /// uds.allow_spectators(false)?; + /// # + /// # Ok(()) + /// # } + /// ``` + #[doc(alias = "udsEjectSpectator")] + #[doc(alias = "udsAllowSpectators")] + pub fn allow_spectators(&mut self, allow: bool) -> Result<(), Error> { + if self.service_status() != ServiceStatus::Server { + return Err(Error::NoNetwork); + } + + ResultCode(unsafe { + if allow { + ctru_sys::udsAllowSpectators() + } else { + ctru_sys::udsEjectSpectator() + } + })?; + + Ok(()) + } + + /// Allow or disallow new clients on the network. + /// + /// Disallowing new clients will not disconnect any currently connected clients. + /// + /// # 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::Uds; + /// let mut uds = Uds::new(None)?; + /// + /// uds.create_network(b"HBW\x10", None, None, b"udsdemo passphrase c186093cd2652741\0", 1)?; + /// uds.allow_new_clients(false)?; + /// # + /// # Ok(()) + /// # } + /// ``` + #[doc(alias = "udsSetNewConnectionsBlocked")] + pub fn allow_new_clients(&mut self, allow: bool) -> Result<(), Error> { + if self.service_status() != ServiceStatus::Server { + return Err(Error::NoNetwork); + } + + ResultCode(unsafe { ctru_sys::udsSetNewConnectionsBlocked(!allow, true, false) })?; + + Ok(()) + } + + /// Returns the [`NodeInfo`] struct for the specified network node. + /// + /// # Errors + /// + /// This function will return an error if [`NodeID::None`] or [`NodeID::Broadcast`] is passed. + /// + /// # Example + /// + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// # + /// use ctru::services::uds::{NodeID, Uds}; + /// let mut uds = Uds::new(None)?; + /// + /// uds.create_network(b"HBW\x10", None, None, b"udsdemo passphrase c186093cd2652741\0", 1)?; + /// let node_info = uds.get_node_info(NodeID::Node(2))?; + /// # + /// # Ok(()) + /// # } + /// ``` + #[doc(alias = "udsGetNodeInformation")] + pub fn get_node_info(&self, address: NodeID) -> Result { + let NodeID::Node(node) = address else { + return Err(Error::NotANode); + }; + + let mut info = MaybeUninit::uninit(); + + ResultCode(unsafe { ctru_sys::udsGetNodeInformation(node as u16, info.as_mut_ptr()) })?; + + let info = unsafe { info.assume_init() }; + + Ok(info.into()) + } } impl Drop for Uds { From 433f9b0591a15955d70fea6ddb18647a161ef7e1 Mon Sep 17 00:00:00 2001 From: Jhynjhiruu Date: Sat, 10 Feb 2024 15:41:10 +0000 Subject: [PATCH 07/12] Fix lint --- ctru-rs/src/services/uds.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ctru-rs/src/services/uds.rs b/ctru-rs/src/services/uds.rs index 98fd630..35c4f50 100644 --- a/ctru-rs/src/services/uds.rs +++ b/ctru-rs/src/services/uds.rs @@ -1225,7 +1225,7 @@ impl Uds { /// Returns the [`NodeInfo`] struct for the specified network node. /// /// # Errors - /// + /// /// This function will return an error if [`NodeID::None`] or [`NodeID::Broadcast`] is passed. /// /// # Example From 5eda3916ba0b5d79da8130d2b2a484aadf05b6d4 Mon Sep 17 00:00:00 2001 From: Jhynjhiruu Date: Sat, 10 Feb 2024 15:51:01 +0000 Subject: [PATCH 08/12] Fix doc tests --- ctru-rs/src/services/uds.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ctru-rs/src/services/uds.rs b/ctru-rs/src/services/uds.rs index 35c4f50..83fbded 100644 --- a/ctru-rs/src/services/uds.rs +++ b/ctru-rs/src/services/uds.rs @@ -395,7 +395,7 @@ impl Uds { /// # fn main() -> Result<(), Box> { /// # /// use ctru::services::uds::Uds; - /// let uds = Uds::new(None)?; + /// let mut uds = Uds::new(None)?; /// /// let networks = uds.scan(b"HBW\x10", None, None)?; /// # @@ -455,7 +455,7 @@ impl Uds { /// # fn main() -> Result<(), Box> { /// # /// use ctru::services::uds::Uds; - /// let uds = Uds::new(None)?; + /// let mut uds = Uds::new(None)?; /// /// let networks = uds.scan(b"HBW\x10", None, None)?; /// let appdata = uds.get_network_appdata(&networks[0], None)?; @@ -510,7 +510,7 @@ impl Uds { /// # fn main() -> Result<(), Box> { /// # /// use ctru::services::uds::{ConnectionType, Uds}; - /// let uds = Uds::new(None)?; + /// 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)?; From e0c8a4d8f6c1ba675b1450ed7a0b1031d02ebe0d Mon Sep 17 00:00:00 2001 From: Jhynjhiruu Date: Sun, 25 Feb 2024 23:19:46 +0000 Subject: [PATCH 09/12] Update ctru-rs/src/services/uds.rs Doh Co-authored-by: Ian Chamberlain --- ctru-rs/src/services/uds.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ctru-rs/src/services/uds.rs b/ctru-rs/src/services/uds.rs index 83fbded..f62dbbd 100644 --- a/ctru-rs/src/services/uds.rs +++ b/ctru-rs/src/services/uds.rs @@ -436,7 +436,7 @@ impl Uds { // 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) + .map(NetworkScanInfo::from) .collect() } else { vec![] From 009bc7b4f1536b3ded0c004c64b6dc602bc52f94 Mon Sep 17 00:00:00 2001 From: Jhynjhiruu Date: Sun, 10 Mar 2024 21:49:54 +0000 Subject: [PATCH 10/12] Replace custom types with wrappers --- ctru-rs/examples/local-networking.rs | 8 +- ctru-rs/src/services/uds.rs | 295 ++++++++++++++++++--------- 2 files changed, 207 insertions(+), 96 deletions(-) diff --git a/ctru-rs/examples/local-networking.rs b/ctru-rs/examples/local-networking.rs index 0138659..3871a1a 100644 --- a/ctru-rs/examples/local-networking.rs +++ b/ctru-rs/examples/local-networking.rs @@ -9,8 +9,8 @@ fn handle_status_event(uds: &Uds, prev_node_mask: u16) -> ctru::Result { println!("Connection status event signalled"); let status = uds.get_connection_status()?; println!("Status: {status:#02X?}"); - let left = prev_node_mask & (status.node_bitmask ^ prev_node_mask); - let joined = status.node_bitmask & (status.node_bitmask ^ prev_node_mask); + let left = prev_node_mask & (status.node_bitmask() ^ prev_node_mask); + let joined = status.node_bitmask() & (status.node_bitmask() ^ prev_node_mask); for i in 0..16 { if left & (1 << i) != 0 { println!("Node {} disconnected", i + 1); @@ -25,7 +25,7 @@ fn handle_status_event(uds: &Uds, prev_node_mask: u16) -> ctru::Result { ); } } - Ok(status.node_bitmask) + Ok(status.node_bitmask()) } fn main() -> Result<(), Error> { @@ -126,7 +126,7 @@ fn main() -> Result<(), Error> { println!( "{} Username: {}", if index == selected_network { ">" } else { " " }, - n.nodes[0].as_ref().unwrap().username + n.nodes()[0].unwrap().username() ); } diff --git a/ctru-rs/src/services/uds.rs b/ctru-rs/src/services/uds.rs index f62dbbd..1ec7407 100644 --- a/ctru-rs/src/services/uds.rs +++ b/ctru-rs/src/services/uds.rs @@ -37,7 +37,7 @@ pub enum Error { /// The provided username was too long. UsernameTooLong, /// The provided username contained a NULL byte. - UsernameContainsNull, + UsernameContainsNull(usize), /// Not connected to a network. NotConnected, /// No context bound. @@ -66,6 +66,12 @@ impl FromResidual for Result { } } +impl From for Error { + fn from(value: std::ffi::NulError) -> Self { + Error::UsernameContainsNull(value.nul_position()) + } +} + impl Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( @@ -74,7 +80,8 @@ impl Display for Error { match self { Self::UsernameTooLong => "provided username was too long (max 10 bytes, not code points)".into(), - Self::UsernameContainsNull => "provided username contained a NULL byte".into(), + Self::UsernameContainsNull(pos) => + format!("provided username contained a NULL byte at position {pos}"), Self::NotConnected => "not connected to a network".into(), Self::NoContext => "no context bound".into(), Self::Spectator => "cannot send data on a network as a spectator".into(), @@ -89,8 +96,7 @@ impl Display for Error { impl StdError for Error {} -/// Possible types of connection to a network -/// +/// Possible types of connection to a network. #[doc(alias = "udsConnectionType")] #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u32)] @@ -158,110 +164,219 @@ impl TryFrom for NodeID { } /// Information about a network node. -#[allow(missing_docs)] #[doc(alias = "udsNodeInfo")] -#[derive(Debug)] -// Ported to Rust so that Debug can be derived on it. -pub struct NodeInfo { - pub uds_friendcodeseed: u64, - pub username: String, - unk_x1c: u16, - pub flag: u8, - pad_x1f: u8, - pub node_id: NodeID, - pad_x22: u16, - word_x24: u32, +#[derive(Copy, Clone)] +pub struct NodeInfo(ctru_sys::udsNodeInfo); + +impl Debug for NodeInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "NodeInfo(")?; + + f.debug_struct("udsNodeInfo") + .field("uds_friendcodeseed", &self.uds_friendcodeseed()) + .field("username", &self.username()) + .field("flag", &self.flag()) + .field("NetworkNodeID", &self.node_id()) + .finish()?; + + write!(f, ")") + } } 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 - .try_into() - .expect("UDS service should always provide a valid NetworkNodeID"), - pad_x22: value.pad_x22, - word_x24: value.word_x24, - } - } + Self(value) + } +} + +impl NodeInfo { + /// Friend code seed associated with this network node. + pub fn uds_friendcodeseed(&self) -> u64 { + self.0.uds_friendcodeseed + } + + /// Username associated with this network node. + pub fn username(&self) -> String { + String::from_utf16_lossy(unsafe { &self.0.__bindgen_anon_1.__bindgen_anon_1.username }) + } + + /// Flag associated with this network node. + pub fn flag(&self) -> u8 { + unsafe { self.0.__bindgen_anon_1.__bindgen_anon_1.flag } + } + + /// Node ID associated with this network node. + pub fn node_id(&self) -> NodeID { + self.0 + .NetworkNodeID + .try_into() + .expect("UDS service should always provide a valid NetworkNodeID") } } /// Information returned from scanning for networks. #[doc(alias = "udsNetworkScanInfo")] -#[derive(Debug)] -// Ported to Rust so that Debug can be derived on it. -pub struct NetworkScanInfo { +#[derive(Copy, Clone)] +pub struct NetworkScanInfo(ctru_sys::udsNetworkScanInfo); + +impl Debug for NetworkScanInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "NetworkScanInfo(")?; + + f.debug_struct("udsNetworkScanInfo") + .field("datareply_entry", &self.datareply_entry()) + .field("network", &self.network()) + .field("nodes", &self.nodes()) + .finish()?; + + write!(f, ")") + } +} + +impl From for NetworkScanInfo { + fn from(value: ctru_sys::udsNetworkScanInfo) -> Self { + Self(value) + } +} + +impl NetworkScanInfo { /// NWM output structure. - pub datareply_entry: ctru_sys::nwmBeaconDataReplyEntry, + pub fn datareply_entry(&self) -> ctru_sys::nwmBeaconDataReplyEntry { + self.0.datareply_entry + } + + /// Get a reference to the NWM output structure. + pub fn datareply_entry_ref(&self) -> &ctru_sys::nwmBeaconDataReplyEntry { + &self.0.datareply_entry + } + + /// Get a mutable reference to the NWM output structure. + pub fn datareply_entry_mut(&mut self) -> &mut ctru_sys::nwmBeaconDataReplyEntry { + &mut self.0.datareply_entry + } + /// Information about the network. - pub network: ctru_sys::udsNetworkStruct, + pub fn network(&self) -> ctru_sys::udsNetworkStruct { + self.0.network + } + + /// Get a reference to the information about the network. + pub fn network_ref(&self) -> &ctru_sys::udsNetworkStruct { + &self.0.network + } + + /// Get a mutable reference to the information about the network. + pub fn network_mut(&mut self) -> &mut ctru_sys::udsNetworkStruct { + &mut self.0.network + } + /// All nodes on the network (first node is the server, /// max 16, `None` means no node connected). - pub nodes: [Option; 16], + pub fn nodes(&self) -> [Option; 16] { + self.0.nodes.map(|n| { + if n.uds_friendcodeseed != 0 { + Some(n.into()) + } else { + None + } + }) + } } -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 - } - }), +/// Possible raw connection status values. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u32)] +#[non_exhaustive] +pub enum ConnectionStatusInfo { + /// Not connected to any network. + Disconnected = 3, + /// Connected as a host. + Host = 6, + /// Connected as a client. + Client = 9, + /// Connected as a spectator. + Spectator = 10, + /// Unknown + Unknown = 11, +} + +impl From for u32 { + fn from(value: ConnectionStatusInfo) -> Self { + value as Self + } +} + +impl TryFrom for ConnectionStatusInfo { + type Error = (); + + fn try_from(value: u32) -> std::result::Result { + match value { + 3 => Ok(Self::Disconnected), + 6 => Ok(Self::Host), + 9 => Ok(Self::Client), + 10 => Ok(Self::Spectator), + 11 => Ok(Self::Unknown), + _ => Err(()), } } } /// Status of the connection. #[doc(alias = "udsConnectionStatus")] -#[derive(Debug)] -pub struct ConnectionStatus { - /// Raw status information - // TODO: is this in some kind of readable format? - pub status: u32, - unk_x4: u32, +#[derive(Clone, Copy)] +pub struct ConnectionStatus(ctru_sys::udsConnectionStatus); + +impl Debug for ConnectionStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "ConnectionStatus(")?; + + f.debug_struct("udsConnectionStatus") + .field("status", &self.status()) + .field("cur_node_id", &self.cur_node_id()) + .field("total_nodes", &self.total_nodes()) + .field("max_nodes", &self.max_nodes()) + .field("node_bitmask", &self.node_bitmask()) + .finish()?; + + write!(f, ")") + } +} + +impl From for ConnectionStatus { + fn from(value: ctru_sys::udsConnectionStatus) -> Self { + Self(value) + } +} + +impl ConnectionStatus { + /// Raw status information. + pub fn status(&self) -> Option { + self.0.status.try_into().ok() + } + /// Network node ID for the current device. - pub cur_node_id: NodeID, - unk_xa: u16, - unk_xc: [u32; 8], + pub fn cur_node_id(&self) -> NodeID { + self.0 + .cur_NetworkNodeID + .try_into() + .expect("UDS service should always provide a valid NetworkNodeID") + } + /// Number of nodes connected to the network. - pub total_nodes: u8, + pub fn total_nodes(&self) -> u8 { + self.0.total_nodes + } + /// Maximum nodes allowed on this network. - pub max_nodes: u8, + pub fn max_nodes(&self) -> u8 { + self.0.max_nodes + } + /// Bitmask for which of the 16 possible nodes are connected /// to this network; bit 0 is the server, bit 1 is the first /// original client, etc. - pub node_bitmask: u16, -} - -impl From for ConnectionStatus { - fn from(value: ctru_sys::udsConnectionStatus) -> Self { - Self { - status: value.status, - unk_x4: value.unk_x4, - cur_node_id: value - .cur_NetworkNodeID - .try_into() - .expect("UDS service should always provide a valid NetworkNodeID"), - unk_xa: value.unk_xa, - unk_xc: value.unk_xc, - total_nodes: value.total_nodes, - max_nodes: value.max_nodes, - node_bitmask: value.node_bitmask, - } + pub fn node_bitmask(&self) -> u16 { + self.0.node_bitmask } } @@ -307,7 +422,11 @@ impl Uds { /// 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; + const MAX_APPDATA_SIZE: usize = Self::size_of_call(|s: ctru_sys::udsNetworkStruct| s.appdata); + + const fn size_of_call(_: fn(T) -> U) -> usize { + std::mem::size_of::() + } /// Retrieve the current status of the service. pub fn service_status(&self) -> ServiceStatus { @@ -352,15 +471,7 @@ impl Uds { 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 cstr = username.map(CString::new).transpose()?; let handler = ServiceReference::new( &UDS_ACTIVE, || { @@ -480,7 +591,7 @@ impl Uds { ResultCode(unsafe { ctru_sys::udsGetNetworkStructApplicationData( - &network.network as *const _, + network.network_ref() as *const _, appdata_buffer.as_mut_ptr().cast(), appdata_buffer.len(), actual_size.as_mut_ptr(), @@ -580,7 +691,7 @@ impl Uds { ResultCode(unsafe { ctru_sys::udsConnectNetwork( - &network.network as *const _, + network.network_ref() as *const _, passphrase.as_ptr().cast(), passphrase.len(), context.as_mut_ptr(), From 33a4b1ca0c30479dcb17a1de687835765bcdc25d Mon Sep 17 00:00:00 2001 From: Jhynjhiruu Date: Thu, 14 Mar 2024 17:25:24 +0000 Subject: [PATCH 11/12] Update uds.rs --- ctru-rs/src/services/uds.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ctru-rs/src/services/uds.rs b/ctru-rs/src/services/uds.rs index 1ec7407..5eb81b8 100644 --- a/ctru-rs/src/services/uds.rs +++ b/ctru-rs/src/services/uds.rs @@ -128,7 +128,7 @@ impl TryFrom for ConnectionType { /// ID for a node on the network. #[doc(alias = "NetworkNodeID")] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum NodeID { /// No node ID set (not connected to a network). None, From b9b8d6baf902c9f589eef77c6b1c26401994140b Mon Sep 17 00:00:00 2001 From: Jhynjhiruu Date: Tue, 9 Apr 2024 15:51:40 +0100 Subject: [PATCH 12/12] Fix nits --- ctru-rs/examples/local-networking.rs | 10 +++++----- ctru-rs/src/services/uds.rs | 22 ++++++++++------------ 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/ctru-rs/examples/local-networking.rs b/ctru-rs/examples/local-networking.rs index 3871a1a..10467d5 100644 --- a/ctru-rs/examples/local-networking.rs +++ b/ctru-rs/examples/local-networking.rs @@ -7,7 +7,7 @@ use ctru::services::uds::*; fn handle_status_event(uds: &Uds, prev_node_mask: u16) -> ctru::Result { println!("Connection status event signalled"); - let status = uds.get_connection_status()?; + let status = uds.connection_status()?; println!("Status: {status:#02X?}"); let left = prev_node_mask & (status.node_bitmask() ^ prev_node_mask); let joined = status.node_bitmask() & (status.node_bitmask() ^ prev_node_mask); @@ -21,7 +21,7 @@ fn handle_status_event(uds: &Uds, prev_node_mask: u16) -> ctru::Result { println!( "Node {} connected: {:?}", i + 1, - uds.get_node_info(NodeID::Node(i + 1)) + uds.node_info(NodeID::Node(i + 1)) ); } } @@ -153,7 +153,7 @@ fn main() -> Result<(), Error> { } } State::Connect => { - let appdata = uds.get_network_appdata(&networks[selected_network], None)?; + let appdata = uds.network_appdata(&networks[selected_network], None)?; println!("App data: {:02X?}", appdata); if let Err(e) = uds.connect_network( @@ -167,10 +167,10 @@ fn main() -> Result<(), Error> { state = State::Initialised; println!("Press A to start scanning or B to create a new network"); } else { - channel = uds.get_channel()?; + channel = uds.channel()?; println!("Connected using channel {}", channel); - let appdata = uds.get_appdata(None)?; + let appdata = uds.appdata(None)?; println!("App data: {:02X?}", appdata); if uds.wait_status_event(false, false)? { diff --git a/ctru-rs/src/services/uds.rs b/ctru-rs/src/services/uds.rs index 1ec7407..58c7e4f 100644 --- a/ctru-rs/src/services/uds.rs +++ b/ctru-rs/src/services/uds.rs @@ -569,13 +569,13 @@ impl Uds { /// let mut uds = Uds::new(None)?; /// /// let networks = uds.scan(b"HBW\x10", None, None)?; - /// let appdata = uds.get_network_appdata(&networks[0], None)?; + /// let appdata = uds.network_appdata(&networks[0], None)?; /// # /// # Ok(()) /// # } /// ``` #[doc(alias = "udsGetNetworkStructApplicationData")] - pub fn get_network_appdata( + pub fn network_appdata( &self, network: &NetworkScanInfo, max_size: Option, @@ -625,13 +625,13 @@ impl Uds { /// /// 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)?; + /// let appdata = uds.appdata(None)?; /// # /// # Ok(()) /// # } /// ``` #[doc(alias = "udsGetApplicationData")] - pub fn get_appdata(&self, max_size: Option) -> Result, Error> { + pub fn appdata(&self, max_size: Option) -> Result, Error> { if self.service_status() == ServiceStatus::Disconnected { return Err(Error::NotConnected); } @@ -807,13 +807,13 @@ impl Uds { /// /// 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()?; + /// let channel = uds.channel()?; /// # /// # Ok(()) /// # } /// ``` #[doc(alias = "udsGetChannel")] - pub fn get_channel(&self) -> Result { + pub fn channel(&self) -> Result { if self.service_status() == ServiceStatus::Disconnected { return Err(Error::NotConnected); } @@ -870,8 +870,6 @@ impl Uds { /// Returns the current [`ConnectionStatus`] struct. /// - /// TODO: should this return an error if not connected? - /// /// # Example /// /// ``` @@ -886,7 +884,7 @@ impl Uds { /// 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()?; + /// let status = uds.connection_status()?; /// println!("Status: {status:#X?}"); /// } /// # @@ -894,7 +892,7 @@ impl Uds { /// # } /// ``` #[doc(alias = "udsGetConnectionStatus")] - pub fn get_connection_status(&self) -> crate::Result { + pub fn connection_status(&self) -> crate::Result { let mut status = MaybeUninit::uninit(); ResultCode(unsafe { ctru_sys::udsGetConnectionStatus(status.as_mut_ptr()) })?; @@ -1350,13 +1348,13 @@ impl Uds { /// let mut uds = Uds::new(None)?; /// /// uds.create_network(b"HBW\x10", None, None, b"udsdemo passphrase c186093cd2652741\0", 1)?; - /// let node_info = uds.get_node_info(NodeID::Node(2))?; + /// let node_info = uds.node_info(NodeID::Node(2))?; /// # /// # Ok(()) /// # } /// ``` #[doc(alias = "udsGetNodeInformation")] - pub fn get_node_info(&self, address: NodeID) -> Result { + pub fn node_info(&self, address: NodeID) -> Result { let NodeID::Node(node) = address else { return Err(Error::NotANode); };