cat_dev/mion/discovery.rs
1//! APIs for discovering Cat-Dev Bridge's, and more specifically their MION
2//! boards.
3//!
4//! There are two main groups of methods for attempting to find MIONs:
5//!
6//! 1. [`discover_bridges`], [`discover_bridges_with_logging_hooks`],
7//! [`discover_and_collect_bridges`], and
8//! [`discover_and_collect_bridges_with_logging_hooks`] incase you
9//! want to output values as you discover mions (processing them in a
10//! stream), or if you want to collect all the values in a single vector
11//! at the very end.
12//! 2. [`find_mion`] which finds a specific MION board based on one of the
13//! identifiers we know how to search for. *NOTE: in some cases this can
14//! lead to a full scan. See the API information for details.*
15//!
16//! It should be noted you can only find bridges that are on the same broadcast
17//! domain within your local network. In general this means under the same
18//! Subnet, and VLAN (unless your repeating broadcast packets across VLANs).
19//!
20//! If you are in different VLANs/Subnets and you do have the ability to run
21//! a repeater heading in BOTH directions (both are required for all bits of
22//! functionality!), you want to broadcast the port
23//! [`crate::mion::proto::DEFAULT_MION_CONTROL_PORT`] aka 7974. Otherwise things
24//! will not work. You may also need to broadcast whatever your configured ATAPI
25//! port is (by default this is also 7974, so not a worry.)
26
27use crate::{
28 errors::{CatBridgeError, NetworkError},
29 mion::proto::{
30 DEFAULT_MION_CONTROL_PORT, MION_ANNOUNCE_TIMEOUT_SECONDS,
31 control::{MionIdentity, MionIdentityAnnouncement},
32 },
33};
34use bytes::{Bytes, BytesMut};
35use fnv::FnvHashSet;
36use futures::stream::{StreamExt, unfold};
37use mac_address::MacAddress;
38use network_interface::{Addr, NetworkInterface, NetworkInterfaceConfig};
39use std::{
40 fmt::{Display, Formatter, Result as FmtResult},
41 hash::BuildHasherDefault,
42 net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4},
43};
44use tokio::{
45 net::UdpSocket,
46 sync::mpsc::{UnboundedReceiver, unbounded_channel},
47 task::JoinSet,
48 time::{Duration, Instant, sleep},
49};
50use tracing::{debug, error, warn};
51
52/// A small wrapper around [`discover_bridges`] that collects all the results
53/// into a list for you to parse through.
54///
55/// This will in general be slower than the `findbridge` cli tool, or even
56/// `bridgectl` because it will attempt to wait for the maximum amount of time
57/// in order to let any slow MIONs respond. Where as `bridgectl` (by default),
58/// and `findbridge` will attempt to exit early if they're not seeing a lot of
59/// responses on the network. To replicate their speed, and behaviour you can
60/// pass an early timeout of 3 seconds.
61///
62/// *note: you probably do not want to set `control_port`, we have not seen
63/// a mion respond on a separate port to this day, but certain tools do try
64/// to query other ports (We believe it's an unintentional bug, however, we
65/// expose it, just incase).
66///
67/// ## Errors
68///
69/// See the error notes for [`discover_bridges`].
70pub async fn discover_and_collect_bridges(
71 fetch_detailed_info: bool,
72 early_timeout: Option<Duration>,
73 override_control_port: Option<u16>,
74) -> Result<Vec<MionIdentity>, CatBridgeError> {
75 discover_and_collect_bridges_with_logging_hooks(
76 fetch_detailed_info,
77 early_timeout,
78 override_control_port,
79 noop_logger_interface,
80 )
81 .await
82}
83
84/// A small wrapper around [`discover_bridges`] that collects all the results
85/// into a list for you to parse through with extra logging handlers.
86///
87/// You ***probably*** don't want to call this directly, and instead call
88/// [`discover_bridges`] this can mainly be used for folks who need to create
89/// CLI tools with hyper-specific `println!`'s that don't use the normal
90/// [`tracing`] crate, or need some custom hooks to process data.
91///
92/// This will in general be slower than the `findbridge` cli tool, or even
93/// `bridgectl` because it will attempt to wait for the maximum amount of time
94/// in order to let any slow MIONs respond. Where as `bridgectl` (by default),
95/// and `findbridge` will attempt to exit early if they're not seeing a lot of
96/// responses on the network. To replicate their speed, and behaviour you can
97/// pass an early timeout of 3 seconds.
98///
99/// *note: you probably do not want to set `control_port`, we have not seen
100/// a mion respond on a separate port to this day, but certain tools do try
101/// to query other ports (We believe it's an unintentional bug, however, we
102/// expose it, just incase).
103///
104/// ## Errors
105///
106/// See the error notes for [`discover_bridges`].
107pub async fn discover_and_collect_bridges_with_logging_hooks<InterfaceLoggingHook>(
108 fetch_detailed_info: bool,
109 early_timeout: Option<Duration>,
110 override_control_port: Option<u16>,
111 interface_logging_hook: InterfaceLoggingHook,
112) -> Result<Vec<MionIdentity>, CatBridgeError>
113where
114 InterfaceLoggingHook: Fn(&'_ Addr) + Clone + Send + 'static,
115{
116 let mut recv_channel = discover_bridges_with_logging_hooks(
117 fetch_detailed_info,
118 override_control_port,
119 interface_logging_hook,
120 )
121 .await?;
122
123 let mut results = Vec::new();
124 loop {
125 tokio::select! {
126 opt = recv_channel.recv() => {
127 let Some(identity) = opt else {
128 // No more identities being received.
129 break;
130 };
131 // The same identity could be broadcast multiple times!
132 if !results.contains(&identity) {
133 results.push(identity);
134 }
135 }
136 () = sleep(early_timeout.unwrap_or(Duration::from_secs(MION_ANNOUNCE_TIMEOUT_SECONDS * 2))) => {
137 break;
138 }
139 }
140 }
141 Ok(results)
142}
143
144/// Discover all the Cat-Dev Bridges actively on the network.
145///
146/// NOTE: This will only find MIONs within the time window of
147/// [`crate::mion::proto::MION_ANNOUNCE_TIMEOUT_SECONDS`].
148/// To stop scanning for broadcasts early, simply close the receiving end of
149/// the channel.
150///
151/// This is what most users will actually want to mess with, as it simply logs
152/// using tracing, and returns the stream, AS devices are discovered. You might
153/// also want to use the api: [`discover_and_collect_bridges`]. Which handles
154/// all the scanning for you for 10 seconds, and then gives you the full list
155/// of discovered MIONs. Which also accepts an optional early timeout so you
156/// don't gotta wait the full seconds if you know you're not having some
157/// respond slowly.
158///
159/// *note: if you have multiple interfaces on the same network it is possible
160/// with this function to receive the same interface multiple times. you should
161/// handle any de-duping on your side!*
162///
163/// There are also two sister functions [`discover_bridges_with_logging_hooks`]
164/// and [`discover_and_collect_bridges_with_logging_hooks`]. Which are used by
165/// the command line tool `findbridge` in order to match the output of the
166/// original tools EXACTLY. For most users you probably don't want those
167/// logging hooks, as they ALREADY get piped through [`tracing`].
168///
169/// *note: you probably do not want to set `control_port`, we have not seen
170/// a mion respond on a separate port to this day, but certain tools do try
171/// to query other ports (We believe it's an unintentional bug, however, we
172/// expose it, just incase).
173///
174/// ## Errors
175///
176/// - If we fail to spawn a task to concurrently look up the MIONs.
177/// - If any background-task fails to create a socket, and broadcast on that
178/// socket.
179///
180/// They will also silently ignore any interfaces that are not up, if there is
181/// no IPv4 Address on the NIC, if we receive a packet from an IPv6 address, or
182/// finally if the broadcast packet is not a MION Identity response.
183pub async fn discover_bridges(
184 fetch_detailed_info: bool,
185 override_control_port: Option<u16>,
186) -> Result<UnboundedReceiver<MionIdentity>, CatBridgeError> {
187 discover_bridges_with_logging_hooks(
188 fetch_detailed_info,
189 override_control_port,
190 noop_logger_interface,
191 )
192 .await
193}
194
195/// Discover all the Cat-Dev Bridges actively on the network.
196///
197/// This is the function that allows you to specify EXTRA logging hooks (e.g.
198/// those that aren't written to [`tracing`], for like when you need to manually
199/// recreate a CLI with old hacky `println!`).
200///
201/// You probably want [`discover_bridges`].
202///
203/// *note: you probably do not want to set `control_port`, we have not seen
204/// a mion respond on a separate port to this day, but certain tools do try
205/// to query other ports (We believe it's an unintentional bug, however, we
206/// expose it, just incase).
207///
208/// ## Errors
209///
210/// See the error notes for [`discover_bridges`].
211pub async fn discover_bridges_with_logging_hooks<InterfaceLoggingHook>(
212 fetch_detailed_info: bool,
213 override_control_port: Option<u16>,
214 interface_logging_hook: InterfaceLoggingHook,
215) -> Result<UnboundedReceiver<MionIdentity>, CatBridgeError>
216where
217 InterfaceLoggingHook: Fn(&'_ Addr) + Clone + Send + 'static,
218{
219 let to_broadcast = Bytes::from(MionIdentityAnnouncement::new(fetch_detailed_info));
220 let mut tasks = JoinSet::new();
221
222 for (interface_addr, interface_ipv4) in get_all_broadcast_addresses()? {
223 let broadcast_messaged_cloned = to_broadcast.clone();
224 let cloned_iface_hook = interface_logging_hook.clone();
225 tasks
226 .build_task()
227 .name(&format!("cat_dev::discover_mion::{interface_ipv4}"))
228 .spawn(async move {
229 broadcast_to_mions_on_interface(
230 override_control_port,
231 broadcast_messaged_cloned,
232 interface_addr,
233 interface_ipv4,
234 cloned_iface_hook,
235 )
236 .await
237 })
238 .map_err(CatBridgeError::SpawnFailure)?;
239 }
240
241 let mut listening_sockets = Vec::with_capacity(tasks.len());
242 while let Some(joined) = tasks.join_next().await {
243 let joined_result = match joined {
244 Ok(data) => data,
245 Err(cause) => {
246 tasks.abort_all();
247 return Err(CatBridgeError::JoinFailure(cause));
248 }
249 };
250 let mut opt_socket = match joined_result {
251 Ok(optional_socket) => optional_socket,
252 Err(cause) => {
253 tasks.abort_all();
254 return Err(cause.into());
255 }
256 };
257 if let Some(socket) = opt_socket.take() {
258 listening_sockets.push(socket);
259 }
260 }
261
262 let mut our_addresses = FnvHashSet::with_capacity_and_hasher(
263 listening_sockets.len(),
264 BuildHasherDefault::default(),
265 );
266 for sock in &listening_sockets {
267 if let Ok(our_addr) = sock.local_addr() {
268 our_addresses.insert(our_addr.ip());
269 }
270 }
271
272 let streams = listening_sockets
273 .into_iter()
274 .map(|socket| Box::pin(unfold(socket, unfold_socket)))
275 .collect::<Vec<_>>();
276 // Combine every single socket receive into a single receive stream.
277 let mut single_stream = futures::stream::select_all(streams);
278 let timeout_at = Instant::now() + Duration::from_secs(MION_ANNOUNCE_TIMEOUT_SECONDS);
279 let (send, recv) = unbounded_channel::<MionIdentity>();
280
281 tokio::task::spawn(async move {
282 loop {
283 tokio::select! {
284 opt = single_stream.next() => {
285 let Some((read_data_len, from, mut buff)) = opt else {
286 continue;
287 };
288 buff.truncate(read_data_len);
289 let frozen = buff.freeze();
290
291 let from_ip = from.ip();
292 if our_addresses.contains(&from_ip) {
293 debug!("broadcast saw our own message");
294 continue;
295 }
296 let ip_address = match from_ip {
297 IpAddr::V4(v4) => v4,
298 IpAddr::V6(v6) => {
299 debug!(%v6, "broadcast packet from IPv6, ignoring, can't be announcement");
300 continue;
301 },
302 };
303
304 let Ok(identity) = MionIdentity::try_from((ip_address, frozen.clone())) else {
305 warn!(%from, packet = %format!("{frozen:02x?}"), "could not parse packet from MION");
306 continue;
307 };
308 if let Err(_closed) = send.send(identity) {
309 break;
310 }
311 }
312 () = tokio::time::sleep_until(timeout_at) => {
313 break;
314 }
315 }
316 }
317 });
318
319 Ok(recv)
320}
321
322/// Attempt to find a specific MION by searching for a specific field.
323///
324/// This _may_ cause a full discovery search to run, or may send a direct
325/// packet to the device itself.
326///
327/// *note: you probably do not want to set `control_port`, we have not seen
328/// a mion respond on a separate port to this day, but certain tools do try
329/// to query other ports (We believe it's an unintentional bug, however, we
330/// expose it, just incase).
331///
332/// ## Errors
333///
334/// - If we fail to spawn a task to concurrently look up the MIONs, and we need
335/// to do a full discovery search.
336/// - If any task fails to create a socket, and broadcast on that socket.
337pub async fn find_mion(
338 find_by: MionFindBy,
339 find_detailed: bool,
340 early_scan_timeout: Option<Duration>,
341 override_control_port: Option<u16>,
342) -> Result<Option<MionIdentity>, CatBridgeError> {
343 find_mion_with_logging_hooks(
344 find_by,
345 find_detailed,
346 early_scan_timeout,
347 override_control_port,
348 noop_logger_interface,
349 )
350 .await
351}
352
353/// Attempt to find a specific MION by searching for a specific field.
354///
355/// This _may_ cause a full discovery search to run, or may send a packet
356/// directly to the device itself.
357///
358/// You probably want [`find_mion`] without logging hooks. Again logs still get
359/// generated through the [`tracing`] crate. This is purely for those who need some
360/// extra manual logging, say because you're implementing a broken CLI.
361///
362/// It should also be noted YOU MAY NOT get logging callbacks, if we don't
363/// need to do a full scan. You can call [`MIONFindBy::will_cause_full_scan`]
364/// in order to determine if you'll get logging callbacks.
365///
366/// *note: you probably do not want to set `control_port`, we have not seen
367/// a mion respond on a separate port to this day, but certain tools do try
368/// to query other ports (We believe it's an unintentional bug, however, we
369/// expose it, just incase).
370///
371/// ## Errors
372///
373/// - If we fail to spawn a task to concurrently look up the MIONs, and we need
374/// to do a full discovery search.
375/// - If any task fails to create a socket, and broadcast on that socket.
376pub async fn find_mion_with_logging_hooks<InterfaceLoggingHook>(
377 find_by: MionFindBy,
378 find_detailed_info: bool,
379 early_scan_timeout: Option<Duration>,
380 override_control_port: Option<u16>,
381 interface_logging_hook: InterfaceLoggingHook,
382) -> Result<Option<MionIdentity>, CatBridgeError>
383where
384 InterfaceLoggingHook: Fn(&'_ Addr) + Clone + Send + 'static,
385{
386 let port = override_control_port.unwrap_or(DEFAULT_MION_CONTROL_PORT);
387 let (find_by_mac, find_by_name) = match find_by {
388 MionFindBy::Ip(ipv4) => {
389 let local_socket = UdpSocket::bind(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, port))
390 .await
391 .map_err(|_| NetworkError::BindFailure)?;
392 local_socket
393 .connect(SocketAddrV4::new(ipv4, port))
394 .await
395 .map_err(NetworkError::IO)?;
396 local_socket
397 .send(&Bytes::from(MionIdentityAnnouncement::new(
398 find_detailed_info,
399 )))
400 .await
401 .map_err(NetworkError::IO)?;
402
403 let mut buff = BytesMut::zeroed(8192);
404 tokio::select! {
405 result = local_socket.recv(&mut buff) => {
406 let actual_size = result.map_err(NetworkError::IO)?;
407 buff.truncate(actual_size);
408 }
409 () = sleep(Duration::from_secs(MION_ANNOUNCE_TIMEOUT_SECONDS)) => {
410 return Ok(None);
411 }
412 }
413 return Ok(Some(MionIdentity::try_from((ipv4, buff.freeze()))?));
414 }
415 MionFindBy::MacAddress(mac) => (Some(mac), None),
416 MionFindBy::Name(name) => (None, Some(name)),
417 };
418
419 let mut recv_channel = discover_bridges_with_logging_hooks(
420 find_detailed_info,
421 override_control_port,
422 interface_logging_hook,
423 )
424 .await?;
425 loop {
426 tokio::select! {
427 opt = recv_channel.recv() => {
428 let Some(identity) = opt else {
429 // No more identities being received.
430 break;
431 };
432
433 if let Some(filter_mac) = find_by_mac.as_ref() && *filter_mac == identity.mac_address() {
434 return Ok(Some(identity));
435 }
436 if let Some(filter_name) = find_by_name.as_ref() && filter_name == identity.name() {
437 return Ok(Some(identity));
438 }
439 }
440 () = sleep(early_scan_timeout.unwrap_or(Duration::from_secs(MION_ANNOUNCE_TIMEOUT_SECONDS * 2))) => {
441 break;
442 }
443 }
444 }
445
446 Ok(None)
447}
448
449/// A way to search for a single MION board.
450///
451/// Some of these can end up causing a full discovery broadcast, some of them
452/// cause just a single packet to a single ip address. You can parse these from
453/// a string or from one of the associated types.
454#[derive(Clone, Debug, Eq, Hash, PartialEq)]
455pub enum MionFindBy {
456 /// Search by a specific IP Address.
457 ///
458 /// The IP Address has to be a V4 address, as the MIONs do not actually
459 /// support being on an IPv6 address, and using `DHCPv6`.
460 ///
461 /// This searching type will only send a specific request to the specific
462 /// IPv4 address that you've specified here. IT WILL NOT cause a full
463 /// broadcast to happen.
464 Ip(Ipv4Addr),
465 /// Search by a mac address coming from a specific device.
466 ///
467 /// This searching type will cause a FULL Broadcast to happen. Meaning we
468 /// will receive potentially many mac addresses that we have to ignore. We
469 /// could in theory avoid this by using RARP (aka reverse arp) requests.
470 /// However, that requires running as an administrator on many OS's to issue
471 /// full RARP's requests. In theory we could parse things like
472 /// `/proc/net/arp`, but that requires doing things like sending
473 /// pings/broadcasts first which isn't always possible, especially because we
474 /// don't know the IP Address before hand.
475 ///
476 /// Maybe one day it would be possible to use RARP requests.
477 MacAddress(MacAddress),
478 /// Search by the name of a Cat-Dev Bridge.
479 ///
480 /// This searching type will cause a FULL Broadcast to happen. Meaning we
481 /// will receive potentially many broadcast responses that we might have to
482 /// ignore. There isn't really a way to avoid this without keeping a cache
483 /// somewhere. In theory a user could still this, and just pass in find by
484 /// ip with their own cache.
485 Name(String),
486}
487impl MionFindBy {
488 /// Techincally the name can collide with a mac address, and even techincally
489 /// an IP.
490 ///
491 /// To help provide similar APIs to the CLIs we offer
492 /// [`MionFindBy::Name`], and [`MionFindBy::from_name_or_ip`],
493 /// and finally [`MionFindBy::from`] to let you choose which collisions
494 /// if any you're okay with.
495 #[must_use]
496 pub fn from_name_or_ip(value: String) -> Self {
497 if let Ok(ipv4) = value.as_str().parse::<Ipv4Addr>() {
498 Self::Ip(ipv4)
499 } else {
500 Self::Name(value)
501 }
502 }
503
504 /// Determine if the scanning method you're actively using will cause a full
505 /// scan of the network.
506 #[must_use]
507 pub const fn will_cause_full_scan(&self) -> bool {
508 match self {
509 Self::Ip(_ip) => false,
510 Self::MacAddress(_mac) => true,
511 Self::Name(_name) => true,
512 }
513 }
514}
515impl From<String> for MionFindBy {
516 fn from(value: String) -> Self {
517 // First we try parsing a mac address, then an ipv4, before we just give up
518 // and use a name.
519 if let Ok(mac) = MacAddress::try_from(value.as_str()) {
520 Self::MacAddress(mac)
521 } else {
522 Self::from_name_or_ip(value)
523 }
524 }
525}
526impl Display for MionFindBy {
527 fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult {
528 match self {
529 Self::Ip(ip) => write!(fmt, "{ip}"),
530 Self::MacAddress(mac) => write!(fmt, "{mac}"),
531 Self::Name(name) => write!(fmt, "{name}"),
532 }
533 }
534}
535
536/// Get a list of all the network interfaces to actively scanning on.
537///
538/// NOTE: this doesn't actually fetch all the broadcast addresses, just the
539/// potential ones we MIGHT be able to scan on. This is specifically required
540/// for implementing the broken behavior of `findbridge`, which DOES NOT
541/// actually ensure that a broadcast address could be made at all.
542///
543/// Thanks `findbridge`.
544///
545/// ## Errors
546///
547/// - If we cannot list all the network interfaces present on the system.
548pub fn get_all_broadcast_addresses() -> Result<Vec<(Addr, Ipv4Addr)>, NetworkError> {
549 Ok(NetworkInterface::show()
550 .map_err(|cause| {
551 error!(?cause, "could not list network interfaces on this device");
552 NetworkError::ListInterfacesFailure(cause)
553 })?
554 .into_iter()
555 .fold(Vec::<(Addr, Ipv4Addr)>::new(), |mut accum, iface| {
556 for local_address in &iface.addr {
557 let ip = match local_address.ip() {
558 IpAddr::V4(v4) => v4,
559 IpAddr::V6(_) => {
560 debug!(?iface, ?local_address, "cannot broadcast to IPv6 addresses");
561 continue;
562 }
563 };
564
565 accum.push((*local_address, ip));
566 }
567
568 accum
569 }))
570}
571
572/// Broadcast to all the MIONs on a particular network interface.
573///
574/// This doesn't actually read the values (we want to queue up all the reads
575/// so we can read from them all concurrently with a timeout that applies to
576/// all of them).
577async fn broadcast_to_mions_on_interface<InterfaceLoggingHook>(
578 override_control_port: Option<u16>,
579 body_to_broadcast: Bytes,
580 interface_addr: Addr,
581 interface_ipv4: Ipv4Addr,
582 interface_hook: InterfaceLoggingHook,
583) -> Result<Option<UdpSocket>, NetworkError>
584where
585 InterfaceLoggingHook: Fn(&'_ Addr),
586{
587 // Nintendo just blindly prints this even if there is no broadcast address
588 // and IT WILL fail.
589 interface_hook(&interface_addr);
590 let Some(broadcast_address) = interface_addr.broadcast() else {
591 debug!(
592 ?interface_addr,
593 ?interface_ipv4,
594 "failed to get broadcast address"
595 );
596 return Ok(None);
597 };
598
599 debug!(
600 ?interface_addr,
601 ?interface_ipv4,
602 "actually broadcasting to interface"
603 );
604
605 let local_socket = UdpSocket::bind(SocketAddr::V4(SocketAddrV4::new(
606 interface_ipv4,
607 override_control_port.unwrap_or(DEFAULT_MION_CONTROL_PORT),
608 )))
609 .await
610 .map_err(|_| NetworkError::BindFailure)?;
611 local_socket
612 .set_broadcast(true)
613 .map_err(|_| NetworkError::SetBroadcastFailure)?;
614 local_socket
615 .send_to(
616 &body_to_broadcast,
617 SocketAddr::new(
618 broadcast_address,
619 override_control_port.unwrap_or(DEFAULT_MION_CONTROL_PORT),
620 ),
621 )
622 .await
623 .map_err(NetworkError::IO)?;
624 Ok(Some(local_socket))
625}
626
627/// Unfold sockets goal is to turn reading from a socket over & over into a
628/// stream.
629///
630/// When the stream has produced a value, and gets polled again,
631/// it queues up another read, and so on.
632async fn unfold_socket(sock: UdpSocket) -> Option<((usize, SocketAddr, BytesMut), UdpSocket)> {
633 let mut buff = BytesMut::zeroed(1024);
634 let Ok((len, addr)) = sock.recv_from(&mut buff).await else {
635 warn!("failed to receive data from broadcast socket");
636 return None;
637 };
638 Some(((len, addr, buff), sock))
639}
640
641/// A logger to use when we don't have another logger passed in.
642#[inline]
643fn noop_logger_interface(_: &Addr) {}
644
645#[cfg(test)]
646mod unit_tests {
647 use super::*;
648
649 #[test]
650 pub fn can_list_at_least_one_interface() {
651 assert!(
652 !get_all_broadcast_addresses()
653 .expect("Failed to list all broadcast addresses!")
654 .is_empty(),
655 "Failed to list all broadcast addresses... for some reason your PC isn't compatible to scan devices... perhaps you don't have a private IPv4 address?",
656 );
657 }
658
659 /// Although we can't actually scan for a real device, as not everyone will
660 /// have that device on their network.
661 ///
662 /// However, we can scan for a device that we know is guaranteed to not
663 /// exist, so we look for a device with a name that is non-ascii, as device
664 /// names have to be ascii.
665 #[tokio::test]
666 pub async fn cant_find_nonexisting_device() {
667 assert!(
668 find_mion(MionFindBy::Name("𩸽".to_owned()), false, None, None)
669 .await
670 .expect("Failed to scan to find a specific mion")
671 .is_none(),
672 "Somehow found a MION that can't exist?"
673 );
674 assert!(
675 find_mion(MionFindBy::Name("𩸽".to_owned()), true, None, None)
676 .await
677 .expect("Failed to scan to find a specific mion")
678 .is_none(),
679 "Somehow found a MION that can't exist?"
680 );
681 assert!(
682 find_mion(
683 MionFindBy::Name("𩸽".to_owned()),
684 true,
685 Some(Duration::from_secs(3)),
686 None,
687 )
688 .await
689 .expect("Failed to scan to find a specific mion")
690 .is_none(),
691 "Somehow found a MION that can't exist?"
692 );
693 }
694}