Skip to main content

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}