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:
2026-06-11 14:18:39 +08:00
parent 09bee86a08
commit 1fec5f8941
11 changed files with 1154 additions and 20 deletions

View File

@@ -64,6 +64,8 @@ pub struct SessionTab {
pub plot_view: plot_view::PlotViewState,
pub view: View,
pub auto_reconnect: bool,
pub dtr: bool,
pub rts: bool,
pub send_input: String,
pub send_mode: SendMode,
pub append_newline: bool,
@@ -183,6 +185,10 @@ impl XserialApp {
fn add_session_tab(&mut self, session_config: SessionConfig) {
let history_limit = session_config.history_limit;
let auto_reconnect = session_config.auto_reconnect;
let (dtr, rts) = match &session_config.transport {
TransportConfig::Serial { dtr, rts, .. } => (*dtr, *rts),
_ => (false, false),
};
let handle = self.manager.create(session_config.clone());
self.tabs.push(SessionTab {
id: handle.id(),
@@ -194,6 +200,8 @@ impl XserialApp {
plot_view: plot_view::PlotViewState::default(),
view: View::Text,
auto_reconnect,
dtr,
rts,
send_input: String::new(),
send_mode: SendMode::Text,
append_newline: true,
@@ -920,6 +928,22 @@ fn render_session_controls(
tab.status = ConnectionStatus::Error(String::from("session not found"));
}
}
if matches!(tab.status, ConnectionStatus::Connected) {
let dtr_changed = ui.checkbox(&mut tab.dtr, "DTR").changed();
let rts_changed = ui.checkbox(&mut tab.rts, "RTS").changed();
if dtr_changed || rts_changed {
if let Some(handle) = manager.get(tab.id) {
let dtr = tab.dtr;
let rts = tab.rts;
tokio::spawn(async move {
if dtr_changed { let _ = handle.set_dtr(dtr).await; }
if rts_changed { let _ = handle.set_rts(rts).await; }
});
} else {
tab.status = ConnectionStatus::Error(String::from("session not found"));
}
}
}
});
auto_reconnect_changed
}
@@ -1062,6 +1086,8 @@ mod tests {
parity: SerialParity::None,
stop_bits: SerialStopBits::One,
flow_control: SerialFlowControl::None,
dtr: false,
rts: false,
}),
"Serial COM7"
);

View File

@@ -17,6 +17,8 @@ pub struct ConfigForm {
pub serial_parity: SerialParity,
pub serial_stop_bits: SerialStopBits,
pub serial_flow_control: SerialFlowControl,
pub serial_dtr: bool,
pub serial_rts: bool,
pub addr: String,
pub udp_bind: String,
pub udp_remote: String,
@@ -116,6 +118,8 @@ impl Default for ConfigForm {
serial_parity: SerialParity::None,
serial_stop_bits: SerialStopBits::One,
serial_flow_control: SerialFlowControl::None,
serial_dtr: false,
serial_rts: false,
addr: String::from("127.0.0.1:8080"),
udp_bind: String::from("0.0.0.0:9000"),
udp_remote: String::new(),
@@ -158,6 +162,14 @@ impl ConfigForm {
TransportConfig::Serial { flow_control, .. } => *flow_control,
_ => SerialFlowControl::None,
},
serial_dtr: match &config.transport {
TransportConfig::Serial { dtr, .. } => *dtr,
_ => false,
},
serial_rts: match &config.transport {
TransportConfig::Serial { rts, .. } => *rts,
_ => false,
},
addr: match &config.transport {
TransportConfig::Tcp { addr } => addr.clone(),
_ => String::from("127.0.0.1:8080"),
@@ -190,6 +202,8 @@ impl ConfigForm {
parity: self.serial_parity,
stop_bits: self.serial_stop_bits,
flow_control: self.serial_flow_control,
dtr: self.serial_dtr,
rts: self.serial_rts,
},
TransportChoice::Tcp => TransportConfig::Tcp {
addr: self.addr.clone(),
@@ -484,6 +498,8 @@ fn render_transport_section(ui: &mut Ui, form: &mut ConfigForm) {
);
});
});
ui.checkbox(&mut form.serial_dtr, "DTR");
ui.checkbox(&mut form.serial_rts, "RTS");
}
TransportChoice::Tcp => {
ui.horizontal(|ui| {