Rewrite README in Chinese and English, add drone examples

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-06-12 21:56:20 +08:00
parent a2c7c0fa71
commit e939b16d2d
67 changed files with 1101 additions and 675 deletions

View File

@@ -0,0 +1,903 @@
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use tokio::io::{AsyncRead, AsyncWrite};
use tracing::info;
use crate::error::Result;
use crate::transport::serial::{
SerialDataBits, SerialFlowControl, SerialParity, SerialStopBits, SerialTransport,
};
use crate::transport::tcp::TcpTransport;
use crate::transport::udp::UdpTransport;
pub mod serial;
pub mod tcp;
pub mod udp;
fn default_serial_data_bits() -> SerialDataBits {
SerialDataBits::Eight
}
fn default_serial_parity() -> SerialParity {
SerialParity::None
}
fn default_serial_stop_bits() -> SerialStopBits {
SerialStopBits::One
}
fn default_serial_flow_control() -> SerialFlowControl {
SerialFlowControl::None
}
fn default_serial_dtr() -> bool {
false
}
fn default_serial_rts() -> bool {
false
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TransportType {
Serial,
Tcp,
Udp,
}
impl std::fmt::Display for TransportType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TransportType::Serial => write!(f, "Serial"),
TransportType::Tcp => write!(f, "Tcp"),
TransportType::Udp => write!(f, "Udp"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TransportConfig {
Serial {
port: String,
baud_rate: u32,
#[serde(default = "default_serial_data_bits")]
data_bits: SerialDataBits,
#[serde(default = "default_serial_parity")]
parity: SerialParity,
#[serde(default = "default_serial_stop_bits")]
stop_bits: SerialStopBits,
#[serde(default = "default_serial_flow_control")]
flow_control: SerialFlowControl,
#[serde(default = "default_serial_dtr")]
dtr: bool,
#[serde(default = "default_serial_rts")]
rts: bool,
},
Tcp {
addr: String,
},
Udp {
bind_addr: String,
remote_addr: Option<String>,
},
}
#[async_trait]
pub trait Transport: AsyncRead + AsyncWrite + Send + Sync + Unpin {
fn name(&self) -> &str;
fn transport_type(&self) -> TransportType;
fn is_connected(&self) -> bool;
async fn connect(&mut self) -> Result<()>;
async fn disconnect(&mut self) -> Result<()>;
}
#[derive(Debug)]
pub enum Connection {
Serial(SerialTransport),
Tcp(TcpTransport),
Udp(UdpTransport),
}
impl Connection {
pub fn new(config: TransportConfig) -> Self {
match config {
TransportConfig::Serial {
port,
baud_rate,
data_bits,
parity,
stop_bits,
flow_control,
dtr,
rts,
} => Connection::Serial(SerialTransport::new(
port,
baud_rate,
data_bits,
parity,
stop_bits,
flow_control,
dtr,
rts,
)),
TransportConfig::Tcp { addr } => Connection::Tcp(TcpTransport::new(addr)),
TransportConfig::Udp {
bind_addr,
remote_addr,
} => Connection::Udp(UdpTransport::new(bind_addr, remote_addr)),
}
}
pub fn transport_type(&self) -> TransportType {
match self {
Connection::Serial(_) => TransportType::Serial,
Connection::Tcp(_) => TransportType::Tcp,
Connection::Udp(_) => TransportType::Udp,
}
}
pub fn name(&self) -> &str {
match self {
Connection::Serial(t) => t.name(),
Connection::Tcp(t) => t.name(),
Connection::Udp(t) => t.name(),
}
}
pub fn is_connected(&self) -> bool {
match self {
Connection::Serial(t) => t.is_connected(),
Connection::Tcp(t) => t.is_connected(),
Connection::Udp(t) => t.is_connected(),
}
}
pub async fn connect(&mut self) -> Result<()> {
info!(transport = ?self.transport_type(), name = self.name(), "Connection connecting");
match self {
Connection::Serial(t) => t.connect().await,
Connection::Tcp(t) => t.connect().await,
Connection::Udp(t) => t.connect().await,
}
}
pub async fn disconnect(&mut self) -> Result<()> {
info!(name = self.name(), "Connection disconnecting");
match self {
Connection::Serial(t) => t.disconnect().await,
Connection::Tcp(t) => t.disconnect().await,
Connection::Udp(t) => t.disconnect().await,
}
}
pub fn set_dtr(&mut self, state: bool) -> Result<()> {
match self {
Connection::Serial(t) => t.set_dtr(state),
Connection::Tcp(_) | Connection::Udp(_) => {
Err(crate::error::Error::ConnectionFailed(
"DTR only supported on Serial connections".into(),
))
}
}
}
pub fn set_rts(&mut self, state: bool) -> Result<()> {
match self {
Connection::Serial(t) => t.set_rts(state),
Connection::Tcp(_) | Connection::Udp(_) => {
Err(crate::error::Error::ConnectionFailed(
"RTS only supported on Serial connections".into(),
))
}
}
}
}
impl AsyncRead for Connection {
fn poll_read(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &mut tokio::io::ReadBuf<'_>,
) -> std::task::Poll<std::io::Result<()>> {
match self.get_mut() {
Connection::Serial(t) => std::pin::Pin::new(t).poll_read(cx, buf),
Connection::Tcp(t) => std::pin::Pin::new(t).poll_read(cx, buf),
Connection::Udp(t) => std::pin::Pin::new(t).poll_read(cx, buf),
}
}
}
impl AsyncWrite for Connection {
fn poll_write(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &[u8],
) -> std::task::Poll<std::io::Result<usize>> {
match self.get_mut() {
Connection::Serial(t) => std::pin::Pin::new(t).poll_write(cx, buf),
Connection::Tcp(t) => std::pin::Pin::new(t).poll_write(cx, buf),
Connection::Udp(t) => std::pin::Pin::new(t).poll_write(cx, buf),
}
}
fn poll_flush(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
match self.get_mut() {
Connection::Serial(t) => std::pin::Pin::new(t).poll_flush(cx),
Connection::Tcp(t) => std::pin::Pin::new(t).poll_flush(cx),
Connection::Udp(t) => std::pin::Pin::new(t).poll_flush(cx),
}
}
fn poll_shutdown(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
match self.get_mut() {
Connection::Serial(t) => std::pin::Pin::new(t).poll_shutdown(cx),
Connection::Tcp(t) => std::pin::Pin::new(t).poll_shutdown(cx),
Connection::Udp(t) => std::pin::Pin::new(t).poll_shutdown(cx),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::transport::serial::{
SerialDataBits, SerialFlowControl, SerialParity, SerialStopBits,
};
fn serial_config(port: &str, baud_rate: u32) -> TransportConfig {
TransportConfig::Serial {
port: port.into(),
baud_rate,
data_bits: SerialDataBits::Eight,
parity: SerialParity::None,
stop_bits: SerialStopBits::One,
flow_control: SerialFlowControl::None,
dtr: false,
rts: false,
}
}
#[test]
fn test_transport_type_display() {
assert_eq!(TransportType::Serial.to_string(), "Serial");
assert_eq!(TransportType::Tcp.to_string(), "Tcp");
assert_eq!(TransportType::Udp.to_string(), "Udp");
}
#[test]
fn test_transport_type_debug() {
let _ = format!("{:?}", TransportType::Serial);
let _ = format!("{:?}", TransportType::Tcp);
let _ = format!("{:?}", TransportType::Udp);
}
#[test]
fn test_transport_type_eq() {
assert_eq!(TransportType::Serial, TransportType::Serial);
assert_eq!(TransportType::Tcp, TransportType::Tcp);
assert_eq!(TransportType::Udp, TransportType::Udp);
assert_ne!(TransportType::Serial, TransportType::Tcp);
assert_ne!(TransportType::Serial, TransportType::Udp);
assert_ne!(TransportType::Tcp, TransportType::Udp);
}
#[test]
fn test_transport_type_copy_clone() {
let original = TransportType::Serial;
let cloned = original;
assert_eq!(original, cloned);
let original = TransportType::Tcp;
let cloned = original;
assert_eq!(original, cloned);
let original = TransportType::Udp;
let cloned = original;
assert_eq!(original, cloned);
}
#[test]
fn test_transport_type_serialize() {
assert_eq!(
serde_json::to_string(&TransportType::Serial).unwrap(),
"\"Serial\""
);
assert_eq!(
serde_json::to_string(&TransportType::Tcp).unwrap(),
"\"Tcp\""
);
assert_eq!(
serde_json::to_string(&TransportType::Udp).unwrap(),
"\"Udp\""
);
}
#[test]
fn test_transport_type_deserialize() {
let t: TransportType = serde_json::from_str("\"Serial\"").unwrap();
assert_eq!(t, TransportType::Serial);
let t: TransportType = serde_json::from_str("\"Tcp\"").unwrap();
assert_eq!(t, TransportType::Tcp);
let t: TransportType = serde_json::from_str("\"Udp\"").unwrap();
assert_eq!(t, TransportType::Udp);
}
#[test]
fn test_transport_type_roundtrip() {
for original in [
TransportType::Serial,
TransportType::Tcp,
TransportType::Udp,
] {
let json = serde_json::to_string(&original).unwrap();
let roundtripped: TransportType = serde_json::from_str(&json).unwrap();
assert_eq!(original, roundtripped);
}
}
#[test]
fn test_transport_config_debug() {
let cfg = TransportConfig::Serial {
port: "COM1".into(),
baud_rate: 115200,
data_bits: SerialDataBits::Eight,
parity: SerialParity::None,
stop_bits: SerialStopBits::One,
flow_control: SerialFlowControl::None,
dtr: false,
rts: false,
};
let _ = format!("{:?}", cfg);
let cfg = TransportConfig::Tcp {
addr: "127.0.0.1:8080".into(),
};
let _ = format!("{:?}", cfg);
let cfg = TransportConfig::Udp {
bind_addr: "0.0.0.0:9000".into(),
remote_addr: Some("192.168.1.1:9001".into()),
};
let _ = format!("{:?}", cfg);
}
#[test]
fn test_transport_config_clone() {
let original = serial_config("COM1", 115200);
let cloned = original.clone();
assert_eq!(format!("{:?}", original), format!("{:?}", cloned));
let original = TransportConfig::Tcp {
addr: "127.0.0.1:8080".into(),
};
let cloned = original.clone();
assert_eq!(format!("{:?}", original), format!("{:?}", cloned));
let original = TransportConfig::Udp {
bind_addr: "0.0.0.0:9000".into(),
remote_addr: Some("192.168.1.1:9001".into()),
};
let cloned = original.clone();
assert_eq!(format!("{:?}", original), format!("{:?}", cloned));
}
#[test]
fn test_transport_config_serialize_serial() {
let cfg = serial_config("COM1", 115200);
let json = serde_json::to_string(&cfg).unwrap();
assert!(json.contains("COM1"));
assert!(json.contains("115200"));
assert!(json.contains("data_bits"));
assert!(json.contains("parity"));
assert!(json.contains("stop_bits"));
assert!(json.contains("flow_control"));
}
#[test]
fn test_transport_config_deserialize_serial() {
let json = r#"{"Serial":{"port":"COM1","baud_rate":9600}}"#;
let cfg: TransportConfig = serde_json::from_str(json).unwrap();
match cfg {
TransportConfig::Serial {
port,
baud_rate,
data_bits,
parity,
stop_bits,
flow_control,
dtr,
rts,
} => {
assert_eq!(port, "COM1");
assert_eq!(baud_rate, 9600);
assert_eq!(data_bits, SerialDataBits::Eight);
assert_eq!(parity, SerialParity::None);
assert_eq!(stop_bits, SerialStopBits::One);
assert_eq!(flow_control, SerialFlowControl::None);
assert!(!dtr);
assert!(!rts);
}
_ => panic!("expected Serial variant"),
}
}
#[test]
fn test_transport_config_deserialize_serial_with_explicit_options() {
let json = r#"{"Serial":{"port":"COM2","baud_rate":57600,"data_bits":"Seven","parity":"Even","stop_bits":"Two","flow_control":"Hardware"}}"#;
let cfg: TransportConfig = serde_json::from_str(json).unwrap();
match cfg {
TransportConfig::Serial {
port,
baud_rate,
data_bits,
parity,
stop_bits,
flow_control,
dtr,
rts,
} => {
assert_eq!(port, "COM2");
assert_eq!(baud_rate, 57600);
assert_eq!(data_bits, SerialDataBits::Seven);
assert_eq!(parity, SerialParity::Even);
assert_eq!(stop_bits, SerialStopBits::Two);
assert_eq!(flow_control, SerialFlowControl::Hardware);
assert!(!dtr);
assert!(!rts);
}
_ => panic!("expected Serial variant"),
}
}
#[test]
fn test_transport_config_serialize_tcp() {
let cfg = TransportConfig::Tcp {
addr: "127.0.0.1:8080".into(),
};
let json = serde_json::to_string(&cfg).unwrap();
assert!(json.contains("127.0.0.1:8080"));
}
#[test]
fn test_transport_config_deserialize_tcp() {
let json = r#"{"Tcp":{"addr":"127.0.0.1:8080"}}"#;
let cfg: TransportConfig = serde_json::from_str(json).unwrap();
match cfg {
TransportConfig::Tcp { addr } => {
assert_eq!(addr, "127.0.0.1:8080");
}
_ => panic!("expected Tcp variant"),
}
}
#[test]
fn test_transport_config_serialize_udp_with_remote() {
let cfg = TransportConfig::Udp {
bind_addr: "0.0.0.0:9000".into(),
remote_addr: Some("192.168.1.1:9001".into()),
};
let json = serde_json::to_string(&cfg).unwrap();
assert!(json.contains("0.0.0.0:9000"));
assert!(json.contains("192.168.1.1:9001"));
}
#[test]
fn test_transport_config_serialize_udp_without_remote() {
let cfg = TransportConfig::Udp {
bind_addr: "0.0.0.0:9000".into(),
remote_addr: None,
};
let json = serde_json::to_string(&cfg).unwrap();
assert!(json.contains("0.0.0.0:9000"));
assert!(json.contains("null"));
}
#[test]
fn test_transport_config_deserialize_udp() {
let json = r#"{"Udp":{"bind_addr":"0.0.0.0:9000","remote_addr":"192.168.1.1:9001"}}"#;
let cfg: TransportConfig = serde_json::from_str(json).unwrap();
match cfg {
TransportConfig::Udp {
bind_addr,
remote_addr,
} => {
assert_eq!(bind_addr, "0.0.0.0:9000");
assert_eq!(remote_addr, Some("192.168.1.1:9001".into()));
}
_ => panic!("expected Udp variant"),
}
let json = r#"{"Udp":{"bind_addr":"0.0.0.0:9000","remote_addr":null}}"#;
let cfg: TransportConfig = serde_json::from_str(json).unwrap();
match cfg {
TransportConfig::Udp {
bind_addr,
remote_addr,
} => {
assert_eq!(bind_addr, "0.0.0.0:9000");
assert_eq!(remote_addr, None);
}
_ => panic!("expected Udp variant"),
}
}
#[test]
fn test_transport_config_roundtrip() {
let configs = vec![
serial_config("COM3", 57600),
TransportConfig::Tcp {
addr: "10.0.0.1:9999".into(),
},
TransportConfig::Udp {
bind_addr: "0.0.0.0:7000".into(),
remote_addr: Some("10.0.0.2:7001".into()),
},
TransportConfig::Udp {
bind_addr: "127.0.0.1:8000".into(),
remote_addr: None,
},
];
for original in &configs {
let json = serde_json::to_string(original).unwrap();
let roundtripped: TransportConfig = serde_json::from_str(&json).unwrap();
assert_eq!(
serde_json::to_string(&roundtripped).unwrap(),
json,
"roundtrip serialized forms must match"
);
}
}
// ── Connection tests ──────────────────────────────────────────────
#[test]
fn test_connection_new_serial() {
let conn = Connection::new(serial_config("COM1", 115200));
assert_eq!(conn.transport_type(), TransportType::Serial);
assert_eq!(conn.name(), "COM1");
assert!(!conn.is_connected());
}
#[test]
fn test_connection_new_tcp() {
let conn = Connection::new(TransportConfig::Tcp {
addr: "192.168.1.1:8080".into(),
});
assert_eq!(conn.transport_type(), TransportType::Tcp);
assert_eq!(conn.name(), "192.168.1.1:8080");
assert!(!conn.is_connected());
}
#[test]
fn test_connection_new_udp_with_remote() {
let conn = Connection::new(TransportConfig::Udp {
bind_addr: "0.0.0.0:9000".into(),
remote_addr: Some("192.168.1.1:9001".into()),
});
assert_eq!(conn.transport_type(), TransportType::Udp);
assert_eq!(conn.name(), "0.0.0.0:9000");
assert!(!conn.is_connected());
}
#[test]
fn test_connection_new_udp_without_remote() {
let conn = Connection::new(TransportConfig::Udp {
bind_addr: "127.0.0.1:0".into(),
remote_addr: None,
});
assert_eq!(conn.transport_type(), TransportType::Udp);
assert_eq!(conn.name(), "127.0.0.1:0");
assert!(!conn.is_connected());
}
#[test]
fn test_connection_is_connected_default() {
let serial = Connection::new(serial_config("COM1", 9600));
let tcp = Connection::new(TransportConfig::Tcp {
addr: "127.0.0.1:8080".into(),
});
let udp = Connection::new(TransportConfig::Udp {
bind_addr: "0.0.0.0:0".into(),
remote_addr: None,
});
assert!(!serial.is_connected());
assert!(!tcp.is_connected());
assert!(!udp.is_connected());
}
#[test]
fn test_connection_debug() {
let serial = Connection::new(serial_config("COM1", 115200));
let tcp = Connection::new(TransportConfig::Tcp {
addr: "127.0.0.1:8080".into(),
});
let udp = Connection::new(TransportConfig::Udp {
bind_addr: "0.0.0.0:0".into(),
remote_addr: None,
});
let _ = format!("{:?}", serial);
let _ = format!("{:?}", tcp);
let _ = format!("{:?}", udp);
}
fn noop_waker() -> std::task::Waker {
use std::task::{RawWaker, RawWakerVTable};
unsafe fn clone(_: *const ()) -> RawWaker {
RawWaker::new(std::ptr::null(), &VTABLE)
}
unsafe fn wake(_: *const ()) {}
unsafe fn wake_by_ref(_: *const ()) {}
unsafe fn drop(_: *const ()) {}
static VTABLE: RawWakerVTable = RawWakerVTable::new(clone, wake, wake_by_ref, drop);
unsafe { std::task::Waker::from_raw(RawWaker::new(std::ptr::null(), &VTABLE)) }
}
fn serial_conn() -> Connection {
Connection::new(serial_config("COM1", 115200))
}
fn tcp_conn() -> Connection {
Connection::new(TransportConfig::Tcp {
addr: "127.0.0.1:8080".into(),
})
}
fn udp_conn() -> Connection {
Connection::new(TransportConfig::Udp {
bind_addr: "127.0.0.1:0".into(),
remote_addr: None,
})
}
#[test]
fn test_connection_poll_read_not_connected() {
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::io::ReadBuf;
for mut conn in [serial_conn(), tcp_conn(), udp_conn()] {
let pinned = Pin::new(&mut conn);
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
let mut buf_data = [0u8; 16];
let mut buf = ReadBuf::new(&mut buf_data);
match pinned.poll_read(&mut cx, &mut buf) {
Poll::Ready(Err(e)) => {
assert_eq!(e.kind(), std::io::ErrorKind::NotConnected);
}
other => panic!("expected Poll::Ready(Err(NotConnected)), got {:?}", other),
}
}
}
#[test]
fn test_connection_poll_write_not_connected() {
use std::pin::Pin;
use std::task::{Context, Poll};
for mut conn in [serial_conn(), tcp_conn(), udp_conn()] {
let pinned = Pin::new(&mut conn);
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
match pinned.poll_write(&mut cx, b"hello") {
Poll::Ready(Err(e)) => {
assert_eq!(e.kind(), std::io::ErrorKind::NotConnected);
}
other => panic!("expected Poll::Ready(Err(NotConnected)), got {:?}", other),
}
}
}
#[test]
fn test_connection_poll_flush_not_connected() {
use std::pin::Pin;
use std::task::{Context, Poll};
for mut conn in [serial_conn(), tcp_conn(), udp_conn()] {
let pinned = Pin::new(&mut conn);
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
match pinned.poll_flush(&mut cx) {
Poll::Ready(Ok(())) => {}
other => panic!("expected Poll::Ready(Ok(())), got {:?}", other),
}
}
}
#[test]
fn test_connection_poll_shutdown_not_connected() {
use std::pin::Pin;
use std::task::{Context, Poll};
for mut conn in [serial_conn(), tcp_conn(), udp_conn()] {
let pinned = Pin::new(&mut conn);
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
match pinned.poll_shutdown(&mut cx) {
Poll::Ready(Ok(())) => {}
other => panic!("expected Poll::Ready(Ok(())), got {:?}", other),
}
}
}
#[tokio::test]
async fn test_connection_tcp_connect_and_disconnect() {
use tokio::net::TcpListener;
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap().to_string();
let mut conn = Connection::new(TransportConfig::Tcp { addr });
assert!(!conn.is_connected());
conn.connect().await.unwrap();
assert!(conn.is_connected());
conn.disconnect().await.unwrap();
assert!(!conn.is_connected());
}
#[tokio::test]
async fn test_connection_udp_connect_and_disconnect_without_remote() {
let mut conn = Connection::new(TransportConfig::Udp {
bind_addr: "127.0.0.1:0".into(),
remote_addr: None,
});
assert!(!conn.is_connected());
conn.connect().await.unwrap();
assert!(conn.is_connected());
conn.disconnect().await.unwrap();
assert!(!conn.is_connected());
}
#[tokio::test]
async fn test_connection_udp_connect_and_disconnect_with_remote() {
use tokio::net::UdpSocket;
let remote = UdpSocket::bind("127.0.0.1:0").await.unwrap();
let remote_addr = remote.local_addr().unwrap().to_string();
let mut conn = Connection::new(TransportConfig::Udp {
bind_addr: "127.0.0.1:0".into(),
remote_addr: Some(remote_addr),
});
assert!(!conn.is_connected());
conn.connect().await.unwrap();
assert!(conn.is_connected());
conn.disconnect().await.unwrap();
assert!(!conn.is_connected());
}
#[tokio::test]
async fn test_connection_tcp_double_connect_is_noop() {
use tokio::net::TcpListener;
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap().to_string();
let mut conn = Connection::new(TransportConfig::Tcp { addr });
conn.connect().await.unwrap();
assert!(conn.is_connected());
let result = conn.connect().await;
assert!(result.is_ok(), "double connect should be a no-op");
assert!(conn.is_connected());
}
#[tokio::test]
async fn test_connection_tcp_disconnect_without_connect() {
let mut conn = Connection::new(TransportConfig::Tcp {
addr: "127.0.0.1:8080".into(),
});
let result = conn.disconnect().await;
assert!(result.is_ok(), "disconnect without connect should be safe");
assert!(!conn.is_connected());
}
#[tokio::test]
async fn test_connection_tcp_connect_to_unreachable() {
let mut conn = Connection::new(TransportConfig::Tcp {
addr: "127.0.0.1:1".into(),
});
let result = conn.connect().await;
assert!(result.is_err());
assert!(!conn.is_connected());
}
#[tokio::test]
async fn test_connection_udp_connect_bind_failure() {
let mut conn = Connection::new(TransportConfig::Udp {
bind_addr: "invalid_addr".into(),
remote_addr: None,
});
let result = conn.connect().await;
assert!(result.is_err());
assert!(!conn.is_connected());
}
#[tokio::test]
async fn test_connection_udp_double_connect_is_noop() {
let mut conn = Connection::new(TransportConfig::Udp {
bind_addr: "127.0.0.1:0".into(),
remote_addr: None,
});
conn.connect().await.unwrap();
assert!(conn.is_connected());
let result = conn.connect().await;
assert!(result.is_ok());
assert!(conn.is_connected());
}
#[tokio::test]
async fn test_connection_tcp_async_read_write() {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap().to_string();
let server = tokio::spawn(async move {
let (mut stream, _) = listener.accept().await.unwrap();
AsyncWriteExt::write_all(&mut stream, b"hello")
.await
.unwrap();
let mut buf = [0u8; 5];
AsyncReadExt::read_exact(&mut stream, &mut buf)
.await
.unwrap();
assert_eq!(&buf, b"world");
});
let mut conn = Connection::new(TransportConfig::Tcp { addr });
conn.connect().await.unwrap();
let mut buf = [0u8; 5];
AsyncReadExt::read_exact(&mut conn, &mut buf).await.unwrap();
assert_eq!(&buf, b"hello");
AsyncWriteExt::write_all(&mut conn, b"world").await.unwrap();
conn.disconnect().await.unwrap();
server.await.unwrap();
}
#[tokio::test]
async fn test_connection_udp_async_read_write_with_remote() {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::UdpSocket;
let remote = UdpSocket::bind("127.0.0.1:0").await.unwrap();
let remote_addr = remote.local_addr().unwrap().to_string();
let mut conn = Connection::new(TransportConfig::Udp {
bind_addr: "127.0.0.1:0".into(),
remote_addr: Some(remote_addr),
});
conn.connect().await.unwrap();
AsyncWriteExt::write_all(&mut conn, b"hello").await.unwrap();
let mut buf = [0u8; 1024];
let (n, from) = remote.recv_from(&mut buf).await.unwrap();
assert_eq!(&buf[..n], b"hello");
remote.send_to(b"world", from).await.unwrap();
let mut read_buf = [0u8; 5];
AsyncReadExt::read_exact(&mut conn, &mut read_buf)
.await
.unwrap();
assert_eq!(&read_buf, b"world");
}
}

View File

@@ -0,0 +1,556 @@
use async_trait::async_trait;
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use tokio_serial::{DataBits, FlowControl, Parity, SerialPortBuilderExt, SerialStream, StopBits};
use serialport::SerialPort;
use tracing::{debug, info, warn};
use super::{Transport, TransportType};
use crate::error::Result;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum SerialDataBits {
Five,
Six,
Seven,
Eight,
}
impl From<SerialDataBits> for DataBits {
fn from(value: SerialDataBits) -> Self {
match value {
SerialDataBits::Five => DataBits::Five,
SerialDataBits::Six => DataBits::Six,
SerialDataBits::Seven => DataBits::Seven,
SerialDataBits::Eight => DataBits::Eight,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum SerialParity {
None,
Odd,
Even,
}
impl From<SerialParity> for Parity {
fn from(value: SerialParity) -> Self {
match value {
SerialParity::None => Parity::None,
SerialParity::Odd => Parity::Odd,
SerialParity::Even => Parity::Even,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum SerialStopBits {
One,
Two,
}
impl From<SerialStopBits> for StopBits {
fn from(value: SerialStopBits) -> Self {
match value {
SerialStopBits::One => StopBits::One,
SerialStopBits::Two => StopBits::Two,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum SerialFlowControl {
None,
Software,
Hardware,
}
impl From<SerialFlowControl> for FlowControl {
fn from(value: SerialFlowControl) -> Self {
match value {
SerialFlowControl::None => FlowControl::None,
SerialFlowControl::Software => FlowControl::Software,
SerialFlowControl::Hardware => FlowControl::Hardware,
}
}
}
#[derive(Debug)]
pub struct SerialTransport {
port: Option<SerialStream>,
port_name: String,
baud_rate: u32,
data_bits: SerialDataBits,
parity: SerialParity,
stop_bits: SerialStopBits,
flow_control: SerialFlowControl,
dtr: bool,
rts: bool,
}
impl SerialTransport {
#[allow(clippy::too_many_arguments)]
pub fn new(
port_name: String,
baud_rate: u32,
data_bits: SerialDataBits,
parity: SerialParity,
stop_bits: SerialStopBits,
flow_control: SerialFlowControl,
dtr: bool,
rts: bool,
) -> Self {
Self {
port: None,
port_name,
baud_rate,
data_bits,
parity,
stop_bits,
flow_control,
dtr,
rts,
}
}
pub fn list_ports() -> Vec<serialport::SerialPortInfo> {
match serialport::available_ports() {
Ok(ports) => ports,
Err(e) => {
warn!("Failed to enumerate serial ports: {}", e);
vec![]
}
}
}
pub fn port_name(&self) -> &str {
&self.port_name
}
pub fn baud_rate(&self) -> u32 {
self.baud_rate
}
pub fn data_bits(&self) -> SerialDataBits {
self.data_bits
}
pub fn parity(&self) -> SerialParity {
self.parity
}
pub fn stop_bits(&self) -> SerialStopBits {
self.stop_bits
}
pub fn flow_control(&self) -> SerialFlowControl {
self.flow_control
}
pub fn set_dtr(&mut self, state: bool) -> Result<()> {
match &mut self.port {
Some(port) => {
port.write_data_terminal_ready(state)?;
self.dtr = state;
debug!("DTR set to {} on {}", state, self.port_name);
Ok(())
}
None => Err(crate::error::Error::ConnectionFailed(format!(
"port {} not open",
self.port_name
))),
}
}
pub fn set_rts(&mut self, state: bool) -> Result<()> {
match &mut self.port {
Some(port) => {
port.write_request_to_send(state)?;
self.rts = state;
debug!("RTS set to {} on {}", state, self.port_name);
Ok(())
}
None => Err(crate::error::Error::ConnectionFailed(format!(
"port {} not open",
self.port_name
))),
}
}
}
impl AsyncRead for SerialTransport {
fn poll_read(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &mut ReadBuf<'_>,
) -> std::task::Poll<std::io::Result<()>> {
match &mut self.get_mut().port {
Some(port) => std::pin::Pin::new(port).poll_read(cx, buf),
None => std::task::Poll::Ready(Err(std::io::Error::new(
std::io::ErrorKind::NotConnected,
"Serial port not open",
))),
}
}
}
impl AsyncWrite for SerialTransport {
fn poll_write(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &[u8],
) -> std::task::Poll<std::io::Result<usize>> {
match &mut self.get_mut().port {
Some(port) => std::pin::Pin::new(port).poll_write(cx, buf),
None => std::task::Poll::Ready(Err(std::io::Error::new(
std::io::ErrorKind::NotConnected,
"Serial port not open",
))),
}
}
fn poll_flush(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
match &mut self.get_mut().port {
Some(port) => std::pin::Pin::new(port).poll_flush(cx),
None => std::task::Poll::Ready(Ok(())),
}
}
fn poll_shutdown(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
match &mut self.get_mut().port {
Some(port) => std::pin::Pin::new(port).poll_shutdown(cx),
None => std::task::Poll::Ready(Ok(())),
}
}
}
#[async_trait]
impl Transport for SerialTransport {
fn name(&self) -> &str {
&self.port_name
}
fn transport_type(&self) -> TransportType {
TransportType::Serial
}
fn is_connected(&self) -> bool {
self.port.is_some()
}
async fn connect(&mut self) -> Result<()> {
if self.is_connected() {
return Ok(());
}
info!(
"Opening serial port {} at {} baud ({:?}, {:?}, {:?}, {:?})",
self.port_name,
self.baud_rate,
self.data_bits,
self.parity,
self.stop_bits,
self.flow_control
);
let mut port = tokio_serial::new(&self.port_name, self.baud_rate)
.data_bits(self.data_bits.into())
.parity(self.parity.into())
.stop_bits(self.stop_bits.into())
.flow_control(self.flow_control.into())
.open_native_async()
.map_err(|e| {
crate::error::Error::ConnectionFailed(format!(
"Failed to open {}: {}",
self.port_name, e
))
})?;
// Linux kernel toggles DTR/RTS on every open() — restore configured
// state immediately afterward. Many wireless serial modules (HC-12,
// HC-15, HC-05, Bluetooth/UART bridges) need DTR asserted to stay in
// transparent data mode and not fall into AT-command / reset state.
if let Err(e) = port.write_data_terminal_ready(self.dtr) {
warn!("Failed to set DTR({}) on {}: {}", self.dtr, self.port_name, e);
}
if let Err(e) = port.write_request_to_send(self.rts) {
warn!("Failed to set RTS({}) on {}: {}", self.rts, self.port_name, e);
}
debug!("Serial port {} opened successfully", self.port_name);
self.port = Some(port);
Ok(())
}
async fn disconnect(&mut self) -> Result<()> {
if let Some(port) = self.port.take() {
debug!("Closing serial port {}", self.port_name);
drop(port);
debug!("Serial port {} closed", self.port_name);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::ErrorKind;
use std::pin::Pin;
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
use tokio::io::ReadBuf;
fn noop_waker() -> Waker {
unsafe fn raw_clone(_: *const ()) -> RawWaker {
RawWaker::new(std::ptr::null(), &RAW_VTABLE)
}
unsafe fn raw_wake(_: *const ()) {}
unsafe fn raw_wake_by_ref(_: *const ()) {}
unsafe fn raw_drop(_: *const ()) {}
static RAW_VTABLE: RawWakerVTable =
RawWakerVTable::new(raw_clone, raw_wake, raw_wake_by_ref, raw_drop);
unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &RAW_VTABLE)) }
}
// ── Constructor and accessor tests ──
#[test]
fn test_new() {
let transport = SerialTransport::new(
"COM1".into(),
115200,
SerialDataBits::Eight,
SerialParity::None,
SerialStopBits::One,
SerialFlowControl::None,
true,
true,
);
assert_eq!(transport.port_name(), "COM1");
assert_eq!(transport.baud_rate(), 115200);
}
#[test]
fn test_new_different_params() {
let transport = SerialTransport::new(
"COM3".into(),
9600,
SerialDataBits::Seven,
SerialParity::Even,
SerialStopBits::Two,
SerialFlowControl::Hardware,
true,
true,
);
assert_eq!(transport.port_name(), "COM3");
assert_eq!(transport.baud_rate(), 9600);
assert_eq!(transport.data_bits(), SerialDataBits::Seven);
assert_eq!(transport.parity(), SerialParity::Even);
assert_eq!(transport.stop_bits(), SerialStopBits::Two);
assert_eq!(transport.flow_control(), SerialFlowControl::Hardware);
}
#[test]
fn test_name() {
let transport = SerialTransport::new(
"COM1".into(),
115200,
SerialDataBits::Eight,
SerialParity::None,
SerialStopBits::One,
SerialFlowControl::None,
true,
true,
);
assert_eq!(transport.name(), "COM1");
}
#[test]
fn test_transport_type() {
let transport = SerialTransport::new(
"COM1".into(),
115200,
SerialDataBits::Eight,
SerialParity::None,
SerialStopBits::One,
SerialFlowControl::None,
true,
true,
);
assert_eq!(transport.transport_type(), TransportType::Serial);
}
#[test]
fn test_is_connected_false_by_default() {
let transport = SerialTransport::new(
"COM1".into(),
115200,
SerialDataBits::Eight,
SerialParity::None,
SerialStopBits::One,
SerialFlowControl::None,
true,
true,
);
assert!(!transport.is_connected());
}
// ── list_ports tests ──
#[test]
fn test_list_ports_does_not_panic() {
let _ = SerialTransport::list_ports();
}
#[test]
fn test_list_ports_returns_vec() {
let ports: Vec<serialport::SerialPortInfo> = SerialTransport::list_ports();
let _ = ports;
}
// ── AsyncRead tests (not connected) ──
#[tokio::test]
async fn test_poll_read_not_connected() {
let mut transport = SerialTransport::new(
"COM1".into(),
115200,
SerialDataBits::Eight,
SerialParity::None,
SerialStopBits::One,
SerialFlowControl::None,
true,
true,
);
let pinned = Pin::new(&mut transport);
let mut buf_data = [0u8; 16];
let mut buf = ReadBuf::new(&mut buf_data);
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
match pinned.poll_read(&mut cx, &mut buf) {
Poll::Ready(Err(e)) => assert_eq!(e.kind(), ErrorKind::NotConnected),
other => panic!("expected Poll::Ready(Err(NotConnected)), got {:?}", other),
}
}
// ── AsyncWrite tests (not connected) ──
#[tokio::test]
async fn test_poll_write_not_connected() {
let mut transport = SerialTransport::new(
"COM1".into(),
115200,
SerialDataBits::Eight,
SerialParity::None,
SerialStopBits::One,
SerialFlowControl::None,
true,
true,
);
let pinned = Pin::new(&mut transport);
let data = b"hello";
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
match pinned.poll_write(&mut cx, data) {
Poll::Ready(Err(e)) => assert_eq!(e.kind(), ErrorKind::NotConnected),
other => panic!("expected Poll::Ready(Err(NotConnected)), got {:?}", other),
}
}
#[tokio::test]
async fn test_poll_flush_not_connected() {
let mut transport = SerialTransport::new(
"COM1".into(),
115200,
SerialDataBits::Eight,
SerialParity::None,
SerialStopBits::One,
SerialFlowControl::None,
true,
true,
);
let pinned = Pin::new(&mut transport);
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
match pinned.poll_flush(&mut cx) {
Poll::Ready(Ok(())) => {}
other => panic!("expected Poll::Ready(Ok(())), got {:?}", other),
}
}
#[tokio::test]
async fn test_poll_shutdown_not_connected() {
let mut transport = SerialTransport::new(
"COM1".into(),
115200,
SerialDataBits::Eight,
SerialParity::None,
SerialStopBits::One,
SerialFlowControl::None,
true,
true,
);
let pinned = Pin::new(&mut transport);
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
match pinned.poll_shutdown(&mut cx) {
Poll::Ready(Ok(())) => {}
other => panic!("expected Poll::Ready(Ok(())), got {:?}", other),
}
}
// ── Transport trait method tests ──
#[test]
fn test_transport_name() {
let transport = SerialTransport::new(
"COM1".into(),
115200,
SerialDataBits::Eight,
SerialParity::None,
SerialStopBits::One,
SerialFlowControl::None,
true,
true,
);
assert_eq!(transport.name(), "COM1");
}
#[test]
fn test_transport_transport_type() {
let transport = SerialTransport::new(
"COM1".into(),
115200,
SerialDataBits::Eight,
SerialParity::None,
SerialStopBits::One,
SerialFlowControl::None,
true,
true,
);
assert_eq!(transport.transport_type(), TransportType::Serial);
}
#[test]
fn test_transport_is_connected() {
let transport = SerialTransport::new(
"COM1".into(),
115200,
SerialDataBits::Eight,
SerialParity::None,
SerialStopBits::One,
SerialFlowControl::None,
true,
true,
);
assert!(!transport.is_connected());
}
}

View File

@@ -0,0 +1,324 @@
use async_trait::async_trait;
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use tokio::net::TcpStream;
use tracing::{debug, info};
use super::{Transport, TransportType};
use crate::error::Result;
#[derive(Debug)]
pub struct TcpTransport {
stream: Option<TcpStream>,
addr: String,
}
impl TcpTransport {
pub fn new(addr: String) -> Self {
Self { stream: None, addr }
}
pub fn addr(&self) -> &str {
&self.addr
}
}
impl AsyncRead for TcpTransport {
fn poll_read(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &mut ReadBuf<'_>,
) -> std::task::Poll<std::io::Result<()>> {
match &mut self.get_mut().stream {
Some(stream) => std::pin::Pin::new(stream).poll_read(cx, buf),
None => std::task::Poll::Ready(Err(std::io::Error::new(
std::io::ErrorKind::NotConnected,
"TCP not connected",
))),
}
}
}
impl AsyncWrite for TcpTransport {
fn poll_write(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &[u8],
) -> std::task::Poll<std::io::Result<usize>> {
match &mut self.get_mut().stream {
Some(stream) => std::pin::Pin::new(stream).poll_write(cx, buf),
None => std::task::Poll::Ready(Err(std::io::Error::new(
std::io::ErrorKind::NotConnected,
"TCP not connected",
))),
}
}
fn poll_flush(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
match &mut self.get_mut().stream {
Some(stream) => std::pin::Pin::new(stream).poll_flush(cx),
None => std::task::Poll::Ready(Ok(())),
}
}
fn poll_shutdown(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
match &mut self.get_mut().stream {
Some(stream) => std::pin::Pin::new(stream).poll_shutdown(cx),
None => std::task::Poll::Ready(Ok(())),
}
}
}
#[async_trait]
impl Transport for TcpTransport {
fn name(&self) -> &str {
&self.addr
}
fn transport_type(&self) -> TransportType {
TransportType::Tcp
}
fn is_connected(&self) -> bool {
self.stream.is_some()
}
async fn connect(&mut self) -> Result<()> {
if self.is_connected() {
return Ok(());
}
info!("Connecting to TCP {}", self.addr);
let stream = TcpStream::connect(&self.addr).await.map_err(|e| {
crate::error::Error::ConnectionFailed(format!(
"Failed to connect to {}: {}",
self.addr, e
))
})?;
debug!("TCP connection to {} established", self.addr);
self.stream = Some(stream);
Ok(())
}
async fn disconnect(&mut self) -> Result<()> {
if let Some(stream) = self.stream.take() {
debug!("Closing TCP connection {}", self.addr);
drop(stream);
info!("TCP connection {} closed", self.addr);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::Error;
use std::pin::Pin;
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
use tokio::io::{AsyncReadExt, AsyncWriteExt, ReadBuf};
use tokio::net::TcpListener;
// ── Helper: noop waker for poll tests ─────────────────────────────
fn noop_waker() -> Waker {
unsafe fn noop_raw_clone(data: *const ()) -> RawWaker {
RawWaker::new(data, &NOOP_VTABLE)
}
unsafe fn noop_raw_wake(_data: *const ()) {}
unsafe fn noop_raw_wake_by_ref(_data: *const ()) {}
unsafe fn noop_raw_drop(_data: *const ()) {}
static NOOP_VTABLE: RawWakerVTable = RawWakerVTable::new(
noop_raw_clone,
noop_raw_wake,
noop_raw_wake_by_ref,
noop_raw_drop,
);
unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &NOOP_VTABLE)) }
}
// ── Sync accessor tests ───────────────────────────────────────────
#[test]
fn test_new() {
let transport = TcpTransport::new("127.0.0.1:8080".into());
assert_eq!(transport.addr(), "127.0.0.1:8080");
}
#[test]
fn test_name() {
let transport = TcpTransport::new("127.0.0.1:8080".into());
assert_eq!(transport.name(), transport.addr());
assert_eq!(transport.name(), "127.0.0.1:8080");
}
#[test]
fn test_transport_type() {
let transport = TcpTransport::new("127.0.0.1:8080".into());
assert_eq!(transport.transport_type(), TransportType::Tcp);
}
#[test]
fn test_is_connected_false_by_default() {
let transport = TcpTransport::new("127.0.0.1:8080".into());
assert!(!transport.is_connected());
}
// ── Poll error-state tests (no real connection) ───────────────────
#[test]
fn test_poll_read_not_connected() {
let mut transport = TcpTransport::new("127.0.0.1:8080".into());
let pinned = Pin::new(&mut transport);
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
let mut buf_data = [0u8; 16];
let mut buf = ReadBuf::new(&mut buf_data);
match pinned.poll_read(&mut cx, &mut buf) {
Poll::Ready(Err(e)) => assert_eq!(e.kind(), std::io::ErrorKind::NotConnected),
other => panic!("expected Poll::Ready(Err(NotConnected)), got {:?}", other),
}
}
#[test]
fn test_poll_write_not_connected() {
let mut transport = TcpTransport::new("127.0.0.1:8080".into());
let pinned = Pin::new(&mut transport);
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
match pinned.poll_write(&mut cx, b"hello") {
Poll::Ready(Err(e)) => assert_eq!(e.kind(), std::io::ErrorKind::NotConnected),
other => panic!("expected Poll::Ready(Err(NotConnected)), got {:?}", other),
}
}
#[test]
fn test_poll_flush_not_connected() {
let mut transport = TcpTransport::new("127.0.0.1:8080".into());
let pinned = Pin::new(&mut transport);
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
match pinned.poll_flush(&mut cx) {
Poll::Ready(Ok(())) => {} // expected
other => panic!("expected Poll::Ready(Ok(())), got {:?}", other),
}
}
#[test]
fn test_poll_shutdown_not_connected() {
let mut transport = TcpTransport::new("127.0.0.1:8080".into());
let pinned = Pin::new(&mut transport);
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
match pinned.poll_shutdown(&mut cx) {
Poll::Ready(Ok(())) => {} // expected
other => panic!("expected Poll::Ready(Ok(())), got {:?}", other),
}
}
// ── Async connect/disconnect tests ────────────────────────────────
#[tokio::test]
async fn test_connect_and_disconnect() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap().to_string();
let mut transport = TcpTransport::new(addr);
assert!(!transport.is_connected());
transport.connect().await.unwrap();
assert!(transport.is_connected());
transport.disconnect().await.unwrap();
assert!(!transport.is_connected());
}
#[tokio::test]
async fn test_double_connect_is_noop() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap().to_string();
let mut transport = TcpTransport::new(addr);
transport.connect().await.unwrap();
assert!(transport.is_connected());
// Second connect should be a safe no-op
let result = transport.connect().await;
assert!(result.is_ok(), "double connect should return Ok");
assert!(transport.is_connected());
}
#[tokio::test]
async fn test_connect_to_unreachable_addr() {
let mut transport = TcpTransport::new("127.0.0.1:1".into());
let result = transport.connect().await;
assert!(result.is_err());
match result.unwrap_err() {
Error::ConnectionFailed(_) => {} // expected
other => panic!("expected ConnectionFailed, got {:?}", other),
}
assert!(!transport.is_connected());
}
#[tokio::test]
async fn test_disconnect_without_connect() {
let mut transport = TcpTransport::new("127.0.0.1:8080".into());
let result = transport.disconnect().await;
assert!(
result.is_ok(),
"disconnect without connect should be a safe no-op"
);
assert!(!transport.is_connected());
}
// ── Async read/write with real connection ─────────────────────────
#[tokio::test]
async fn test_async_read_write() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap().to_string();
// Server: accept one connection, write "hello", read 5 bytes back, verify "world"
let server_handle = tokio::spawn(async move {
let (mut stream, _) = listener.accept().await.unwrap();
let written = tokio::io::AsyncWriteExt::write_all(&mut stream, b"hello").await;
assert!(written.is_ok(), "server write_all hello failed");
let mut buf = [0u8; 5];
let read = tokio::io::AsyncReadExt::read_exact(&mut stream, &mut buf).await;
assert!(read.is_ok(), "server read_exact failed");
assert_eq!(&buf, b"world");
});
// Client: connect, read "hello", write "world", disconnect
let mut transport = TcpTransport::new(addr);
transport.connect().await.unwrap();
let mut read_buf = [0u8; 5];
AsyncReadExt::read_exact(&mut transport, &mut read_buf)
.await
.unwrap();
assert_eq!(&read_buf, b"hello");
AsyncWriteExt::write_all(&mut transport, b"world")
.await
.unwrap();
transport.disconnect().await.unwrap();
// Ensure server task completed without panic
server_handle.await.unwrap();
}
}

View File

@@ -0,0 +1,426 @@
use std::pin::Pin;
use std::task::{Context, Poll};
use async_trait::async_trait;
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use tokio::net::UdpSocket;
use tracing::{debug, info};
use super::{Transport, TransportType};
use crate::error::Result;
#[derive(Debug)]
pub struct UdpTransport {
socket: Option<UdpSocket>,
bind_addr: String,
remote_addr: Option<String>,
read_buf: Vec<u8>,
read_pos: usize,
temp_recv_buf: Vec<u8>,
}
impl UdpTransport {
pub fn new(bind_addr: String, remote_addr: Option<String>) -> Self {
Self {
socket: None,
bind_addr,
remote_addr,
read_buf: Vec::new(),
read_pos: 0,
temp_recv_buf: vec![0u8; 65536], // 初始化一次,重复使用
}
}
pub fn bind_addr(&self) -> &str {
&self.bind_addr
}
}
impl AsyncRead for UdpTransport {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
let this = self.get_mut();
// 优先处理遗留的内部缓冲数据
if this.read_pos < this.read_buf.len() {
let remaining = &this.read_buf[this.read_pos..];
let to_copy = std::cmp::min(remaining.len(), buf.remaining());
buf.put_slice(&remaining[..to_copy]);
this.read_pos += to_copy;
// 如果缓冲区数据已被全部读完,清空它以便后续复用,避免无止境增长
if this.read_pos == this.read_buf.len() {
this.read_buf.clear();
this.read_pos = 0;
}
return Poll::Ready(Ok(()));
}
// 如果内部缓冲为空,尝试从 Socket 读取新的 UDP 包
let socket = match this.socket.as_ref() {
Some(s) => s,
None => {
return Poll::Ready(Err(std::io::Error::new(
std::io::ErrorKind::NotConnected,
"UDP socket is not connected",
)));
}
};
// 使用复用的 temp_recv_buf包裹为 tokio 需要的 ReadBuf
let mut temp_buf = ReadBuf::new(&mut this.temp_recv_buf);
// 如果配置了 remote_addr说明 socket 已经 connect 过,可以用 poll_recv
// 否则只能用 poll_recv_from丢弃掉远端地址信息
let poll_result = if this.remote_addr.is_some() {
socket.poll_recv(cx, &mut temp_buf)
} else {
// poll_recv_from 会返回 (usize, SocketAddr),我们将其映射回统一的 () 类型
socket
.poll_recv_from(cx, &mut temp_buf)
.map(|res| res.map(|_| ()))
};
match poll_result {
Poll::Ready(Ok(())) => {
let filled = temp_buf.filled();
let to_copy = std::cmp::min(filled.len(), buf.remaining());
buf.put_slice(&filled[..to_copy]);
// 核心逻辑:如果本次读取到的 UDP 包大于外面提供的 buf 的剩余空间
// 将没装下的剩余部分放进内部的 read_buf 留作下次 poll_read 使用
if to_copy < filled.len() {
this.read_buf.clear(); // 确保安全清空
this.read_buf.extend_from_slice(&filled[to_copy..]);
this.read_pos = 0;
}
Poll::Ready(Ok(()))
}
Poll::Ready(Err(e)) => Poll::Ready(Err(e)),
Poll::Pending => Poll::Pending,
}
}
}
impl AsyncWrite for UdpTransport {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<std::io::Result<usize>> {
let this = self.get_mut();
let socket = match this.socket.as_ref() {
Some(s) => s,
None => {
return Poll::Ready(Err(std::io::Error::new(
std::io::ErrorKind::NotConnected,
"UDP socket is not connected",
)));
}
};
// AsyncWrite 是不带目标地址的流式写入接口。
// 因此必须绑定了远端地址 (remote_addr) 才能确切知道把 UDP 包发送给谁。
if this.remote_addr.is_some() {
socket.poll_send(cx, buf)
} else {
Poll::Ready(Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Remote address must be set to use AsyncWrite for UDP",
)))
}
}
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
// UDP 是无连接的数据报协议,没有缓冲区需要手动 flush
Poll::Ready(Ok(()))
}
fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
// UDP 没有 TCP 的四次挥手过程,直接 Ready
Poll::Ready(Ok(()))
}
}
#[async_trait]
impl Transport for UdpTransport {
fn name(&self) -> &str {
&self.bind_addr
}
fn transport_type(&self) -> TransportType {
TransportType::Udp
}
fn is_connected(&self) -> bool {
self.socket.is_some()
}
async fn connect(&mut self) -> Result<()> {
if self.is_connected() {
return Ok(());
}
info!("Binding UDP socket to {}", self.bind_addr);
let socket = UdpSocket::bind(&self.bind_addr).await.map_err(|e| {
crate::error::Error::ConnectionFailed(format!(
"Failed to bind UDP {}: {}",
self.bind_addr, e
))
})?;
if let Some(ref remote) = self.remote_addr {
socket.connect(remote).await.map_err(|e| {
crate::error::Error::ConnectionFailed(format!(
"Failed to connect UDP to {}: {}",
remote, e
))
})?;
debug!("UDP connected to {}", remote);
}
debug!("UDP socket bound to {}", self.bind_addr);
self.socket = Some(socket);
Ok(())
}
async fn disconnect(&mut self) -> Result<()> {
if let Some(socket) = self.socket.take() {
debug!("Closing UDP socket {}", self.bind_addr);
drop(socket);
info!("UDP socket {} closed", self.bind_addr);
}
self.read_buf.clear();
self.read_pos = 0;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::Error;
use std::sync::LazyLock;
use std::task::{RawWaker, RawWakerVTable, Waker};
use tokio::io::{AsyncReadExt, AsyncWriteExt, ReadBuf};
use tokio::net::UdpSocket;
static NOOP_VTABLE: RawWakerVTable = RawWakerVTable::new(
|_| RawWaker::new(std::ptr::null(), &NOOP_VTABLE),
|_| {},
|_| {},
|_| {},
);
fn noop_waker() -> Waker {
unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &NOOP_VTABLE)) }
}
static NOOP_WAKER: LazyLock<Waker> = LazyLock::new(noop_waker);
fn noop_context() -> Context<'static> {
Context::from_waker(&NOOP_WAKER)
}
#[test]
fn test_new_with_remote() {
let t = UdpTransport::new("127.0.0.1:9000".into(), Some("127.0.0.1:9001".into()));
assert_eq!(t.bind_addr(), "127.0.0.1:9000");
}
#[test]
fn test_new_without_remote() {
let t = UdpTransport::new("0.0.0.0:0".into(), None);
assert_eq!(t.bind_addr(), "0.0.0.0:0");
}
#[test]
fn test_name() {
let t = UdpTransport::new("192.168.1.1:8888".into(), None);
assert_eq!(t.name(), "192.168.1.1:8888");
}
#[test]
fn test_transport_type() {
let t = UdpTransport::new("127.0.0.1:0".into(), None);
assert_eq!(t.transport_type(), TransportType::Udp);
}
#[test]
fn test_is_connected_false_by_default() {
let t = UdpTransport::new("127.0.0.1:0".into(), None);
assert!(!t.is_connected());
}
#[test]
fn test_poll_read_not_connected() {
let mut t = UdpTransport::new("127.0.0.1:0".into(), None);
let mut buf_data = [0u8; 64];
let mut buf = ReadBuf::new(&mut buf_data);
let mut cx = noop_context();
let result = Pin::new(&mut t).poll_read(&mut cx, &mut buf);
match result {
Poll::Ready(Err(ref e)) => assert_eq!(e.kind(), std::io::ErrorKind::NotConnected),
other => panic!("expected Poll::Ready(Err(NotConnected)), got {other:?}"),
}
}
#[test]
fn test_poll_write_not_connected() {
let mut t = UdpTransport::new("127.0.0.1:0".into(), None);
let mut cx = noop_context();
let data = b"test";
let result = Pin::new(&mut t).poll_write(&mut cx, data);
match result {
Poll::Ready(Err(ref e)) => assert_eq!(e.kind(), std::io::ErrorKind::NotConnected),
other => panic!("expected Poll::Ready(Err(NotConnected)), got {other:?}"),
}
}
#[test]
fn test_poll_flush_not_connected() {
let mut t = UdpTransport::new("127.0.0.1:0".into(), None);
let mut cx = noop_context();
let result = Pin::new(&mut t).poll_flush(&mut cx);
assert!(matches!(result, Poll::Ready(Ok(()))));
}
#[test]
fn test_poll_shutdown_not_connected() {
let mut t = UdpTransport::new("127.0.0.1:0".into(), None);
let mut cx = noop_context();
let result = Pin::new(&mut t).poll_shutdown(&mut cx);
assert!(matches!(result, Poll::Ready(Ok(()))));
}
#[tokio::test]
async fn test_connect_and_disconnect_without_remote() {
let mut t = UdpTransport::new("127.0.0.1:0".into(), None);
assert!(!t.is_connected());
t.connect().await.expect("connect should succeed");
assert!(t.is_connected());
t.disconnect().await.expect("disconnect should succeed");
assert!(!t.is_connected());
}
#[tokio::test]
async fn test_connect_and_disconnect_with_remote() {
let remote = UdpSocket::bind("127.0.0.1:0").await.expect("remote bind");
let remote_addr = remote.local_addr().unwrap().to_string();
let mut t = UdpTransport::new("127.0.0.1:0".into(), Some(remote_addr));
t.connect()
.await
.expect("connect with remote should succeed");
assert!(t.is_connected());
t.disconnect().await.expect("disconnect should succeed");
assert!(!t.is_connected());
}
#[tokio::test]
async fn test_double_connect_is_noop() {
let mut t = UdpTransport::new("127.0.0.1:0".into(), None);
t.connect().await.expect("first connect");
assert!(t.is_connected());
t.connect().await.expect("second connect (idempotent)");
assert!(t.is_connected());
}
#[tokio::test]
async fn test_connect_bind_failure() {
let mut t = UdpTransport::new("invalid_addr".into(), None);
let result = t.connect().await;
match result {
Err(Error::ConnectionFailed(_)) => {}
other => panic!("expected ConnectionFailed error, got {other:?}"),
}
}
#[tokio::test]
async fn test_async_read_write_with_remote() {
let remote = UdpSocket::bind("127.0.0.1:0").await.expect("remote bind");
let remote_addr = remote.local_addr().unwrap().to_string();
let mut t = UdpTransport::new("127.0.0.1:0".into(), Some(remote_addr));
t.connect().await.expect("connect");
AsyncWriteExt::write_all(&mut t, b"hello")
.await
.expect("write_all hello");
let mut buf = [0u8; 1024];
let (n, transport_addr) = remote.recv_from(&mut buf).await.expect("remote recv_from");
assert_eq!(&buf[..n], b"hello");
remote
.send_to(b"world", transport_addr)
.await
.expect("remote send_to");
let mut read_buf = [0u8; 5];
AsyncReadExt::read_exact(&mut t, &mut read_buf)
.await
.expect("read_exact world");
assert_eq!(&read_buf, b"world");
}
#[tokio::test]
async fn test_write_fails_without_remote() {
let mut t = UdpTransport::new("127.0.0.1:0".into(), None);
t.connect().await.expect("connect (binds but no remote)");
let result = AsyncWriteExt::write_all(&mut t, b"test").await;
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
}
#[tokio::test]
async fn test_oversized_datagram_buffering() {
let remote = UdpSocket::bind("127.0.0.1:0").await.expect("remote bind");
let remote_addr = remote.local_addr().unwrap().to_string();
let mut t = UdpTransport::new("127.0.0.1:0".into(), Some(remote_addr));
t.connect().await.expect("connect");
AsyncWriteExt::write_all(&mut t, b"x")
.await
.expect("write ping");
let mut ping_buf = [0u8; 1];
let (_, transport_addr) = remote
.recv_from(&mut ping_buf)
.await
.expect("remote recv ping");
let large_data: Vec<u8> = vec![b'A'; 100];
remote
.send_to(&large_data, transport_addr)
.await
.expect("send large datagram");
let mut small_buf = [0u8; 10];
AsyncReadExt::read_exact(&mut t, &mut small_buf)
.await
.expect("read 10 bytes");
assert_eq!(&small_buf, b"AAAAAAAAAA");
let mut rest_buf = [0u8; 90];
AsyncReadExt::read_exact(&mut t, &mut rest_buf)
.await
.expect("read 90 bytes");
assert_eq!(&rest_buf[..], &vec![b'A'; 90][..]);
}
}