feat: Add DTR/RTS control for serial ports with GUI toggles
- Assert DTR/RTS after serial port open to prevent wireless modules (HC-12/HC-15/Bluetooth bridges) from entering AT command mode after the unavoidable Linux kernel DTR pulse on every open() call. - Add dtr/rts fields to TransportConfig::Serial (default: false). - Add set_dtr()/set_rts() on SerialTransport, Connection, SessionHandle. - Add SessionCmd::SetDtr/SetRts for live toggling via command channel. - Add DTR/RTS checkboxes to GUI session controls (visible when connected) and to config form for persistence. - Add test_plot_serial.c and test_text_serial.c — standalone C test tools for sending MixedTextPlot frames and plain text over serial. - Update AGENTS.md with build prerequisites, architecture map, testing conventions, env vars, and known quirks.
This commit is contained in:
@@ -30,6 +30,14 @@ 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,
|
||||
@@ -60,6 +68,10 @@ pub enum TransportConfig {
|
||||
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,
|
||||
@@ -96,6 +108,8 @@ impl Connection {
|
||||
parity,
|
||||
stop_bits,
|
||||
flow_control,
|
||||
dtr,
|
||||
rts,
|
||||
} => Connection::Serial(SerialTransport::new(
|
||||
port,
|
||||
baud_rate,
|
||||
@@ -103,6 +117,8 @@ impl Connection {
|
||||
parity,
|
||||
stop_bits,
|
||||
flow_control,
|
||||
dtr,
|
||||
rts,
|
||||
)),
|
||||
TransportConfig::Tcp { addr } => Connection::Tcp(TcpTransport::new(addr)),
|
||||
TransportConfig::Udp {
|
||||
@@ -153,6 +169,28 @@ impl Connection {
|
||||
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 {
|
||||
@@ -220,6 +258,8 @@ mod tests {
|
||||
parity: SerialParity::None,
|
||||
stop_bits: SerialStopBits::One,
|
||||
flow_control: SerialFlowControl::None,
|
||||
dtr: false,
|
||||
rts: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,6 +352,8 @@ mod tests {
|
||||
parity: SerialParity::None,
|
||||
stop_bits: SerialStopBits::One,
|
||||
flow_control: SerialFlowControl::None,
|
||||
dtr: false,
|
||||
rts: false,
|
||||
};
|
||||
let _ = format!("{:?}", cfg);
|
||||
|
||||
@@ -371,6 +413,8 @@ mod tests {
|
||||
parity,
|
||||
stop_bits,
|
||||
flow_control,
|
||||
dtr,
|
||||
rts,
|
||||
} => {
|
||||
assert_eq!(port, "COM1");
|
||||
assert_eq!(baud_rate, 9600);
|
||||
@@ -378,6 +422,8 @@ mod tests {
|
||||
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"),
|
||||
}
|
||||
@@ -395,6 +441,8 @@ mod tests {
|
||||
parity,
|
||||
stop_bits,
|
||||
flow_control,
|
||||
dtr,
|
||||
rts,
|
||||
} => {
|
||||
assert_eq!(port, "COM2");
|
||||
assert_eq!(baud_rate, 57600);
|
||||
@@ -402,6 +450,8 @@ mod tests {
|
||||
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"),
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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};
|
||||
@@ -83,9 +84,12 @@ pub struct SerialTransport {
|
||||
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,
|
||||
@@ -93,6 +97,8 @@ impl SerialTransport {
|
||||
parity: SerialParity,
|
||||
stop_bits: SerialStopBits,
|
||||
flow_control: SerialFlowControl,
|
||||
dtr: bool,
|
||||
rts: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
port: None,
|
||||
@@ -102,6 +108,8 @@ impl SerialTransport {
|
||||
parity,
|
||||
stop_bits,
|
||||
flow_control,
|
||||
dtr,
|
||||
rts,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,6 +146,36 @@ impl SerialTransport {
|
||||
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 {
|
||||
@@ -221,7 +259,7 @@ impl Transport for SerialTransport {
|
||||
self.flow_control
|
||||
);
|
||||
|
||||
let port = tokio_serial::new(&self.port_name, self.baud_rate)
|
||||
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())
|
||||
@@ -234,6 +272,17 @@ impl Transport for SerialTransport {
|
||||
))
|
||||
})?;
|
||||
|
||||
// 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(())
|
||||
@@ -282,6 +331,8 @@ mod tests {
|
||||
SerialParity::None,
|
||||
SerialStopBits::One,
|
||||
SerialFlowControl::None,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
assert_eq!(transport.port_name(), "COM1");
|
||||
assert_eq!(transport.baud_rate(), 115200);
|
||||
@@ -296,6 +347,8 @@ mod tests {
|
||||
SerialParity::Even,
|
||||
SerialStopBits::Two,
|
||||
SerialFlowControl::Hardware,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
assert_eq!(transport.port_name(), "COM3");
|
||||
assert_eq!(transport.baud_rate(), 9600);
|
||||
@@ -314,6 +367,8 @@ mod tests {
|
||||
SerialParity::None,
|
||||
SerialStopBits::One,
|
||||
SerialFlowControl::None,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
assert_eq!(transport.name(), "COM1");
|
||||
}
|
||||
@@ -327,6 +382,8 @@ mod tests {
|
||||
SerialParity::None,
|
||||
SerialStopBits::One,
|
||||
SerialFlowControl::None,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
assert_eq!(transport.transport_type(), TransportType::Serial);
|
||||
}
|
||||
@@ -340,6 +397,8 @@ mod tests {
|
||||
SerialParity::None,
|
||||
SerialStopBits::One,
|
||||
SerialFlowControl::None,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
assert!(!transport.is_connected());
|
||||
}
|
||||
@@ -368,6 +427,8 @@ mod tests {
|
||||
SerialParity::None,
|
||||
SerialStopBits::One,
|
||||
SerialFlowControl::None,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
let pinned = Pin::new(&mut transport);
|
||||
let mut buf_data = [0u8; 16];
|
||||
@@ -391,6 +452,8 @@ mod tests {
|
||||
SerialParity::None,
|
||||
SerialStopBits::One,
|
||||
SerialFlowControl::None,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
let pinned = Pin::new(&mut transport);
|
||||
let data = b"hello";
|
||||
@@ -411,6 +474,8 @@ mod tests {
|
||||
SerialParity::None,
|
||||
SerialStopBits::One,
|
||||
SerialFlowControl::None,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
let pinned = Pin::new(&mut transport);
|
||||
let waker = noop_waker();
|
||||
@@ -430,6 +495,8 @@ mod tests {
|
||||
SerialParity::None,
|
||||
SerialStopBits::One,
|
||||
SerialFlowControl::None,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
let pinned = Pin::new(&mut transport);
|
||||
let waker = noop_waker();
|
||||
@@ -451,6 +518,8 @@ mod tests {
|
||||
SerialParity::None,
|
||||
SerialStopBits::One,
|
||||
SerialFlowControl::None,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
assert_eq!(transport.name(), "COM1");
|
||||
}
|
||||
@@ -464,6 +533,8 @@ mod tests {
|
||||
SerialParity::None,
|
||||
SerialStopBits::One,
|
||||
SerialFlowControl::None,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
assert_eq!(transport.transport_type(), TransportType::Serial);
|
||||
}
|
||||
@@ -477,6 +548,8 @@ mod tests {
|
||||
SerialParity::None,
|
||||
SerialStopBits::One,
|
||||
SerialFlowControl::None,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
assert!(!transport.is_connected());
|
||||
}
|
||||
|
||||
@@ -688,6 +688,8 @@ async fn connection_transport_type_dispatch() {
|
||||
parity: SerialParity::None,
|
||||
stop_bits: SerialStopBits::One,
|
||||
flow_control: SerialFlowControl::None,
|
||||
dtr: false,
|
||||
rts: false,
|
||||
});
|
||||
let tcp = Connection::new(TransportConfig::Tcp {
|
||||
addr: "127.0.0.1:8080".into(),
|
||||
|
||||
Reference in New Issue
Block a user