From 1fec5f8941793b551c519625de02a7142fb346ec Mon Sep 17 00:00:00 2001 From: FallenSigh Date: Thu, 11 Jun 2026 14:18:39 +0800 Subject: [PATCH] feat: Add DTR/RTS control for serial ports with GUI toggles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- AGENTS.md | 144 +++++- Cargo.lock | 3 + crates/xserial-client/src/cmd.rs | 2 + crates/xserial-client/src/session.rs | 46 ++ crates/xserial-core/src/transport/mod.rs | 50 ++ crates/xserial-core/src/transport/serial.rs | 75 ++- crates/xserial-core/tests/pipeline.rs | 2 + crates/xserial-gui/src/app.rs | 26 + crates/xserial-gui/src/panels/config.rs | 16 + tools/test_plot_serial.c | 540 ++++++++++++++++++++ tools/test_text_serial.c | 270 ++++++++++ 11 files changed, 1154 insertions(+), 20 deletions(-) create mode 100644 tools/test_plot_serial.c create mode 100644 tools/test_text_serial.c diff --git a/AGENTS.md b/AGENTS.md index 591e01a..3953624 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,28 +1,134 @@ # Repository Guidelines -## Project Structure & Module Organization -`xserial` is a Rust workspace. The root [Cargo.toml](/D:/Dev/xserial/Cargo.toml) defines four crates in `crates/`: -- `xserial-core`: transport, framing, protocol, and pipeline primitives in `src/`, with integration tests in `tests/pipeline.rs`. -- `xserial-client`: session management, history, commands, and Lua bindings, with integration tests under `tests/`. -- `xserial-tui`: terminal app entrypoint in `src/main.rs`. -- `xserial-gui`: egui/eframe app, with UI panels in `src/panels/` and font helpers in `src/ui_fonts.rs`. +## Build Prerequisites -Use `tools/test_plot.py` as a local waveform source for GUI plot testing. Treat `target/` as generated output. +- **Linux**: `libudev-dev` (for `serialport`). +- **All platforms**: a C compiler (`cc`, `gcc`, `clang`, or MSVC) — `mlua` uses `features = ["vendored"]` which compiles LuaJIT from source at build time. +- **No pinned toolchain**: all crates use `edition = "2024"` (requires Rust ≥ 1.85). No `rust-toolchain.toml`; builds with whatever `rustc` is on `PATH`. + +## Project Structure + +`xserial` is a Rust workspace. The root `Cargo.toml` defines four crates in `crates/`, with dependencies flowing: + +``` +xserial-core (no internal deps — transport, framing, protocol, pipeline) + ↑ +xserial-client (depends on xserial-core — sessions, history, commands, Lua) + ↑ ↑ +xserial-gui xserial-tui +(both depend on xserial-core AND xserial-client) +``` + +| Crate | Type | What goes here | +|-------|------|---------------| +| `xserial-core` | library (`src/lib.rs`) | `transport/`, `frame/`, `protocol/`, `pipeline.rs` — protocol and I/O. Integration tests: `tests/pipeline.rs`. | +| `xserial-client` | library (`src/lib.rs`) | `session.rs`, `manager.rs`, `config.rs`, `history.rs`, `lua/` — state management and Lua bindings. Integration tests: `tests/session_lifecycle.rs`, `tests/lua_tests.rs`. | +| `xserial-gui` | binary (`src/main.rs`) | egui/eframe app. Panels in `src/panels/`. Font helpers in `src/ui_fonts.rs`. Profiling in `src/perf.rs`. | +| `xserial-tui` | binary (`src/main.rs`) | ratatui/crossterm app. **Still a placeholder** — zero tests, not at feature parity with GUI. | + +**Module boundaries**: transport/protocol → `xserial-core`. Session/Lua → `xserial-client`. UI state → `xserial-gui` or `xserial-tui`. Never put I/O logic in the UI crates, never put UI state in the client crate. ## Build, Test, and Development Commands -- `cargo check --workspace`: fast compile check for all crates. -- `cargo test --workspace`: run the workspace test suite, including `crates/xserial-core/tests` and `crates/xserial-client/tests`. -- `cargo run -p xserial-gui`: launch the desktop GUI. -- `cargo run -p xserial-tui`: launch the terminal UI. -- `cargo fmt --all`: apply standard Rust formatting. -- `cargo clippy --workspace --all-targets -- -D warnings`: catch lint issues before review. -- `python tools/test_plot.py --help`: inspect options for the TCP plot-frame generator. -## Coding Style & Naming Conventions -Follow standard Rust formatting: 4-space indentation, trailing commas in multiline literals, and `snake_case` for modules, files, functions, and tests. Use `PascalCase` for structs/enums and `SCREAMING_SNAKE_CASE` for constants. Keep crate boundaries clean: protocol/transport code belongs in `xserial-core`; session and Lua integration belong in `xserial-client`; UI state stays in `xserial-gui` or `xserial-tui`. +```bash +cargo check --workspace # fast compile check (all crates) +cargo test --workspace # full suite (~266 tests) +cargo fmt --all # standard Rust formatting +cargo clippy --workspace --all-targets -- -D warnings # lint gate +``` + +**Run specific tests:** + +```bash +cargo test -p xserial-core -- tcp_line_text_utf8 +cargo test -p xserial-client -- session_echo_send_and_read +cargo test -p xserial-core -- --nocapture # show test output + +# Integration tests only +cargo test -p xserial-core --test pipeline +cargo test -p xserial-client --test session_lifecycle +cargo test -p xserial-client --test lua_tests +``` + +**Launch:** + +```bash +cargo run -p xserial-gui +cargo run -p xserial-tui +``` + +## Environment & Configuration + +### Tracing (all entrypoints) + +```bash +RUST_LOG=info cargo run -p xserial-gui +RUST_LOG=xserial_gui::perf=debug cargo run -p xserial-gui +``` + +`tracing_subscriber::EnvFilter::from_default_env()` reads `RUST_LOG`. The workspace dep `tracing-subscriber` has `features = ["env-filter"]`. + +### GUI Profiling + +```bash +RUST_LOG=xserial_gui::perf=info XSERIAL_GUI_PROFILE=1 cargo run -p xserial-gui +XSERIAL_GUI_PROFILE_INTERVAL_MS=500 cargo run -p xserial-gui # custom interval (default 1s) +``` + +Profiling logs go to target `xserial_gui::perf`. + +### State & Config File Paths (runtime, not compile-time) + +Uses `env::consts::OS` + env vars — no `cfg(target_os)` gating for path resolution: + +| Platform | Root config path | +|----------|-----------------| +| Linux | `$XDG_CONFIG_HOME/xserial/` or `~/.config/xserial/` | +| macOS | `~/Library/Application Support/xserial/` | +| Windows | `%APPDATA%\xserial\` | + +State files: `gui-state.json`, `tui-state.json`, `font-settings.json`. + +### GUI Plot Testing + +```bash +python tools/test_plot.py --wire-format mixed --host 127.0.0.1 --port 8091 +python tools/test_plot.py --wire-format mixed --format xy --channels 2 +python tools/test_plot.py --wire-format raw --channels 2 --framelen 256 +``` + +Configure in GUI: Transport `TCP 127.0.0.1:8091`, Framer `MixedTextPlot`, Decoder `MixedTextPlot`. + +The wire format spec lives in `tools/xs_mixed_plot.h` (C header, reference only — not compiled into Rust). + +## Coding Style + +Standard Rust conventions (`cargo fmt`). No crate-level lint attrs, no `[lints]` table in any `Cargo.toml`. Names: `snake_case` for modules/files/functions/tests, `PascalCase` for types, `SCREAMING_SNAKE_CASE` for constants. ## Testing Guidelines -Prefer focused unit tests next to implementation and integration tests in `crates//tests/*.rs`. Name tests after behavior, for example `session_lifecycle` or `pipeline`. Run `cargo test --workspace` before opening a PR; add targeted regression tests for transport, framing, parsing, and session-state fixes. -## Commit & Pull Request Guidelines -Recent history uses short, imperative subjects such as `Persist GUI font settings` and `Improve session controls and GUI views`, with occasional `feat:` prefixes for major additions. Keep subjects concise and action-oriented. PRs should state which crate(s) changed, list validation commands, and include screenshots or short recordings for GUI/TUI changes. Call out protocol, transport, or Lua API compatibility impacts explicitly. +- **Inline unit tests** (`#[cfg(test)] mod tests`) next to implementation — 24 source files with `#[test]` or `#[tokio::test]`. +- **Integration tests** in `crates//tests/*.rs` — 3 files covering pipeline, session lifecycle, and Lua bindings. +- **~266 tests total** — all active (zero `#[ignore]`). Single `#[should_panic]` in `frame/fixed.rs`. +- **All tests are self-contained**: TCP/UDP bind to `127.0.0.1:0` (OS-assigned port). No hardcoded ports. No external services. Serial tests use dummy port names and never open real hardware. +- **Async tests** use `#[tokio::test]` (no `async-std`). **No benchmarks** exist. +- Run `cargo test --workspace` before opening PRs. Add targeted regression tests for transport, framing, parsing, and session-state fixes. + +## Known Quirks & Constraints + +- **No CI** — no `.github/workflows` or other CI config. Agents must self-verify with `cargo test --workspace` and `cargo clippy --workspace --all-targets -- -D warnings`. +- **No feature flags** — zero `[features]` in any `Cargo.toml`. No `#[cfg(feature)]` in any source. +- **No build scripts** — zero `build.rs` files. No codegen. No `include!`/`include_str!`/`include_bytes!`. +- **All `unsafe` is test-only** — constructing noop wakers for poll-based unit tests in `transport/`. No non-test `unsafe` anywhere. +- **`xserial-tui` is a stub** — no tests, minimal implementation. GUI is the primary target. +- **Platform font directories** use `#[cfg(target_os)]` in `ui_fonts.rs` (three blocks: Windows, macOS, Linux/BSD). +- **`tokio` features differ by build**: `xserial-client` uses `["sync", "time"]` for production, `["full"]` for dev-dependencies (tests need net/IO). + +## Commit & PR Guidelines + +Short, imperative subjects (`Persist GUI font settings`), occasional `feat:` prefix for major additions. PRs: state which crate(s) changed, list validation commands, include screenshots/recordings for GUI/TUI changes. Explicitly call out protocol, transport, or Lua API compatibility impacts. + +## Reference + +- `tools/test_plot.py --help` — TCP plot-frame generator options. +- `tools/xs_mixed_plot.h` — wire format spec for MixedTextPlot (COBS-framed binary plot + text on one stream). diff --git a/Cargo.lock b/Cargo.lock index 80104a3..f057efe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5806,7 +5806,10 @@ dependencies = [ "crossterm 0.28.1", "hex", "image", + "mlua", "ratatui", + "serde", + "serde_json", "tokio", "tracing", "tracing-appender", diff --git a/crates/xserial-client/src/cmd.rs b/crates/xserial-client/src/cmd.rs index e1b9a78..fef3595 100644 --- a/crates/xserial-client/src/cmd.rs +++ b/crates/xserial-client/src/cmd.rs @@ -8,4 +8,6 @@ pub enum SessionCmd { SetAutoReconnect(bool), Close, Reconfigure(SessionConfig), + SetDtr(bool), + SetRts(bool), } diff --git a/crates/xserial-client/src/session.rs b/crates/xserial-client/src/session.rs index cffd766..6ce4ad8 100644 --- a/crates/xserial-client/src/session.rs +++ b/crates/xserial-client/src/session.rs @@ -138,6 +138,22 @@ impl SessionHandle { .map_err(|_| String::from("session closed")) } + pub async fn set_dtr(&self, state: bool) -> Result<(), String> { + self.inner + .cmd_tx + .send(SessionCmd::SetDtr(state)) + .await + .map_err(|_| String::from("session closed")) + } + + pub async fn set_rts(&self, state: bool) -> Result<(), String> { + self.inner + .cmd_tx + .send(SessionCmd::SetRts(state)) + .await + .map_err(|_| String::from("session closed")) + } + pub async fn read(&self, timeout_ms: u64) -> Option { let mut rx = self.inner.event_tx.subscribe(); let deadline = Duration::from_millis(timeout_ms); @@ -304,6 +320,14 @@ impl Session { self.handle_reconfigure(config).await; true } + Some(SessionCmd::SetDtr(state)) => { + self.handle_set_dtr(state); + true + } + Some(SessionCmd::SetRts(state)) => { + self.handle_set_rts(state); + true + } None => false, } } @@ -332,6 +356,28 @@ impl Session { } } + fn handle_set_dtr(&mut self, state: bool) { + match self.conn.as_mut() { + Some(conn) => { + if let Err(err) = conn.set_dtr(state) { + self.emit_error(format!("set_dtr: {err}")); + } + } + None => self.emit_error(String::from("set_dtr: session not connected")), + } + } + + fn handle_set_rts(&mut self, state: bool) { + match self.conn.as_mut() { + Some(conn) => { + if let Err(err) = conn.set_rts(state) { + self.emit_error(format!("set_rts: {err}")); + } + } + None => self.emit_error(String::from("set_rts: session not connected")), + } + } + async fn handle_read_result( &mut self, result: Result, std::io::Error>, diff --git a/crates/xserial-core/src/transport/mod.rs b/crates/xserial-core/src/transport/mod.rs index ec564cb..4c9bf63 100644 --- a/crates/xserial-core/src/transport/mod.rs +++ b/crates/xserial-core/src/transport/mod.rs @@ -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"), } diff --git a/crates/xserial-core/src/transport/serial.rs b/crates/xserial-core/src/transport/serial.rs index 02a8177..45122bc 100644 --- a/crates/xserial-core/src/transport/serial.rs +++ b/crates/xserial-core/src/transport/serial.rs @@ -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()); } diff --git a/crates/xserial-core/tests/pipeline.rs b/crates/xserial-core/tests/pipeline.rs index 3616c50..e1f4daa 100644 --- a/crates/xserial-core/tests/pipeline.rs +++ b/crates/xserial-core/tests/pipeline.rs @@ -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(), diff --git a/crates/xserial-gui/src/app.rs b/crates/xserial-gui/src/app.rs index 01ccf44..054e413 100644 --- a/crates/xserial-gui/src/app.rs +++ b/crates/xserial-gui/src/app.rs @@ -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" ); diff --git a/crates/xserial-gui/src/panels/config.rs b/crates/xserial-gui/src/panels/config.rs index d20c107..fd1e949 100644 --- a/crates/xserial-gui/src/panels/config.rs +++ b/crates/xserial-gui/src/panels/config.rs @@ -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| { diff --git a/tools/test_plot_serial.c b/tools/test_plot_serial.c new file mode 100644 index 0000000..86a7117 --- /dev/null +++ b/tools/test_plot_serial.c @@ -0,0 +1,540 @@ +/* + * test_plot_serial.c — 持续向串口发送 MixedTextPlot 正弦波 + 文本流 + * + * 编译 (Linux): + * gcc -std=c11 -Wall -Wextra -O2 -o test_plot_serial tools/test_plot_serial.c -lm + * + * 用法: + * ./test_plot_serial /dev/ttyUSB0 + * ./test_plot_serial /dev/ttyUSB0 --baud 115200 --channels 2 --format xy --rate 1024 --freq 2.0 --amp 150 + * ./test_plot_serial --help + * + * 依赖: tools/xs_mixed_plot.h (同目录, 定义 COBS 和 MixedTextPlot 帧格式) + * + * 协议说明: + * Plot 帧: 0x1E 'P' + COBS(packet) + 0x00 + * 文本帧: UTF-8 文本行 + '\n' (直接写入, 由 MixedTextPlot framer 按行分割) + * + * 串口参数: 8N1, 无流控, 默认 115200 baud + */ + +#define _POSIX_C_SOURCE 200809L +#include "xs_mixed_plot.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +/* ── 配置默认值 ───────────────────────────────────────────────────── */ + +#define DEFAULT_BAUDRATE 115200 +#define DEFAULT_RATE 1024.0 /* samples/sec per channel */ +#define DEFAULT_FREQ 1.0 /* sine Hz */ +#define DEFAULT_AMP 100.0 +#define DEFAULT_TEXT_INTERVAL 1.0 /* seconds between text lines */ + +/* ── 全局状态 (用于信号处理) ──────────────────────────────────────── */ + +static volatile sig_atomic_t g_running = 1; + +static void sigint_handler(int sig) { + (void)sig; + g_running = 0; +} + +/* ── 缓冲大小 ─────────────────────────────────────────────────────── */ + +#define MAX_CHANNELS 8 +#define MAX_SPC 4096 /* samples_per_channel */ +#define MAX_VALUES (MAX_CHANNELS * MAX_SPC) /* float values per frame */ +#define MAX_PAYLOAD (MAX_VALUES * 4) +#define MAX_PACKET (13 + MAX_PAYLOAD) +#define MAX_FRAME (2 + (MAX_PACKET + MAX_PACKET / 254 + 1) + 1) +#define TEXT_BUF_SIZE 512 + +/* ── 运行时参数 ───────────────────────────────────────────────────── */ + +typedef struct { + const char *port; + int baudrate; + uint8_t channels; + uint16_t samples_per_channel; + uint8_t plot_format; /* XS_PLOT_FORMAT_INTERLEAVED / XY */ + double rate; /* samples/sec */ + double freq; /* sine Hz */ + double amp; + double text_interval; /* seconds */ +} args_t; + +/* ── 串口操作 ─────────────────────────────────────────────────────── */ + +static int serial_open(const char *path, int baudrate) { + int fd = open(path, O_RDWR | O_NOCTTY | O_NONBLOCK); + if (fd < 0) { + perror("open"); + return -1; + } + + /* 恢复阻塞模式, 但配合超时使用 select */ + int flags = fcntl(fd, F_GETFL, 0); + if (flags < 0) { perror("fcntl"); close(fd); return -1; } + flags &= ~O_NONBLOCK; + if (fcntl(fd, F_SETFL, flags) < 0) { perror("fcntl"); close(fd); return -1; } + + struct termios tty; + memset(&tty, 0, sizeof(tty)); + if (tcgetattr(fd, &tty) < 0) { perror("tcgetattr"); close(fd); return -1; } + + /* 输入: 忽略 BREAK, CR→NL 不转换, 奇偶校验不检查 */ + tty.c_iflag = IGNBRK; + + /* 输出: 原始模式, 不做任何转换 */ + tty.c_oflag = 0; + + /* 控制: 8 位, 无奇偶校验, 启用接收器 */ + tty.c_cflag = CS8 | CREAD | CLOCAL; + + /* 本地: 关闭标准行处理/回显/信号字符 */ + tty.c_lflag = 0; + + /* 特殊字符: 无超时 (VMIN=1, VTIME=0 → 阻塞读直到至少1字节) */ + tty.c_cc[VMIN] = 1; + tty.c_cc[VTIME] = 0; + + /* 设置波特率 */ + speed_t speed = B115200; + switch (baudrate) { + case 9600: speed = B9600; break; + case 19200: speed = B19200; break; + case 38400: speed = B38400; break; + case 57600: speed = B57600; break; + case 115200: speed = B115200; break; + case 230400: speed = B230400; break; + case 460800: speed = B460800; break; + case 921600: speed = B921600; break; + default: + fprintf(stderr, "不支持的波特率: %d (支持: 9600-921600)\n", baudrate); + close(fd); + return -1; + } + cfsetispeed(&tty, speed); + cfsetospeed(&tty, speed); + +#ifdef CRTSCTS + tty.c_cflag &= ~CRTSCTS; +#endif + /* 关闭软件流控 */ + tty.c_iflag &= ~(IXON | IXOFF | IXANY); + + if (tcsetattr(fd, TCSANOW, &tty) < 0) { + perror("tcsetattr"); + close(fd); + return -1; + } + + /* 排空缓冲区 */ + tcflush(fd, TCIOFLUSH); + + return fd; +} + +/* ── 时间工具 ─────────────────────────────────────────────────────── */ + +static double now_sec(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9; +} + +static void sleep_sec(double seconds) { + if (seconds <= 0.0) return; + struct timespec ts; + ts.tv_sec = (time_t)seconds; + ts.tv_nsec = (long)((seconds - (time_t)seconds) * 1e9); + clock_nanosleep(CLOCK_MONOTONIC, 0, &ts, NULL); +} + +/* ── 信号生成 ─────────────────────────────────────────────────────── */ + +static void generate_samples( + float *samples, + uint32_t sample_index, + uint8_t channels, + uint16_t samples_per_channel, + uint8_t plot_format, + double rate, + double freq, + double amp) +{ + if (plot_format == XS_PLOT_FORMAT_XY) { + /* XY 模式: 每对 (cos, sin) 画一个圆 */ + for (uint16_t i = 0; i < samples_per_channel; ++i) { + double t = (double)(sample_index + i) / rate; + samples[2 * i] = (float)(amp * cos(2.0 * M_PI * freq * t)); + samples[2 * i + 1] = (float)(amp * sin(2.0 * M_PI * freq * t)); + } + } else { + /* interleaved 模式: 每个通道相位偏移 */ + for (uint16_t i = 0; i < samples_per_channel; ++i) { + double t = (double)(sample_index + i) / rate; + for (uint8_t ch = 0; ch < channels; ++ch) { + double phase = 2.0 * M_PI * (double)ch / (double)channels; + samples[i * channels + ch] = (float)(amp * sin(2.0 * M_PI * freq * t + phase)); + } + } + } +} + +/* ── 用法 ─────────────────────────────────────────────────────────── */ + +static void print_usage(const char *prog) { + printf("用法: %s <串口> [选项]\n", prog); + printf("\n选项:\n"); + printf(" --baud 波特率 (默认 %d)\n", DEFAULT_BAUDRATE); + printf(" --channels 通道数 (默认 1, --format xy 时强制 2)\n"); + printf(" --format 格式: interleaved | xy (默认 interleaved)\n"); + printf(" --rate 采样率, samples/sec (默认 %.0f)\n", DEFAULT_RATE); + printf(" --freq 正弦频率 Hz (默认 %.1f)\n", DEFAULT_FREQ); + printf(" --amp 振幅 (默认 %.0f)\n", DEFAULT_AMP); + printf(" --text-interval 文本输出间隔秒 (默认 %.1f)\n", DEFAULT_TEXT_INTERVAL); + printf(" --spc 每帧每通道采样数 (默认 1)\n"); + printf(" -h, --help 显示帮助\n"); + printf("\n支持的波特率: 9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600\n"); + printf("\n示例:\n"); + printf(" %s /dev/ttyUSB0\n", prog); + printf(" %s /dev/ttyUSB0 --baud 921600 --channels 2 --rate 2048 --freq 5\n", prog); + printf(" %s /dev/ttyUSB0 --format xy --amp 200 --text-interval 2.0\n", prog); + printf(" %s /dev/ttyUSB0 --channels 4 --spc 128 --rate 1024\n", prog); +} + +/* ── 参数解析 ─────────────────────────────────────────────────────── */ + +static int parse_args(int argc, char **argv, args_t *args) { + memset(args, 0, sizeof(*args)); + args->baudrate = DEFAULT_BAUDRATE; + args->channels = 1; + args->samples_per_channel = 1; + args->plot_format = XS_PLOT_FORMAT_INTERLEAVED; + args->rate = DEFAULT_RATE; + args->freq = DEFAULT_FREQ; + args->amp = DEFAULT_AMP; + args->text_interval = DEFAULT_TEXT_INTERVAL; + + enum { OPT_BAUD = 256, OPT_CHANNELS, OPT_FORMAT, OPT_RATE, OPT_FREQ, + OPT_AMP, OPT_TEXT_INTERVAL, OPT_SPC }; + + static struct option long_opts[] = { + {"baud", required_argument, 0, OPT_BAUD}, + {"channels", required_argument, 0, OPT_CHANNELS}, + {"format", required_argument, 0, OPT_FORMAT}, + {"rate", required_argument, 0, OPT_RATE}, + {"freq", required_argument, 0, OPT_FREQ}, + {"amp", required_argument, 0, OPT_AMP}, + {"text-interval", required_argument, 0, OPT_TEXT_INTERVAL}, + {"spc", required_argument, 0, OPT_SPC}, + {"help", no_argument, 0, 'h'}, + {0, 0, 0, 0} + }; + + int opt; + while ((opt = getopt_long(argc, argv, "h", long_opts, NULL)) != -1) { + switch (opt) { + case OPT_BAUD: + args->baudrate = atoi(optarg); + break; + case OPT_CHANNELS: { + int v = atoi(optarg); + if (v < 1 || v > MAX_CHANNELS) { + fprintf(stderr, "通道数必须在 1-%d 之间\n", MAX_CHANNELS); + return -1; + } + args->channels = (uint8_t)v; + break; + } + case OPT_FORMAT: + if (strcmp(optarg, "xy") == 0) { + args->plot_format = XS_PLOT_FORMAT_XY; + } else if (strcmp(optarg, "interleaved") == 0) { + args->plot_format = XS_PLOT_FORMAT_INTERLEAVED; + } else { + fprintf(stderr, "未知格式: %s (支持: interleaved, xy)\n", optarg); + return -1; + } + break; + case OPT_RATE: + args->rate = atof(optarg); + break; + case OPT_FREQ: + args->freq = atof(optarg); + break; + case OPT_AMP: + args->amp = atof(optarg); + break; + case OPT_TEXT_INTERVAL: + args->text_interval = atof(optarg); + break; + case OPT_SPC: { + int v = atoi(optarg); + if (v < 1 || v > MAX_SPC) { + fprintf(stderr, "spc 必须在 1-%d 之间\n", MAX_SPC); + return -1; + } + args->samples_per_channel = (uint16_t)v; + break; + } + case 'h': + print_usage(argv[0]); + exit(0); + default: + print_usage(argv[0]); + return -1; + } + } + + if (optind >= argc) { + fprintf(stderr, "错误: 缺少串口路径\n"); + print_usage(argv[0]); + return -1; + } + args->port = argv[optind]; + + /* 校验 */ + if (args->plot_format == XS_PLOT_FORMAT_XY) { + if (args->channels != 2) { + fprintf(stderr, "错误: --format xy 要求 --channels 2\n"); + return -1; + } + } + if (args->channels == 0) { + fprintf(stderr, "错误: --channels 必须 >= 1\n"); + return -1; + } + if (args->rate <= 0.0) { + fprintf(stderr, "错误: --rate 必须为正数\n"); + return -1; + } + if (args->text_interval <= 0.0) { + fprintf(stderr, "错误: --text-interval 必须为正数\n"); + return -1; + } + + return 0; +} + +/* ── 发送 plot 帧 ─────────────────────────────────────────────────── */ + +static int send_plot_frame( + int fd, + const float *samples, + uint8_t channels, + uint16_t samples_per_channel, + uint8_t plot_format, + uint8_t *packet_buf, + size_t packet_cap, + uint8_t *frame_buf, + size_t frame_cap) +{ + size_t frame_len = 0; + xs_status_t rc = xs_mixed_build_plot_frame_f32( + samples, channels, samples_per_channel, plot_format, + packet_buf, packet_cap, + frame_buf, frame_cap, + &frame_len); + if (rc != XS_OK) { + fprintf(stderr, "构建 plot 帧失败: %d\n", rc); + return -1; + } + + size_t written = 0; + while (written < frame_len) { + ssize_t n = write(fd, frame_buf + written, frame_len - written); + if (n < 0) { + perror("write"); + return -1; + } + written += (size_t)n; + } + return 0; +} + +/* ── 发送文本行 ───────────────────────────────────────────────────── */ + +static int send_text_line( + int fd, + uint32_t sample_index, + const float *current_samples, + uint8_t channels, + uint16_t samples_per_channel, + const char *format_name) +{ + char buf[TEXT_BUF_SIZE]; + int pos = 0; + + pos += snprintf(buf + pos, sizeof(buf) - pos, + "sample=%-7u", sample_index); + + if (channels > 1) { + for (uint8_t ch = 0; ch < channels; ++ch) { + pos += snprintf(buf + pos, sizeof(buf) - pos, + " ch%d=%8.3f", ch + 1, (double)current_samples[ch]); + } + } else { + pos += snprintf(buf + pos, sizeof(buf) - pos, + " value=%8.3f", (double)current_samples[0]); + } + + pos += snprintf(buf + pos, sizeof(buf) - pos, + " [fmt=%s, ch=%u, spc=%u]\n", + format_name, channels, samples_per_channel); + + /* 确保不超过缓冲 */ + if (pos >= (int)sizeof(buf)) pos = (int)sizeof(buf) - 1; + + size_t len = (size_t)pos; + size_t written = 0; + while (written < len) { + ssize_t n = write(fd, buf + written, len - written); + if (n < 0) { + perror("write"); + return -1; + } + written += (size_t)n; + } + return 0; +} + +/* ── 主函数 ───────────────────────────────────────────────────────── */ + +int main(int argc, char **argv) { + args_t args; + if (parse_args(argc, argv, &args) < 0) { + return 1; + } + + /* 打印配置 */ + const char *format_name = (args.plot_format == XS_PLOT_FORMAT_XY) ? "xy" : "interleaved"; + double frame_interval = (double)args.samples_per_channel / args.rate; + double frame_rate = args.rate / (double)args.samples_per_channel; + + printf("═══ xserial Serial Plot Generator ═══\n"); + printf("串口: %s @ %d baud\n", args.port, args.baudrate); + printf("通道: %u 格式: %s\n", args.channels, format_name); + printf("频率: %.1f Hz 振幅: %.1f\n", args.freq, args.amp); + printf("采样率: %.0f samples/sec spc: %u\n", args.rate, args.samples_per_channel); + printf("帧间隔: %.3f ms (%.2f fps)\n", frame_interval * 1000.0, frame_rate); + printf("文本间隔: %.1f s\n", args.text_interval); + printf("═══════════════════════════════════════\n"); + + /* 分配缓冲 */ + uint32_t value_count = (uint32_t)args.channels * (uint32_t)args.samples_per_channel; + uint32_t payload_len = value_count * 4; + size_t packet_cap = (size_t)XS_PLOT_HEADER_SIZE + (size_t)payload_len; + size_t frame_cap = 2u + xs_cobs_max_encoded_size(packet_cap) + 1u; + + uint8_t *packet_buf = malloc(packet_cap); + uint8_t *frame_buf = malloc(frame_cap); + float *samples = malloc((size_t)value_count * sizeof(float)); + + if (!packet_buf || !frame_buf || !samples) { + fprintf(stderr, "内存分配失败\n"); + free(packet_buf); free(frame_buf); free(samples); + return 1; + } + + /* 打开串口 */ + int fd = serial_open(args.port, args.baudrate); + if (fd < 0) { + fprintf(stderr, "无法打开串口 %s\n", args.port); + free(packet_buf); free(frame_buf); free(samples); + return 1; + } + printf("已打开 %s\n\n", args.port); + + /* 设置信号处理 */ + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = sigint_handler; + sigaction(SIGINT, &sa, NULL); + sigaction(SIGTERM, &sa, NULL); + + /* 主循环 */ + uint32_t sample_index = 0; + uint64_t frame_count = 0; + uint64_t text_count = 0; + double started_at = now_sec(); + double next_text_at = started_at + args.text_interval; + + printf("开始发送 (Ctrl+C 停止) ...\n"); + + while (g_running) { + /* 生成采样数据 */ + generate_samples(samples, sample_index, + args.channels, args.samples_per_channel, + args.plot_format, args.rate, args.freq, args.amp); + + /* 发送 plot 帧 */ + if (send_plot_frame(fd, samples, + args.channels, args.samples_per_channel, + args.plot_format, + packet_buf, packet_cap, + frame_buf, frame_cap) < 0) { + fprintf(stderr, "发送失败, 退出\n"); + break; + } + frame_count++; + + /* 检查是否需要发送文本行 */ + double now = now_sec(); + if (now >= next_text_at) { + if (send_text_line(fd, sample_index, samples, + args.channels, args.samples_per_channel, + format_name) < 0) { + fprintf(stderr, "发送文本失败, 退出\n"); + break; + } + text_count++; + next_text_at += args.text_interval; + } + + sample_index += args.samples_per_channel; + + /* 帧间隔时序控制 */ + double elapsed = now_sec() - started_at; + double target = (double)frame_count * frame_interval; + double wait = target - elapsed; + if (wait > 0.0) { + sleep_sec(wait); + } + } + + double total_time = now_sec() - started_at; + printf("\n"); + printf("═══ 已停止 ═══\n"); + printf("运行时间: %.1f 秒\n", total_time); + printf("Plot 帧: %lu (%.1f fps)\n", + (unsigned long)frame_count, + total_time > 0.0 ? (double)frame_count / total_time : 0.0); + printf("文本行: %lu\n", (unsigned long)text_count); + + /* 清理 */ + tcflush(fd, TCIOFLUSH); + close(fd); + free(packet_buf); + free(frame_buf); + free(samples); + + return 0; +} diff --git a/tools/test_text_serial.c b/tools/test_text_serial.c new file mode 100644 index 0000000..a9d82e1 --- /dev/null +++ b/tools/test_text_serial.c @@ -0,0 +1,270 @@ +/* + * test_text_serial.c — 持续向串口发送纯文本正弦波数据流 + * + * 编译 (Linux): + * gcc -std=c11 -Wall -Wextra -O2 -o test_text_serial tools/test_text_serial.c -lm + * + * 用法: + * ./test_text_serial /dev/ttyUSB0 + * ./test_text_serial /dev/ttyUSB0 --baud 115200 --channels 2 --rate 100 --freq 2.0 --amp 150 + * ./test_text_serial --help + * + * 输出格式 (每行): + * sample=123 ch1=100.000 ch2=0.000 + * + * 串口参数: 8N1, 无流控, 默认 115200 baud + */ + +#define _POSIX_C_SOURCE 200809L + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +#define DEFAULT_BAUDRATE 115200 +#define DEFAULT_RATE 100.0 /* lines/sec */ +#define DEFAULT_FREQ 1.0 +#define DEFAULT_AMP 100.0 +#define MAX_CHANNELS 8 +#define LINE_BUF_SIZE 512 + +static volatile sig_atomic_t g_running = 1; + +static void sigint_handler(int sig) { + (void)sig; + g_running = 0; +} + +typedef struct { + const char *port; + int baudrate; + uint8_t channels; + double rate; + double freq; + double amp; +} args_t; + +static int serial_open(const char *path, int baudrate) { + int fd = open(path, O_RDWR | O_NOCTTY | O_NONBLOCK); + if (fd < 0) { perror("open"); return -1; } + + int flags = fcntl(fd, F_GETFL, 0); + if (flags < 0) { perror("fcntl"); close(fd); return -1; } + flags &= ~O_NONBLOCK; + if (fcntl(fd, F_SETFL, flags) < 0) { perror("fcntl"); close(fd); return -1; } + + struct termios tty; + memset(&tty, 0, sizeof(tty)); + if (tcgetattr(fd, &tty) < 0) { perror("tcgetattr"); close(fd); return -1; } + + tty.c_iflag = IGNBRK; + tty.c_oflag = 0; + tty.c_cflag = CS8 | CREAD | CLOCAL; + tty.c_lflag = 0; + tty.c_cc[VMIN] = 1; + tty.c_cc[VTIME] = 0; + + speed_t speed = B115200; + switch (baudrate) { + case 9600: speed = B9600; break; + case 19200: speed = B19200; break; + case 38400: speed = B38400; break; + case 57600: speed = B57600; break; + case 115200: speed = B115200; break; + case 230400: speed = B230400; break; + case 460800: speed = B460800; break; + case 921600: speed = B921600; break; + default: + fprintf(stderr, "不支持的波特率: %d\n", baudrate); + close(fd); + return -1; + } + cfsetispeed(&tty, speed); + cfsetospeed(&tty, speed); + +#ifdef CRTSCTS + tty.c_cflag &= ~CRTSCTS; +#endif + tty.c_iflag &= ~(IXON | IXOFF | IXANY); + + if (tcsetattr(fd, TCSANOW, &tty) < 0) { + perror("tcsetattr"); + close(fd); + return -1; + } + tcflush(fd, TCIOFLUSH); + return fd; +} + +static double now_sec(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9; +} + +static void sleep_sec(double seconds) { + if (seconds <= 0.0) return; + struct timespec ts; + ts.tv_sec = (time_t)seconds; + ts.tv_nsec = (long)((seconds - (time_t)seconds) * 1e9); + clock_nanosleep(CLOCK_MONOTONIC, 0, &ts, NULL); +} + +static int format_line(char *buf, size_t cap, uint32_t sample, const float *values, uint8_t channels) { + int pos = snprintf(buf, cap, "sample=%-7u", sample); + for (uint8_t ch = 0; ch < channels; ++ch) { + pos += snprintf(buf + pos, cap - (size_t)pos, " ch%d=%8.3f", ch + 1, (double)values[ch]); + } + pos += snprintf(buf + pos, cap - (size_t)pos, "\n"); + return pos >= (int)cap ? (int)cap - 1 : pos; +} + +static void print_usage(const char *prog) { + printf("用法: %s <串口> [选项]\n", prog); + printf("\n选项:\n"); + printf(" --baud 波特率 (默认 %d)\n", DEFAULT_BAUDRATE); + printf(" --channels 通道数 (默认 1)\n"); + printf(" --rate 行/秒 输出速率 (默认 %.0f)\n", DEFAULT_RATE); + printf(" --freq 正弦频率 Hz (默认 %.1f)\n", DEFAULT_FREQ); + printf(" --amp 振幅 (默认 %.0f)\n", DEFAULT_AMP); + printf(" -h, --help 显示帮助\n"); + printf("\n示例:\n"); + printf(" %s /dev/ttyUSB0\n", prog); + printf(" %s /dev/ttyUSB0 --channels 2 --rate 50 --freq 3\n", prog); + printf(" %s /dev/ttyUSB0 --baud 921600 --rate 1000\n", prog); +} + +static int parse_args(int argc, char **argv, args_t *args) { + memset(args, 0, sizeof(*args)); + args->baudrate = DEFAULT_BAUDRATE; + args->channels = 1; + args->rate = DEFAULT_RATE; + args->freq = DEFAULT_FREQ; + args->amp = DEFAULT_AMP; + + enum { OPT_BAUD = 256, OPT_CHANNELS, OPT_RATE, OPT_FREQ, OPT_AMP }; + + static struct option long_opts[] = { + {"baud", required_argument, 0, OPT_BAUD}, + {"channels", required_argument, 0, OPT_CHANNELS}, + {"rate", required_argument, 0, OPT_RATE}, + {"freq", required_argument, 0, OPT_FREQ}, + {"amp", required_argument, 0, OPT_AMP}, + {"help", no_argument, 0, 'h'}, + {0, 0, 0, 0} + }; + + int opt; + while ((opt = getopt_long(argc, argv, "h", long_opts, NULL)) != -1) { + switch (opt) { + case OPT_BAUD: args->baudrate = atoi(optarg); break; + case OPT_CHANNELS: { + int v = atoi(optarg); + if (v < 1 || v > MAX_CHANNELS) { + fprintf(stderr, "通道数必须在 1-%d 之间\n", MAX_CHANNELS); + return -1; + } + args->channels = (uint8_t)v; + break; + } + case OPT_RATE: args->rate = atof(optarg); break; + case OPT_FREQ: args->freq = atof(optarg); break; + case OPT_AMP: args->amp = atof(optarg); break; + case 'h': print_usage(argv[0]); exit(0); + default: print_usage(argv[0]); return -1; + } + } + + if (optind >= argc) { + fprintf(stderr, "错误: 缺少串口路径\n"); + print_usage(argv[0]); + return -1; + } + args->port = argv[optind]; + + if (args->rate <= 0.0) { fprintf(stderr, "错误: --rate 必须为正数\n"); return -1; } + + return 0; +} + +int main(int argc, char **argv) { + args_t args; + if (parse_args(argc, argv, &args) < 0) return 1; + + double interval = 1.0 / args.rate; + + printf("═══ xserial Plain Text Generator ═══\n"); + printf("串口: %s @ %d baud\n", args.port, args.baudrate); + printf("通道: %u 振幅: %.1f 频率: %.1f Hz\n", args.channels, args.amp, args.freq); + printf("速率: %.0f lines/sec (间隔 %.3f ms)\n", args.rate, interval * 1000.0); + printf("═════════════════════════════════════\n"); + + float values[MAX_CHANNELS]; + char line[LINE_BUF_SIZE]; + + int fd = serial_open(args.port, args.baudrate); + if (fd < 0) { fprintf(stderr, "无法打开串口 %s\n", args.port); return 1; } + printf("已打开 %s\n\n开始发送 (Ctrl+C 停止) ...\n", args.port); + + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = sigint_handler; + sigaction(SIGINT, &sa, NULL); + sigaction(SIGTERM, &sa, NULL); + + uint32_t sample_index = 0; + uint64_t line_count = 0; + double started_at = now_sec(); + + while (g_running) { + double t = (double)sample_index / args.rate; + for (uint8_t ch = 0; ch < args.channels; ++ch) { + double phase = 2.0 * M_PI * (double)ch / (double)args.channels; + values[ch] = (float)(args.amp * sin(2.0 * M_PI * args.freq * t + phase)); + } + + int len = format_line(line, sizeof(line), sample_index, values, args.channels); + size_t written = 0; + while (written < (size_t)len) { + ssize_t n = write(fd, line + written, (size_t)len - written); + if (n < 0) { perror("write"); goto cleanup; } + written += (size_t)n; + } + line_count++; + + double elapsed = now_sec() - started_at; + double target = (double)line_count * interval; + double wait = target - elapsed; + if (wait > 0.0) sleep_sec(wait); + + sample_index++; + } + +cleanup: { + double total_time = now_sec() - started_at; + printf("\n═══ 已停止 ═══\n"); + printf("运行时间: %.1f 秒\n", total_time); + printf("行数: %lu (%.1f lines/sec)\n", + (unsigned long)line_count, + total_time > 0.0 ? (double)line_count / total_time : 0.0); + + tcflush(fd, TCIOFLUSH); + close(fd); + return 0; +} +}