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