Krolyxon
Commit 62ea4cb

Branches

Refs →

budctlcratesbudctl-coresrctransportmod.rs

110 lines · 3.4 KB · Rust

1//! Byte transports. Device protocols only see the [`Transport`] trait, so they can be tested
2//! against a scripted mock instead of real buds.
3
4#[cfg(target_os = "linux")]
5use std::io;
6use std::time::Instant;
7
8use crate::debug;
9#[cfg(target_os = "linux")]
10use crate::error::Error;
11use crate::error::Result;
12
13#[cfg(target_os = "linux")]
14pub mod rfcomm;
15#[cfg(target_os = "linux")]
16pub mod sdp;
17#[cfg(target_os = "linux")]
18mod socket;
19
20#[cfg(test)]
21pub(crate) mod mock;
22
23/// A bidirectional byte stream to the buds.
24pub trait Transport: Send {
25 /// Send all of `bytes`.
26 fn send(&mut self, bytes: &[u8]) -> Result<()>;
27
28 /// The next bytes from the buds, or `None` once `deadline` passes with nothing to read.
29 fn recv(&mut self, deadline: Instant) -> Result<Option<Vec<u8>>>;
30}
31
32/// Wraps a transport and prints every chunk sent and received (`--debug`).
33pub struct Hexdump {
34 inner: Box<dyn Transport>,
35 start: Instant,
36}
37
38impl Hexdump {
39 pub fn new(inner: Box<dyn Transport>) -> Self {
40 Self {
41 inner,
42 start: Instant::now(),
43 }
44 }
45
46 fn print(&self, direction: &str, bytes: &[u8]) {
47 let elapsed = self.start.elapsed().as_secs_f64();
48 debug::line(&format!("{elapsed:>7.3} {direction} {}", debug::hex(bytes)));
49 }
50}
51
52impl Transport for Hexdump {
53 fn send(&mut self, bytes: &[u8]) -> Result<()> {
54 self.print("TX", bytes);
55 self.inner.send(bytes)
56 }
57
58 fn recv(&mut self, deadline: Instant) -> Result<Option<Vec<u8>>> {
59 let received = self.inner.recv(deadline)?;
60 if let Some(bytes) = &received {
61 self.print("RX", bytes);
62 }
63 Ok(received)
64 }
65}
66
67/// What an OS error on a Bluetooth socket means for budctl.
68/// `may_be_busy` is set only for the RFCOMM connect, where a refusal means another client
69/// holds the channel. Spec: transport.md#errno
70#[cfg(target_os = "linux")]
71pub(crate) fn classify(error: io::Error, context: &'static str, may_be_busy: bool) -> Error {
72 let detail = || format!("{context}: {error}");
73 match error.raw_os_error() {
74 Some(libc::EBUSY | libc::EADDRINUSE | libc::ECONNREFUSED) if may_be_busy => Error::ChannelBusy(detail()),
75 Some(
76 libc::EHOSTDOWN
77 | libc::EHOSTUNREACH
78 | libc::ENOTCONN
79 | libc::ECONNRESET
80 | libc::ECONNABORTED
81 | libc::EPIPE
82 | libc::ENODEV
83 | libc::ENETDOWN,
84 ) => Error::NotConnected(detail()),
85 Some(libc::ETIMEDOUT | libc::EINPROGRESS | libc::EAGAIN) => Error::Timeout(detail()),
86 _ => Error::Os { context, source: error },
87 }
88}
89
90#[cfg(all(test, target_os = "linux"))]
91mod tests {
92 use super::*;
93 use crate::error::Kind;
94
95 fn kind_of(errno: i32, may_be_busy: bool) -> Kind {
96 classify(io::Error::from_raw_os_error(errno), "test", may_be_busy).kind()
97 }
98
99 #[test]
100 fn errno_maps_to_the_documented_kinds() {
101 assert_eq!(kind_of(libc::ECONNREFUSED, true), Kind::Busy);
102 assert_eq!(kind_of(libc::EBUSY, true), Kind::Busy);
103 assert_eq!(kind_of(libc::ECONNREFUSED, false), Kind::NotFound);
104 assert_eq!(kind_of(libc::EHOSTDOWN, true), Kind::NotFound);
105 assert_eq!(kind_of(libc::ECONNRESET, false), Kind::NotFound);
106 assert_eq!(kind_of(libc::EINPROGRESS, true), Kind::Failed);
107 assert_eq!(kind_of(libc::ETIMEDOUT, false), Kind::Failed);
108 assert_eq!(kind_of(libc::EACCES, false), Kind::NotFound);
109 }
110}