- Track serial timeout in field instead of returning hardcoded 2s - Always restore timeout in drain_rx, even on read errors - Handle EINTR/Interrupted in read_frame (retry instead of error) - Use connect_timeout for TCP mux (no more 2-min hangs) - Set write timeout on mux sockets (unix + tcp) - Log unexpected unsolicited frames in recv via tracing::warn - Add unix socket timeout test for WouldBlock handling - Add CHANGELOG.md, remove resolved CLAUDE_TODO.md
229 lines
7.1 KiB
Rust
229 lines
7.1 KiB
Rust
//! Transport abstraction for byte-stream connections.
|
|
//!
|
|
//! The [`Transport`] trait unifies serial ports and mux socket connections,
|
|
//! allowing the [`Client`](crate::client::Client) to work over any of them.
|
|
|
|
use std::io::{self, Read, Write};
|
|
use std::time::Duration;
|
|
|
|
/// A byte-stream transport that the client can talk over.
|
|
pub trait Transport: Read + Write + Send {
|
|
/// Set the read timeout. A read that takes longer returns zero bytes.
|
|
fn set_timeout(&mut self, timeout: Duration) -> anyhow::Result<()>;
|
|
|
|
/// Get the current read timeout.
|
|
fn timeout(&self) -> Duration;
|
|
}
|
|
|
|
// ── Serial transport ───────────────────────────────────────────────
|
|
|
|
/// Direct USB serial port connection to a DongLoRa dongle.
|
|
pub struct SerialTransport {
|
|
port: Box<dyn serialport::SerialPort>,
|
|
timeout: Duration,
|
|
}
|
|
|
|
impl SerialTransport {
|
|
/// Open a serial port with the given timeout.
|
|
///
|
|
/// Baud rate is irrelevant for USB CDC-ACM but we pass a conventional value.
|
|
pub fn open(path: &str, timeout: Duration) -> anyhow::Result<Self> {
|
|
let port = serialport::new(path, 115_200)
|
|
.timeout(timeout)
|
|
.open()
|
|
.map_err(|e| anyhow::anyhow!("failed to open serial port {path}: {e}"))?;
|
|
Ok(Self { port, timeout })
|
|
}
|
|
|
|
/// Clear the serial input buffer.
|
|
pub fn reset_input_buffer(&self) -> anyhow::Result<()> {
|
|
self.port
|
|
.clear(serialport::ClearBuffer::Input)
|
|
.map_err(|e| anyhow::anyhow!("failed to clear input buffer: {e}"))
|
|
}
|
|
}
|
|
|
|
impl Read for SerialTransport {
|
|
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
|
self.port.read(buf)
|
|
}
|
|
}
|
|
|
|
impl Write for SerialTransport {
|
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
|
self.port.write(buf)
|
|
}
|
|
|
|
fn flush(&mut self) -> io::Result<()> {
|
|
self.port.flush()
|
|
}
|
|
}
|
|
|
|
impl Transport for SerialTransport {
|
|
fn set_timeout(&mut self, timeout: Duration) -> anyhow::Result<()> {
|
|
self.port
|
|
.set_timeout(timeout)
|
|
.map_err(|e| anyhow::anyhow!("failed to set serial timeout: {e}"))?;
|
|
self.timeout = timeout;
|
|
Ok(())
|
|
}
|
|
|
|
fn timeout(&self) -> Duration {
|
|
self.timeout
|
|
}
|
|
}
|
|
|
|
// ── Mux transport ──────────────────────────────────────────────────
|
|
|
|
/// Connection to the DongLoRa mux daemon via Unix socket or TCP.
|
|
pub struct MuxTransport {
|
|
stream: MuxStream,
|
|
timeout: Duration,
|
|
}
|
|
|
|
enum MuxStream {
|
|
#[cfg(unix)]
|
|
Unix(std::os::unix::net::UnixStream),
|
|
Tcp(std::net::TcpStream),
|
|
}
|
|
|
|
impl MuxTransport {
|
|
/// Connect to the mux daemon via Unix domain socket.
|
|
#[cfg(unix)]
|
|
pub fn unix(path: &str, timeout: Duration) -> anyhow::Result<Self> {
|
|
let stream = std::os::unix::net::UnixStream::connect(path)
|
|
.map_err(|e| anyhow::anyhow!("failed to connect to mux socket {path}: {e}"))?;
|
|
stream
|
|
.set_read_timeout(Some(timeout))
|
|
.map_err(|e| anyhow::anyhow!("failed to set socket read timeout: {e}"))?;
|
|
stream
|
|
.set_write_timeout(Some(timeout))
|
|
.map_err(|e| anyhow::anyhow!("failed to set socket write timeout: {e}"))?;
|
|
Ok(Self {
|
|
stream: MuxStream::Unix(stream),
|
|
timeout,
|
|
})
|
|
}
|
|
|
|
/// Connect to the mux daemon via TCP.
|
|
pub fn tcp(host: &str, port: u16, timeout: Duration) -> anyhow::Result<Self> {
|
|
use std::net::ToSocketAddrs;
|
|
|
|
let addr = format!("{host}:{port}");
|
|
let sock_addr = addr
|
|
.to_socket_addrs()
|
|
.map_err(|e| anyhow::anyhow!("failed to resolve mux address {addr}: {e}"))?
|
|
.next()
|
|
.ok_or_else(|| anyhow::anyhow!("no addresses found for {addr}"))?;
|
|
let stream = std::net::TcpStream::connect_timeout(&sock_addr, timeout)
|
|
.map_err(|e| anyhow::anyhow!("failed to connect to mux at {addr}: {e}"))?;
|
|
stream
|
|
.set_read_timeout(Some(timeout))
|
|
.map_err(|e| anyhow::anyhow!("failed to set TCP read timeout: {e}"))?;
|
|
stream
|
|
.set_write_timeout(Some(timeout))
|
|
.map_err(|e| anyhow::anyhow!("failed to set TCP write timeout: {e}"))?;
|
|
Ok(Self {
|
|
stream: MuxStream::Tcp(stream),
|
|
timeout,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Read for MuxTransport {
|
|
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
|
match &mut self.stream {
|
|
#[cfg(unix)]
|
|
MuxStream::Unix(s) => s.read(buf),
|
|
MuxStream::Tcp(s) => s.read(buf),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Write for MuxTransport {
|
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
|
match &mut self.stream {
|
|
#[cfg(unix)]
|
|
MuxStream::Unix(s) => s.write(buf),
|
|
MuxStream::Tcp(s) => s.write(buf),
|
|
}
|
|
}
|
|
|
|
fn flush(&mut self) -> io::Result<()> {
|
|
match &mut self.stream {
|
|
#[cfg(unix)]
|
|
MuxStream::Unix(s) => s.flush(),
|
|
MuxStream::Tcp(s) => s.flush(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Transport for MuxTransport {
|
|
fn set_timeout(&mut self, timeout: Duration) -> anyhow::Result<()> {
|
|
self.timeout = timeout;
|
|
let result = match &self.stream {
|
|
#[cfg(unix)]
|
|
MuxStream::Unix(s) => s
|
|
.set_read_timeout(Some(timeout))
|
|
.and_then(|()| s.set_write_timeout(Some(timeout))),
|
|
MuxStream::Tcp(s) => s
|
|
.set_read_timeout(Some(timeout))
|
|
.and_then(|()| s.set_write_timeout(Some(timeout))),
|
|
};
|
|
result.map_err(|e| anyhow::anyhow!("failed to set timeout: {e}"))
|
|
}
|
|
|
|
fn timeout(&self) -> Duration {
|
|
self.timeout
|
|
}
|
|
}
|
|
|
|
// ── AnyTransport ───────────────────────────────────────────────────
|
|
|
|
/// Type-erased transport for the [`connect`](crate::connect::connect) return type.
|
|
pub enum AnyTransport {
|
|
Serial(SerialTransport),
|
|
Mux(MuxTransport),
|
|
}
|
|
|
|
impl Read for AnyTransport {
|
|
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
|
match self {
|
|
Self::Serial(t) => t.read(buf),
|
|
Self::Mux(t) => t.read(buf),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Write for AnyTransport {
|
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
|
match self {
|
|
Self::Serial(t) => t.write(buf),
|
|
Self::Mux(t) => t.write(buf),
|
|
}
|
|
}
|
|
|
|
fn flush(&mut self) -> io::Result<()> {
|
|
match self {
|
|
Self::Serial(t) => t.flush(),
|
|
Self::Mux(t) => t.flush(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Transport for AnyTransport {
|
|
fn set_timeout(&mut self, timeout: Duration) -> anyhow::Result<()> {
|
|
match self {
|
|
Self::Serial(t) => t.set_timeout(timeout),
|
|
Self::Mux(t) => t.set_timeout(timeout),
|
|
}
|
|
}
|
|
|
|
fn timeout(&self) -> Duration {
|
|
match self {
|
|
Self::Serial(t) => t.timeout(),
|
|
Self::Mux(t) => t.timeout(),
|
|
}
|
|
}
|
|
}
|