sericom_core/
cli.rs

1//! This module holds the functions that are called from `sericom` when receiving
2//! CLI commands/arguments.
3
4use crate::{
5    configs::get_config,
6    create_recursive,
7    debug::run_debug_output,
8    map_miette,
9    screen_buffer::UICommand,
10    serial_actor::{
11        SerialActor, SerialEvent, SerialMessage,
12        tasks::{run_file_output, run_stdin_input, run_stdout_output},
13    },
14};
15use crossterm::{
16    cursor, event, execute,
17    style::Stylize,
18    terminal::{self, ClearType},
19};
20use miette::{Context, IntoDiagnostic};
21use serial2_tokio::SerialPort;
22use std::{
23    io::{self, Write},
24    path::PathBuf,
25};
26
27/// Spawns all of the tasks responsible for maintaining an interactive terminal session.
28pub async fn interactive_session(
29    connection: SerialPort,
30    file: Option<String>,
31    debug: bool,
32    port_name: &str,
33) -> miette::Result<()> {
34    // Setup terminal
35    let mut stdout = io::stdout();
36    terminal::enable_raw_mode()
37        .into_diagnostic()
38        .wrap_err("Failed to enable raw mode.".red())?;
39    execute!(
40        stdout,
41        terminal::EnterAlternateScreen,
42        terminal::SetTitle(port_name),
43        terminal::Clear(ClearType::All),
44        event::EnableBracketedPaste,
45        event::EnableMouseCapture,
46        cursor::MoveTo(0, 0)
47    )
48    .into_diagnostic()
49    .wrap_err("Failed to setup the terminal.".red())?;
50
51    // Create channels
52    let (command_tx, command_rx) = tokio::sync::mpsc::channel::<SerialMessage>(100);
53    let (ui_tx, ui_rx) = tokio::sync::mpsc::channel::<UICommand>(100);
54    let (broadcast_event_tx, _) = tokio::sync::broadcast::channel::<SerialEvent>(128);
55    let stdout_rx = broadcast_event_tx.subscribe();
56
57    // Create tasks
58    let mut tasks = tokio::task::JoinSet::new();
59
60    if let Some(path) = file {
61        let config = get_config();
62        let default_out_dir = PathBuf::from(&config.defaults.out_dir);
63        let input_path = PathBuf::from(path);
64
65        let file_path = if input_path.is_absolute() {
66            let parent = input_path.parent().unwrap_or(&default_out_dir);
67            create_recursive!(parent);
68            input_path
69        } else {
70            let joined_path = default_out_dir.join(input_path);
71            let parent_path = joined_path.parent().expect("Does not have root");
72            create_recursive!(parent_path);
73            joined_path
74        };
75
76        let file_rx = broadcast_event_tx.subscribe();
77        tasks.spawn(run_file_output(file_rx, file_path));
78    }
79
80    if debug {
81        let debug_rx = broadcast_event_tx.subscribe();
82        tasks.spawn(run_debug_output(debug_rx));
83    }
84
85    let actor = SerialActor::new(connection, command_rx, broadcast_event_tx);
86    tasks.spawn(actor.run());
87
88    tasks.spawn(run_stdout_output(stdout_rx, ui_rx));
89    tasks.spawn(run_stdin_input(command_tx, ui_tx));
90
91    tasks.join_all().await;
92    ensure_terminal_cleanup(stdout);
93    Ok(())
94}
95
96/// Opens a serial `port` for communication with the specified `baud`.
97///
98/// Returns `Ok(SerialPort)` or errors if unable to set the baud rate or open the `port`.
99pub fn open_connection(baud: u32, port: &str) -> miette::Result<SerialPort> {
100    let settings = |mut s: serial2_tokio::Settings| -> std::io::Result<serial2_tokio::Settings> {
101        s.set_raw();
102        s.set_baud_rate(baud)?;
103        s.set_char_size(serial2_tokio::CharSize::Bits8);
104        s.set_stop_bits(serial2_tokio::StopBits::One);
105        s.set_parity(serial2_tokio::Parity::None);
106        s.set_flow_control(serial2_tokio::FlowControl::None);
107        Ok(s)
108    };
109    let con = map_miette!(
110        SerialPort::open(port, settings),
111        format!("Failed to open port '{}'", port),
112        format!(
113            "{} {} [OPTIONS] [PORT] [COMMAND]",
114            "USAGE:".bold().underlined(),
115            "sericom".bold()
116        ),
117        help = format!(
118            "To see available ports, try `{}`.",
119            "sericom list-ports".bold().cyan()
120        )
121    )?;
122    Ok(con)
123}
124
125/// Gets the settings for the `port` with the specified `baud`.
126pub fn get_settings(baud: u32, port: &str) -> miette::Result<()> {
127    // https://www.contec.com/support/basic-knowledge/daq-control/serial-communicatin/
128    let mut stdout = io::stdout();
129    let con = open_connection(baud, port)?;
130    let settings = map_miette!(
131        con.get_configuration(),
132        format!("Failed to get settings for port '{}'", port),
133        format!(
134            "{} {} [OPTIONS] {} <PORT>",
135            "USAGE:".bold().underlined(),
136            "sericom list-settings".bold(),
137            "--port".bold()
138        )
139    )?;
140    let b = map_miette!(
141        settings.get_baud_rate(),
142        format!("Failed to get the baud rate for port '{}'", port),
143        format!(
144            "{} {} [OPTIONS] {} <PORT>",
145            "USAGE:".bold().underlined(),
146            "sericom list-settings".bold(),
147            "--port".bold()
148        )
149    )?;
150    let c = map_miette!(
151        settings.get_char_size(),
152        format!("Failed to get the char size for port '{}'", port),
153        format!(
154            "{} {} [OPTIONS] {} <PORT>",
155            "USAGE:".bold().underlined(),
156            "sericom list-settings".bold(),
157            "--port".bold()
158        )
159    )?;
160    let s = map_miette!(
161        settings.get_stop_bits(),
162        format!("Failed to get stop bits for port '{}'", port),
163        format!(
164            "{} {} [OPTIONS] {} <PORT>",
165            "USAGE:".bold().underlined(),
166            "sericom list-settings".bold(),
167            "--port".bold()
168        )
169    )?;
170    let p = map_miette!(
171        settings.get_parity(),
172        format!("Failed to get parity for port '{}'", port),
173        format!(
174            "{} {} [OPTIONS] {} <PORT>",
175            "USAGE:".bold().underlined(),
176            "sericom list-settings".bold(),
177            "--port".bold()
178        )
179    )?;
180    let f = map_miette!(
181        settings.get_flow_control(),
182        format!("Failed to get flow control for port '{}'", port),
183        format!(
184            "{} {} [OPTIONS] {} <PORT>",
185            "USAGE:".bold().underlined(),
186            "sericom list-settings".bold(),
187            "--port".bold()
188        )
189    )?;
190
191    let cts = map_miette!(
192        con.read_cts(),
193        format!("Failed to read CTS for port '{}'", port),
194        format!(
195            "{} {} [OPTIONS] {} <PORT>",
196            "USAGE:".bold().underlined(),
197            "sericom list-settings".bold(),
198            "--port".bold()
199        )
200    )?;
201    let dsr = map_miette!(
202        con.read_dsr(),
203        format!("Failed to read DSR for port '{}'", port),
204        format!(
205            "{} {} [OPTIONS] {} <PORT>",
206            "USAGE:".bold().underlined(),
207            "sericom list-settings".bold(),
208            "--port".bold()
209        )
210    )?;
211    let ri = map_miette!(
212        con.read_ri(),
213        format!("Failed to read RI for port '{}'", port),
214        format!(
215            "{} {} [OPTIONS] {} <PORT>",
216            "USAGE:".bold().underlined(),
217            "sericom list-settings".bold(),
218            "--port".bold()
219        )
220    )?;
221    let cd = map_miette!(
222        con.read_cd(),
223        format!("Failed to read CD for port '{}'", port),
224        format!(
225            "{} {} [OPTIONS] {} <PORT>",
226            "USAGE:".bold().underlined(),
227            "sericom list-settings".bold(),
228            "--port".bold()
229        )
230    )?;
231
232    write!(stdout, "Baud rate: {b}\r\n")
233        .into_diagnostic()
234        .wrap_err("Failed to write to stdout.".red())?;
235    write!(stdout, "Char size: {c}\r\n")
236        .into_diagnostic()
237        .wrap_err("Failed to write to stdout.".red())?;
238    write!(stdout, "Stop bits: {s}\r\n")
239        .into_diagnostic()
240        .wrap_err("Failed to write to stdout.".red())?;
241    write!(stdout, "Parity mechanism: {p}\r\n")
242        .into_diagnostic()
243        .wrap_err("Failed to write to stdout.".red())?;
244    write!(stdout, "Flow control: {f}\r\n")
245        .into_diagnostic()
246        .wrap_err("Failed to write to stdout.".red())?;
247    write!(stdout, "Clear To Send line: {cts}\r\n")
248        .into_diagnostic()
249        .wrap_err("Failed to write to stdout.".red())?;
250    write!(stdout, "Data Set Ready line: {dsr}\r\n")
251        .into_diagnostic()
252        .wrap_err("Failed to write to stdout.".red())?;
253    write!(stdout, "Ring Indicator line: {ri}\r\n")
254        .into_diagnostic()
255        .wrap_err("Failed to write to stdout.".red())?;
256    write!(stdout, "Carrier Detect line: {cd}\r\n")
257        .into_diagnostic()
258        .wrap_err("Failed to write to stdout.".red())?;
259
260    Ok(())
261}
262
263/// Prints a list of available serial ports to stdout.
264///
265/// Ultimately a wrapper around [`SerialPort::available_ports()`] and may error
266/// if it is called on an unsupported platform as per [`SerialPort::available_ports()]s docs
267pub fn list_serial_ports() -> miette::Result<()> {
268    let mut stdout = io::stdout();
269    let ports = map_miette!(
270        SerialPort::available_ports(),
271        "Could not list available ports."
272    )?;
273    for path in ports {
274        if let Some(path) = path.to_str() {
275            let line = [path, "\r\n"].concat();
276            stdout
277                .write(line.as_bytes())
278                .into_diagnostic()
279                .wrap_err("Failed to write to stdout.".red())?
280        } else {
281            continue;
282        };
283    }
284    Ok(())
285}
286
287/// Used as a 'value_parser' for sericom's clap CLI struct to validate baud rates
288pub fn valid_baud_rate(s: &str) -> Result<u32, String> {
289    let baud: u32 = s
290        .parse()
291        .map_err(|_| format!("`{s}` isn't a valid baud rate"))?;
292    if serial2_tokio::COMMON_BAUD_RATES.contains(&baud) {
293        Ok(baud)
294    } else {
295        Err(format!(
296            "'{}' is not a valid baud rate; valid baud rates include {:?}",
297            baud,
298            serial2_tokio::COMMON_BAUD_RATES
299        ))
300    }
301}
302
303fn ensure_terminal_cleanup(mut stdout: io::Stdout) {
304    use crossterm::{
305        cursor::Show,
306        execute,
307        terminal::{LeaveAlternateScreen, disable_raw_mode},
308    };
309    let _ = execute!(
310        stdout,
311        event::DisableMouseCapture,
312        event::DisableBracketedPaste,
313        LeaveAlternateScreen,
314        Show
315    );
316    let _ = disable_raw_mode();
317    let _ = stdout.flush();
318}