rtcom_core/config.rs
1//! Serial line configuration and modem-status types.
2//!
3//! These are the framing and flow parameters every [`SerialDevice`] needs to
4//! expose. They intentionally mirror the classic `termios` model (data bits /
5//! stop bits / parity / flow control) so behaviour lines up with user
6//! expectations inherited from `picocom` and `tio`.
7//!
8//! [`SerialDevice`]: crate::SerialDevice
9
10use std::time::Duration;
11
12/// Default read poll timeout used by blocking backends.
13///
14/// The async backend does not gate reads on this value, but it is stored so
15/// [`SerialConfig`] can be printed verbatim and so future blocking fallbacks
16/// behave consistently.
17pub const DEFAULT_READ_TIMEOUT: Duration = Duration::from_millis(100);
18
19/// Number of data bits per frame.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21pub enum DataBits {
22 /// 5 data bits per frame.
23 Five,
24 /// 6 data bits per frame.
25 Six,
26 /// 7 data bits per frame.
27 Seven,
28 /// 8 data bits per frame (the default and the only mode most USB-serial
29 /// bridges support).
30 Eight,
31}
32
33impl DataBits {
34 /// Returns the numeric width in bits.
35 #[must_use]
36 pub const fn bits(self) -> u8 {
37 match self {
38 Self::Five => 5,
39 Self::Six => 6,
40 Self::Seven => 7,
41 Self::Eight => 8,
42 }
43 }
44}
45
46/// Number of stop bits appended after the data / parity bits.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
48pub enum StopBits {
49 /// One stop bit (default).
50 One,
51 /// Two stop bits.
52 Two,
53}
54
55/// Parity mode.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
57pub enum Parity {
58 /// No parity bit (default).
59 None,
60 /// Even parity.
61 Even,
62 /// Odd parity.
63 Odd,
64 /// Mark parity (parity bit always 1). Rare; not supported on all
65 /// platforms.
66 Mark,
67 /// Space parity (parity bit always 0). Rare; not supported on all
68 /// platforms.
69 Space,
70}
71
72/// Flow-control mode.
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
74pub enum FlowControl {
75 /// No flow control (default).
76 None,
77 /// Hardware flow control using the RTS/CTS lines.
78 Hardware,
79 /// Software flow control using XON/XOFF bytes (0x11 / 0x13).
80 Software,
81}
82
83/// Snapshot of the input-side modem control lines.
84///
85/// Returned by [`SerialDevice::modem_status`](crate::SerialDevice::modem_status).
86/// Each field is `true` when the corresponding line is asserted. The struct
87/// is deliberately a flat record of four booleans — it mirrors the hardware
88/// register one-to-one — so the `struct_excessive_bools` lint does not apply.
89#[allow(clippy::struct_excessive_bools)]
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
91pub struct ModemStatus {
92 /// Clear to Send.
93 pub cts: bool,
94 /// Data Set Ready.
95 pub dsr: bool,
96 /// Ring Indicator.
97 pub ri: bool,
98 /// Carrier Detect.
99 pub cd: bool,
100}
101
102/// Snapshot of the modem output lines as rtcom knows them.
103///
104/// Unlike [`ModemStatus`] (which reflects the input-side lines CTS / DSR /
105/// RI / CD and requires polling the device), the output lines DTR and RTS
106/// are driven by rtcom itself — so the current state is simply whatever
107/// the `Session` last wrote. The TUI modem-control dialog (v0.2 task 14)
108/// consumes this snapshot for its read-only "Current output lines"
109/// display.
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
111pub struct ModemLineSnapshot {
112 /// Data Terminal Ready output line: `true` when asserted.
113 pub dtr: bool,
114 /// Request To Send output line: `true` when asserted.
115 pub rts: bool,
116}
117
118/// Full serial-link configuration.
119///
120/// `SerialConfig` is what the CLI builds from command-line flags (see
121/// `rtcom-cli` Issue #3) and what the session orchestrator hands to a
122/// [`SerialDevice`](crate::SerialDevice) at open time. It is also what
123/// [`SerialDevice::config`](crate::SerialDevice::config) returns so runtime
124/// code can display or serialize the current link parameters.
125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
126pub struct SerialConfig {
127 /// Baud rate in bits per second.
128 pub baud_rate: u32,
129 /// Data bits per frame.
130 pub data_bits: DataBits,
131 /// Stop bits per frame.
132 pub stop_bits: StopBits,
133 /// Parity mode.
134 pub parity: Parity,
135 /// Flow-control mode.
136 pub flow_control: FlowControl,
137 /// Timeout used by blocking reads (unused on the async path, but kept so
138 /// `config()` remains a faithful record of the requested settings).
139 pub read_timeout: Duration,
140}
141
142impl Default for SerialConfig {
143 /// Returns the tio/picocom-compatible default: `115200 8N1`, no flow control.
144 fn default() -> Self {
145 Self {
146 baud_rate: 115_200,
147 data_bits: DataBits::Eight,
148 stop_bits: StopBits::One,
149 parity: Parity::None,
150 flow_control: FlowControl::None,
151 read_timeout: DEFAULT_READ_TIMEOUT,
152 }
153 }
154}
155
156impl SerialConfig {
157 /// Validates that the configuration is internally consistent.
158 ///
159 /// Currently only rejects a zero baud rate; more checks (e.g. disallowing
160 /// `Mark`/`Space` on platforms that don't implement them) may be added in
161 /// the future.
162 ///
163 /// # Errors
164 ///
165 /// Returns [`Error::InvalidConfig`](crate::Error::InvalidConfig) if the
166 /// configuration cannot be used to open a device.
167 pub fn validate(&self) -> crate::Result<()> {
168 if self.baud_rate == 0 {
169 return Err(crate::Error::InvalidConfig(
170 "baud_rate must be non-zero".into(),
171 ));
172 }
173 Ok(())
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 #[test]
182 fn default_is_115200_8n1_no_flow() {
183 let cfg = SerialConfig::default();
184 assert_eq!(cfg.baud_rate, 115_200);
185 assert_eq!(cfg.data_bits, DataBits::Eight);
186 assert_eq!(cfg.stop_bits, StopBits::One);
187 assert_eq!(cfg.parity, Parity::None);
188 assert_eq!(cfg.flow_control, FlowControl::None);
189 }
190
191 #[test]
192 fn data_bits_width_matches_enum() {
193 assert_eq!(DataBits::Five.bits(), 5);
194 assert_eq!(DataBits::Six.bits(), 6);
195 assert_eq!(DataBits::Seven.bits(), 7);
196 assert_eq!(DataBits::Eight.bits(), 8);
197 }
198
199 #[test]
200 fn validate_rejects_zero_baud() {
201 let cfg = SerialConfig {
202 baud_rate: 0,
203 ..SerialConfig::default()
204 };
205 assert!(cfg.validate().is_err());
206 }
207
208 #[test]
209 fn validate_accepts_default() {
210 assert!(SerialConfig::default().validate().is_ok());
211 }
212
213 #[test]
214 fn modem_line_snapshot_default_both_false() {
215 let s = ModemLineSnapshot::default();
216 assert!(!s.dtr);
217 assert!(!s.rts);
218 }
219}