Compare commits
66 Commits
testing/vu
...
main
Author | SHA1 | Date |
---|---|---|
xenua | 937f1dfd10 | 8 months ago |
Meziu | cd80a338fc | 9 months ago |
Jhynjhiruu | fe85aa2cfb | 9 months ago |
Jhynjhiruu | b9b8d6baf9 | 9 months ago |
Jhynjhiruu | dc70dfc771 | 9 months ago |
Meziu | d308205934 | 9 months ago |
Fenrir | ad8b328387 | 9 months ago |
Fenrir | 476a8120ea | 9 months ago |
Fenrir | d8cffcff61 | 9 months ago |
Fenrir | efaccdd2e5 | 9 months ago |
Ian Chamberlain | 519820bc0f | 9 months ago |
Jhynjhiruu | 33a4b1ca0c | 9 months ago |
Jhynjhiruu | 009bc7b4f1 | 10 months ago |
Fenrir | e5581b46e7 | 10 months ago |
Fenrir | 01731cac0e | 10 months ago |
Fenrir | 83ebecbb1f | 10 months ago |
FenrirWolf | c63932ccbb | 10 months ago |
Fenrir | c1f0bbd9c6 | 10 months ago |
FenrirWolf | 4b1c2e0b3c | 10 months ago |
Fenrir | 9396939d64 | 10 months ago |
Fenrir | b967440eb1 | 10 months ago |
Fenrir | 559c757d35 | 10 months ago |
Fenrir | efbf913380 | 10 months ago |
Fenrir | ffbe6604a6 | 10 months ago |
Fenrir | 6da884e7b6 | 10 months ago |
Fenrir | 5cec437e1d | 10 months ago |
Fenrir | d630c25926 | 10 months ago |
Ian Chamberlain | 432fca3c16 | 10 months ago |
Ian Chamberlain | eed5fc9a2e | 10 months ago |
Jhynjhiruu | e0c8a4d8f6 | 10 months ago |
Fenrir | a65b9ed577 | 10 months ago |
Fenrir | a67c8c0bf6 | 10 months ago |
Fenrir | 17dc4c4c76 | 10 months ago |
Fenrir | d32cd1525f | 10 months ago |
Fenrir | c6d5cdc367 | 10 months ago |
Fenrir | a9ded199fa | 10 months ago |
Fenrir | cd30981348 | 10 months ago |
Lena | 80853b2238 | 10 months ago |
Fenrir | 7a84ce3d70 | 10 months ago |
Fenrir | d4ad0ea8b0 | 10 months ago |
Fenrir | 1ef2164822 | 10 months ago |
Fenrir | becdc05abd | 10 months ago |
Fenrir | 2f7a04b08d | 10 months ago |
Fenrir | 3d59bdd2b7 | 10 months ago |
Fenrir | 032acb1524 | 10 months ago |
Fenrir | dea122ad16 | 10 months ago |
FenrirWolf | 10163fd225 | 10 months ago |
Lena | e6dddff3c3 | 10 months ago |
Fenrir | a18dfc49f7 | 10 months ago |
Fenrir | a75ac1ffd2 | 10 months ago |
Lena | f780bcedbb | 10 months ago |
Fenrir | dc2959396c | 10 months ago |
Lena | 0c55e1ef9b | 10 months ago |
Lena | 66e5a54952 | 10 months ago |
Lena | e2364ff5e1 | 10 months ago |
Lena | cc2e535e96 | 10 months ago |
Lena | 4f3562b523 | 10 months ago |
Lena | c95cd26d5c | 10 months ago |
Jhynjhiruu | 5eda3916ba | 11 months ago |
Jhynjhiruu | 433f9b0591 | 11 months ago |
Jhynjhiruu | 5feb906690 | 11 months ago |
Jhynjhiruu | 5287023486 | 11 months ago |
Jhynjhiruu | 1cdf4737f2 | 11 months ago |
Jhynjhiruu | bcb2be8cf7 | 11 months ago |
Jhynjhiruu | 4fd4ef08cb | 11 months ago |
Jhynjhiruu | f065d0da3d | 11 months ago |
22 changed files with 2095 additions and 193 deletions
@ -0,0 +1,301 @@ |
|||||||
|
//! Local networking example.
|
||||||
|
//!
|
||||||
|
//! This example showcases local networking using the UDS module.
|
||||||
|
|
||||||
|
use ctru::prelude::*; |
||||||
|
use ctru::services::uds::*; |
||||||
|
|
||||||
|
fn handle_status_event(uds: &Uds, prev_node_mask: u16) -> ctru::Result<u16> { |
||||||
|
println!("Connection status event signalled"); |
||||||
|
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); |
||||||
|
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.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(); |
||||||
|
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; |
||||||
|
|
||||||
|
let mut prev_node_mask = 0; |
||||||
|
|
||||||
|
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(); |
||||||
|
prev_node_mask = 0; |
||||||
|
} else if hid.keys_down().contains(KeyPad::B) { |
||||||
|
state = State::Create; |
||||||
|
console.clear(); |
||||||
|
prev_node_mask = 0; |
||||||
|
} |
||||||
|
} |
||||||
|
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].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.network_appdata(&networks[selected_network], None)?; |
||||||
|
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.channel()?; |
||||||
|
println!("Connected using channel {}", channel); |
||||||
|
|
||||||
|
let appdata = uds.appdata(None)?; |
||||||
|
println!("App data: {:02X?}", appdata); |
||||||
|
|
||||||
|
if uds.wait_status_event(false, false)? { |
||||||
|
prev_node_mask = handle_status_event(&uds, prev_node_mask)?; |
||||||
|
} |
||||||
|
|
||||||
|
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 {:?}", |
||||||
|
pkt[0], pkt[1], pkt[2], pkt[3], node |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
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()?; |
||||||
|
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(), |
||||||
|
NodeID::Broadcast, |
||||||
|
data_channel, |
||||||
|
SendFlags::Default, |
||||||
|
)?; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
Err(e) => { |
||||||
|
uds.disconnect_network()?; |
||||||
|
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::<Vec<_>>(); |
||||||
|
|
||||||
|
uds.set_appdata(&appdata)?; |
||||||
|
|
||||||
|
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 {:?}", |
||||||
|
pkt[0], pkt[1], pkt[2], pkt[3], node |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
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()?; |
||||||
|
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(), |
||||||
|
NodeID::Broadcast, |
||||||
|
data_channel, |
||||||
|
SendFlags::Default, |
||||||
|
)?; |
||||||
|
} |
||||||
|
} |
||||||
|
Err(e) => { |
||||||
|
uds.destroy_network()?; |
||||||
|
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"); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Ok(()) |
||||||
|
} |
@ -0,0 +1,153 @@ |
|||||||
|
//! Error applet.
|
||||||
|
//!
|
||||||
|
//! This applet displays error text as a pop-up message on the lower screen.
|
||||||
|
|
||||||
|
use crate::services::{apt::Apt, gfx::Gfx}; |
||||||
|
|
||||||
|
use ctru_sys::errorConf; |
||||||
|
|
||||||
|
/// Configuration struct to set up the Error applet.
|
||||||
|
#[doc(alias = "errorConf")] |
||||||
|
pub struct PopUp { |
||||||
|
state: Box<errorConf>, |
||||||
|
} |
||||||
|
|
||||||
|
/// Determines whether the Error applet will use word wrapping when displaying a message.
|
||||||
|
#[doc(alias = "errorType")] |
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)] |
||||||
|
#[repr(u32)] |
||||||
|
pub enum WordWrap { |
||||||
|
/// Error text is centered in the error applet window and does not use word wrapping.
|
||||||
|
Disabled = ctru_sys::ERROR_TEXT, |
||||||
|
/// Error text starts at the top of the error applet window and uses word wrapping.
|
||||||
|
Enabled = ctru_sys::ERROR_TEXT_WORD_WRAP, |
||||||
|
} |
||||||
|
|
||||||
|
/// Error returned by an unsuccessful [`PopUp::launch()`].
|
||||||
|
#[doc(alias = "errorReturnCode")] |
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)] |
||||||
|
#[repr(i8)] |
||||||
|
pub enum Error { |
||||||
|
/// Unknown error occurred.
|
||||||
|
Unknown = ctru_sys::ERROR_UNKNOWN, |
||||||
|
/// Operation not supported.
|
||||||
|
NotSupported = ctru_sys::ERROR_NOT_SUPPORTED, |
||||||
|
/// Home button pressed while [`PopUp`] was running.
|
||||||
|
HomePressed = ctru_sys::ERROR_HOME_BUTTON, |
||||||
|
/// Power button pressed while [`PopUp`] was running.
|
||||||
|
PowerPressed = ctru_sys::ERROR_POWER_BUTTON, |
||||||
|
/// Reset button pressed while [`PopUp`] was running.
|
||||||
|
ResetPressed = ctru_sys::ERROR_SOFTWARE_RESET, |
||||||
|
} |
||||||
|
|
||||||
|
impl PopUp { |
||||||
|
/// Initializes the error applet with the provided word wrap setting.
|
||||||
|
#[doc(alias = "errorInit")] |
||||||
|
pub fn new(word_wrap: WordWrap) -> Self { |
||||||
|
let mut state = Box::<errorConf>::default(); |
||||||
|
|
||||||
|
unsafe { ctru_sys::errorInit(state.as_mut(), word_wrap as _, 0) }; |
||||||
|
|
||||||
|
Self { state } |
||||||
|
} |
||||||
|
|
||||||
|
/// Sets the error text to display.
|
||||||
|
///
|
||||||
|
/// # Notes
|
||||||
|
///
|
||||||
|
/// The text will be converted to UTF-16 for display with the applet, and the message will be truncated if it exceeds
|
||||||
|
/// 1900 UTF-16 code units in length after conversion.
|
||||||
|
#[doc(alias = "errorText")] |
||||||
|
pub fn set_text(&mut self, text: &str) { |
||||||
|
for (idx, code_unit) in text |
||||||
|
.encode_utf16() |
||||||
|
.take(self.state.Text.len() - 1) |
||||||
|
.chain(std::iter::once(0)) |
||||||
|
.enumerate() |
||||||
|
{ |
||||||
|
self.state.Text[idx] = code_unit; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Launches the error applet.
|
||||||
|
#[doc(alias = "errorDisp")] |
||||||
|
pub fn launch(&mut self, _apt: &Apt, _gfx: &Gfx) -> Result<(), Error> { |
||||||
|
unsafe { self.launch_unchecked() } |
||||||
|
} |
||||||
|
|
||||||
|
/// Launches the error applet without requiring an [`Apt`] or [`Gfx`] handle.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// Potentially leads to undefined behavior if the aforementioned services are not actually active when the applet launches.
|
||||||
|
unsafe fn launch_unchecked(&mut self) -> Result<(), Error> { |
||||||
|
unsafe { ctru_sys::errorDisp(self.state.as_mut()) }; |
||||||
|
|
||||||
|
match self.state.returnCode { |
||||||
|
ctru_sys::ERROR_NONE | ctru_sys::ERROR_SUCCESS => Ok(()), |
||||||
|
ctru_sys::ERROR_NOT_SUPPORTED => Err(Error::NotSupported), |
||||||
|
ctru_sys::ERROR_HOME_BUTTON => Err(Error::HomePressed), |
||||||
|
ctru_sys::ERROR_POWER_BUTTON => Err(Error::PowerPressed), |
||||||
|
ctru_sys::ERROR_SOFTWARE_RESET => Err(Error::ResetPressed), |
||||||
|
_ => Err(Error::Unknown), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Sets a custom [panic hook](https://doc.rust-lang.org/std/panic/fn.set_hook.html) that uses the error applet to display panic messages.
|
||||||
|
///
|
||||||
|
/// You can also choose to have the previously registered panic hook called along with the error applet popup, which can be useful
|
||||||
|
/// if you want to use output redirection to display panic messages over `3dslink` or `GDB`.
|
||||||
|
///
|
||||||
|
/// You can use [`std::panic::take_hook`](https://doc.rust-lang.org/std/panic/fn.take_hook.html) to unregister the panic hook
|
||||||
|
/// set by this function.
|
||||||
|
///
|
||||||
|
/// # Notes
|
||||||
|
///
|
||||||
|
/// * If the [`Gfx`] service is not initialized during a panic, the error applet will not be displayed and the old panic hook will be called.
|
||||||
|
pub fn set_panic_hook(call_old_hook: bool) { |
||||||
|
use crate::services::gfx::GFX_ACTIVE; |
||||||
|
use std::sync::TryLockError; |
||||||
|
|
||||||
|
let old_hook = std::panic::take_hook(); |
||||||
|
|
||||||
|
std::panic::set_hook(Box::new(move |panic_info| { |
||||||
|
// If we get a `WouldBlock` error, we know that the `Gfx` service has been initialized.
|
||||||
|
// Otherwise fallback to using the old panic hook.
|
||||||
|
if let (Err(TryLockError::WouldBlock), Ok(_apt)) = (GFX_ACTIVE.try_lock(), Apt::new()) { |
||||||
|
if call_old_hook { |
||||||
|
old_hook(panic_info); |
||||||
|
} |
||||||
|
|
||||||
|
let thread = std::thread::current(); |
||||||
|
|
||||||
|
let name = thread.name().unwrap_or("<unnamed>"); |
||||||
|
|
||||||
|
let message = format!("thread '{name}' {panic_info}"); |
||||||
|
|
||||||
|
let mut popup = PopUp::new(WordWrap::Enabled); |
||||||
|
|
||||||
|
popup.set_text(&message); |
||||||
|
|
||||||
|
unsafe { |
||||||
|
let _ = popup.launch_unchecked(); |
||||||
|
} |
||||||
|
} else { |
||||||
|
old_hook(panic_info); |
||||||
|
} |
||||||
|
})); |
||||||
|
} |
||||||
|
|
||||||
|
impl std::fmt::Display for Error { |
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||||
|
match self { |
||||||
|
Self::NotSupported => write!(f, "operation not supported"), |
||||||
|
Self::HomePressed => write!(f, "home button pressed while error applet was running"), |
||||||
|
Self::PowerPressed => write!(f, "power button pressed while error applet was running"), |
||||||
|
Self::ResetPressed => write!(f, "reset button pressed while error applet was running"), |
||||||
|
Self::Unknown => write!(f, "an unknown error occurred"), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl std::error::Error for Error {} |
Loading…
Reference in new issue