Add mixed text/plot transport and improve plot UI

This commit is contained in:
2026-06-09 00:51:34 +08:00
parent 25433038d5
commit d5b6bb52c8
18 changed files with 2101 additions and 192 deletions

View File

@@ -4,7 +4,9 @@ use tokio::io::{AsyncRead, AsyncWrite};
use tracing::info;
use crate::error::Result;
use crate::transport::serial::SerialTransport;
use crate::transport::serial::{
SerialDataBits, SerialFlowControl, SerialParity, SerialStopBits, SerialTransport,
};
use crate::transport::tcp::TcpTransport;
use crate::transport::udp::UdpTransport;
@@ -12,6 +14,22 @@ 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
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TransportType {
Serial,
@@ -34,6 +52,14 @@ 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,
},
Tcp {
addr: String,
@@ -63,9 +89,21 @@ pub enum Connection {
impl Connection {
pub fn new(config: TransportConfig) -> Self {
match config {
TransportConfig::Serial { port, baud_rate } => {
Connection::Serial(SerialTransport::new(port, baud_rate))
}
TransportConfig::Serial {
port,
baud_rate,
data_bits,
parity,
stop_bits,
flow_control,
} => Connection::Serial(SerialTransport::new(
port,
baud_rate,
data_bits,
parity,
stop_bits,
flow_control,
)),
TransportConfig::Tcp { addr } => Connection::Tcp(TcpTransport::new(addr)),
TransportConfig::Udp {
bind_addr,
@@ -170,6 +208,20 @@ impl AsyncWrite for Connection {
#[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,
}
}
#[test]
fn test_transport_type_display() {
@@ -256,6 +308,10 @@ mod tests {
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,
};
let _ = format!("{:?}", cfg);
@@ -273,10 +329,7 @@ mod tests {
#[test]
fn test_transport_config_clone() {
let original = TransportConfig::Serial {
port: "COM1".into(),
baud_rate: 115200,
};
let original = serial_config("COM1", 115200);
let cloned = original.clone();
assert_eq!(format!("{:?}", original), format!("{:?}", cloned));
@@ -296,13 +349,14 @@ mod tests {
#[test]
fn test_transport_config_serialize_serial() {
let cfg = TransportConfig::Serial {
port: "COM1".into(),
baud_rate: 115200,
};
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]
@@ -310,9 +364,44 @@ mod tests {
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 } => {
TransportConfig::Serial {
port,
baud_rate,
data_bits,
parity,
stop_bits,
flow_control,
} => {
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);
}
_ => 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,
} => {
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);
}
_ => panic!("expected Serial variant"),
}
@@ -393,10 +482,7 @@ mod tests {
#[test]
fn test_transport_config_roundtrip() {
let configs = vec![
TransportConfig::Serial {
port: "COM3".into(),
baud_rate: 57600,
},
serial_config("COM3", 57600),
TransportConfig::Tcp {
addr: "10.0.0.1:9999".into(),
},
@@ -425,10 +511,7 @@ mod tests {
#[test]
fn test_connection_new_serial() {
let conn = Connection::new(TransportConfig::Serial {
port: "COM1".into(),
baud_rate: 115200,
});
let conn = Connection::new(serial_config("COM1", 115200));
assert_eq!(conn.transport_type(), TransportType::Serial);
assert_eq!(conn.name(), "COM1");
assert!(!conn.is_connected());
@@ -468,10 +551,7 @@ mod tests {
#[test]
fn test_connection_is_connected_default() {
let serial = Connection::new(TransportConfig::Serial {
port: "COM1".into(),
baud_rate: 9600,
});
let serial = Connection::new(serial_config("COM1", 9600));
let tcp = Connection::new(TransportConfig::Tcp {
addr: "127.0.0.1:8080".into(),
});
@@ -486,10 +566,7 @@ mod tests {
#[test]
fn test_connection_debug() {
let serial = Connection::new(TransportConfig::Serial {
port: "COM1".into(),
baud_rate: 115200,
});
let serial = Connection::new(serial_config("COM1", 115200));
let tcp = Connection::new(TransportConfig::Tcp {
addr: "127.0.0.1:8080".into(),
});
@@ -515,10 +592,7 @@ mod tests {
}
fn serial_conn() -> Connection {
Connection::new(TransportConfig::Serial {
port: "COM1".into(),
baud_rate: 115200,
})
Connection::new(serial_config("COM1", 115200))
}
fn tcp_conn() -> Connection {

View File

@@ -1,24 +1,107 @@
use async_trait::async_trait;
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use tokio_serial::{SerialPortBuilderExt, SerialStream};
use tokio_serial::{DataBits, FlowControl, Parity, SerialPortBuilderExt, SerialStream, StopBits};
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,
}
impl SerialTransport {
pub fn new(port_name: String, baud_rate: u32) -> Self {
pub fn new(
port_name: String,
baud_rate: u32,
data_bits: SerialDataBits,
parity: SerialParity,
stop_bits: SerialStopBits,
flow_control: SerialFlowControl,
) -> Self {
Self {
port: None,
port_name,
baud_rate,
data_bits,
parity,
stop_bits,
flow_control,
}
}
@@ -39,6 +122,22 @@ impl SerialTransport {
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
}
}
impl AsyncRead for SerialTransport {
@@ -113,11 +212,20 @@ impl Transport for SerialTransport {
}
info!(
"Opening serial port {} at {} baud",
self.port_name, self.baud_rate
"Opening serial port {} at {} baud ({:?}, {:?}, {:?}, {:?})",
self.port_name,
self.baud_rate,
self.data_bits,
self.parity,
self.stop_bits,
self.flow_control
);
let 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!(
@@ -167,33 +275,72 @@ mod tests {
#[test]
fn test_new() {
let transport = SerialTransport::new("COM1".into(), 115200);
let transport = SerialTransport::new(
"COM1".into(),
115200,
SerialDataBits::Eight,
SerialParity::None,
SerialStopBits::One,
SerialFlowControl::None,
);
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);
let transport = SerialTransport::new(
"COM3".into(),
9600,
SerialDataBits::Seven,
SerialParity::Even,
SerialStopBits::Two,
SerialFlowControl::Hardware,
);
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);
let transport = SerialTransport::new(
"COM1".into(),
115200,
SerialDataBits::Eight,
SerialParity::None,
SerialStopBits::One,
SerialFlowControl::None,
);
assert_eq!(transport.name(), "COM1");
}
#[test]
fn test_transport_type() {
let transport = SerialTransport::new("COM1".into(), 115200);
let transport = SerialTransport::new(
"COM1".into(),
115200,
SerialDataBits::Eight,
SerialParity::None,
SerialStopBits::One,
SerialFlowControl::None,
);
assert_eq!(transport.transport_type(), TransportType::Serial);
}
#[test]
fn test_is_connected_false_by_default() {
let transport = SerialTransport::new("COM1".into(), 115200);
let transport = SerialTransport::new(
"COM1".into(),
115200,
SerialDataBits::Eight,
SerialParity::None,
SerialStopBits::One,
SerialFlowControl::None,
);
assert!(!transport.is_connected());
}
@@ -214,7 +361,14 @@ mod tests {
#[tokio::test]
async fn test_poll_read_not_connected() {
let mut transport = SerialTransport::new("COM1".into(), 115200);
let mut transport = SerialTransport::new(
"COM1".into(),
115200,
SerialDataBits::Eight,
SerialParity::None,
SerialStopBits::One,
SerialFlowControl::None,
);
let pinned = Pin::new(&mut transport);
let mut buf_data = [0u8; 16];
let mut buf = ReadBuf::new(&mut buf_data);
@@ -230,7 +384,14 @@ mod tests {
#[tokio::test]
async fn test_poll_write_not_connected() {
let mut transport = SerialTransport::new("COM1".into(), 115200);
let mut transport = SerialTransport::new(
"COM1".into(),
115200,
SerialDataBits::Eight,
SerialParity::None,
SerialStopBits::One,
SerialFlowControl::None,
);
let pinned = Pin::new(&mut transport);
let data = b"hello";
let waker = noop_waker();
@@ -243,7 +404,14 @@ mod tests {
#[tokio::test]
async fn test_poll_flush_not_connected() {
let mut transport = SerialTransport::new("COM1".into(), 115200);
let mut transport = SerialTransport::new(
"COM1".into(),
115200,
SerialDataBits::Eight,
SerialParity::None,
SerialStopBits::One,
SerialFlowControl::None,
);
let pinned = Pin::new(&mut transport);
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
@@ -255,7 +423,14 @@ mod tests {
#[tokio::test]
async fn test_poll_shutdown_not_connected() {
let mut transport = SerialTransport::new("COM1".into(), 115200);
let mut transport = SerialTransport::new(
"COM1".into(),
115200,
SerialDataBits::Eight,
SerialParity::None,
SerialStopBits::One,
SerialFlowControl::None,
);
let pinned = Pin::new(&mut transport);
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
@@ -269,19 +444,40 @@ mod tests {
#[test]
fn test_transport_name() {
let transport = SerialTransport::new("COM1".into(), 115200);
let transport = SerialTransport::new(
"COM1".into(),
115200,
SerialDataBits::Eight,
SerialParity::None,
SerialStopBits::One,
SerialFlowControl::None,
);
assert_eq!(transport.name(), "COM1");
}
#[test]
fn test_transport_transport_type() {
let transport = SerialTransport::new("COM1".into(), 115200);
let transport = SerialTransport::new(
"COM1".into(),
115200,
SerialDataBits::Eight,
SerialParity::None,
SerialStopBits::One,
SerialFlowControl::None,
);
assert_eq!(transport.transport_type(), TransportType::Serial);
}
#[test]
fn test_transport_is_connected() {
let transport = SerialTransport::new("COM1".into(), 115200);
let transport = SerialTransport::new(
"COM1".into(),
115200,
SerialDataBits::Eight,
SerialParity::None,
SerialStopBits::One,
SerialFlowControl::None,
);
assert!(!transport.is_connected());
}
}