Skip to main content

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    compat_port_path,
6    configs::get_config,
7    create_recursive,
8    debug::run_debug_output,
9    map_miette,
10    screen_buffer::UICommand,
11    serial_actor::{
12        SerialActor, SerialEvent, SerialMessage,
13        tasks::{run_file_output, run_stdin_input, run_stdout_output},
14    },
15};
16use crossterm::{
17    cursor, event, execute,
18    style::Stylize,
19    terminal::{self, ClearType},
20};
21use miette::{Context, IntoDiagnostic};
22use serial2_tokio::SerialPort;
23use std::{
24    io::{self, Write},
25    path::PathBuf,
26};
27use tracing::{Level, trace};
28
29/// Spawns all of the tasks responsible for maintaining an interactive terminal session.
30pub async fn interactive_session(
31    connection: SerialPort,
32    file_path: Option<Option<PathBuf>>,
33    debug: bool,
34    port_name: &str,
35) -> miette::Result<()> {
36    let span = tracing::span!(Level::TRACE, "Interactive Session");
37    let _enter = span.enter();
38    // Setup terminal
39    let mut stdout = io::stdout();
40    terminal::enable_raw_mode()
41        .into_diagnostic()
42        .wrap_err("Failed to enable raw mode.".red())?;
43    execute!(
44        stdout,
45        terminal::EnterAlternateScreen,
46        terminal::SetTitle(port_name),
47        terminal::Clear(ClearType::All),
48        event::EnableBracketedPaste,
49        event::EnableMouseCapture,
50        cursor::MoveTo(0, 0)
51    )
52    .into_diagnostic()
53    .wrap_err("Failed to setup the terminal.".red())?;
54    let config = get_config();
55
56    trace!("Creating channels");
57    // Create channels
58    let (command_tx, command_rx) = tokio::sync::mpsc::channel::<SerialMessage>(100);
59    let (ui_tx, ui_rx) = tokio::sync::mpsc::channel::<UICommand>(100);
60    let (broadcast_event_tx, _) = tokio::sync::broadcast::channel::<SerialEvent>(128);
61    let stdout_rx = broadcast_event_tx.subscribe();
62
63    // Create tasks
64    let mut tasks = tokio::task::JoinSet::new();
65
66    if let Some(maybe_path) = file_path {
67        let default_out_dir = PathBuf::from(&config.defaults.out_dir);
68        let file_path = match maybe_path {
69            Some(path) => {
70                // If given an absolute path - override the `default_out_dir`
71                if path.is_absolute() {
72                    let parent = path.parent().unwrap_or(&default_out_dir);
73                    create_recursive!(parent);
74                    path
75                } else {
76                    let joined_path = default_out_dir.join(&path);
77                    let parent_path = joined_path.parent().expect("Does not have root");
78                    create_recursive!(parent_path);
79                    joined_path
80                }
81            }
82            None => {
83                let default_out_dir = PathBuf::from(&config.defaults.out_dir);
84                compat_port_path!(default_out_dir, port_name)
85            }
86        };
87        let file_rx = broadcast_event_tx.subscribe();
88        tasks.spawn(async move {
89            run_file_output(file_rx, file_path.clone()).await;
90            run_file_exit_script(config, file_path);
91        });
92    };
93
94    if debug {
95        let debug_rx = broadcast_event_tx.subscribe();
96        tasks.spawn(run_debug_output(debug_rx));
97    }
98
99    let actor = SerialActor::new(connection, command_rx, broadcast_event_tx);
100    tasks.spawn(actor.run());
101
102    tasks.spawn(run_stdout_output(stdout_rx, ui_rx));
103    tasks.spawn(run_stdin_input(command_tx, ui_tx));
104
105    tasks.join_all().await;
106    ensure_terminal_cleanup(stdout);
107    Ok(())
108}
109
110/// Opens a serial `port` for communication with the specified `baud`.
111///
112/// Returns `Ok(SerialPort)` or errors if unable to set the baud rate or open the `port`.
113pub fn open_connection(baud: u32, port: &str) -> miette::Result<SerialPort> {
114    let settings = |mut s: serial2_tokio::Settings| -> std::io::Result<serial2_tokio::Settings> {
115        s.set_raw();
116        s.set_baud_rate(baud)?;
117        s.set_char_size(serial2_tokio::CharSize::Bits8);
118        s.set_stop_bits(serial2_tokio::StopBits::One);
119        s.set_parity(serial2_tokio::Parity::None);
120        s.set_flow_control(serial2_tokio::FlowControl::None);
121        Ok(s)
122    };
123    let con = map_miette!(
124        SerialPort::open(port, settings),
125        format!("Failed to open port '{}'", port),
126        format!(
127            "{} {} [OPTIONS] [PORT] [COMMAND]",
128            "USAGE:".bold().underlined(),
129            "sericom".bold()
130        ),
131        help = format!(
132            "To see available ports, try `{}`.",
133            "sericom list-ports".bold().cyan()
134        )
135    )?;
136    Ok(con)
137}
138
139/// Gets the settings for the `port` with the specified `baud`.
140pub fn get_settings(baud: u32, port: &str) -> miette::Result<()> {
141    // https://www.contec.com/support/basic-knowledge/daq-control/serial-communicatin/
142    let mut stdout = io::stdout();
143    let con = open_connection(baud, port)?;
144    let settings = map_miette!(
145        con.get_configuration(),
146        format!("Failed to get settings for port '{}'", port),
147        format!(
148            "{} {} [OPTIONS] {} <PORT>",
149            "USAGE:".bold().underlined(),
150            "sericom list-settings".bold(),
151            "--port".bold()
152        )
153    )?;
154    let b = map_miette!(
155        settings.get_baud_rate(),
156        format!("Failed to get the baud rate for port '{}'", port),
157        format!(
158            "{} {} [OPTIONS] {} <PORT>",
159            "USAGE:".bold().underlined(),
160            "sericom list-settings".bold(),
161            "--port".bold()
162        )
163    )?;
164    let c = map_miette!(
165        settings.get_char_size(),
166        format!("Failed to get the char size for port '{}'", port),
167        format!(
168            "{} {} [OPTIONS] {} <PORT>",
169            "USAGE:".bold().underlined(),
170            "sericom list-settings".bold(),
171            "--port".bold()
172        )
173    )?;
174    let s = map_miette!(
175        settings.get_stop_bits(),
176        format!("Failed to get stop bits for port '{}'", port),
177        format!(
178            "{} {} [OPTIONS] {} <PORT>",
179            "USAGE:".bold().underlined(),
180            "sericom list-settings".bold(),
181            "--port".bold()
182        )
183    )?;
184    let p = map_miette!(
185        settings.get_parity(),
186        format!("Failed to get parity for port '{}'", port),
187        format!(
188            "{} {} [OPTIONS] {} <PORT>",
189            "USAGE:".bold().underlined(),
190            "sericom list-settings".bold(),
191            "--port".bold()
192        )
193    )?;
194    let f = map_miette!(
195        settings.get_flow_control(),
196        format!("Failed to get flow control for port '{}'", port),
197        format!(
198            "{} {} [OPTIONS] {} <PORT>",
199            "USAGE:".bold().underlined(),
200            "sericom list-settings".bold(),
201            "--port".bold()
202        )
203    )?;
204
205    let cts = map_miette!(
206        con.read_cts(),
207        format!("Failed to read CTS for port '{}'", port),
208        format!(
209            "{} {} [OPTIONS] {} <PORT>",
210            "USAGE:".bold().underlined(),
211            "sericom list-settings".bold(),
212            "--port".bold()
213        )
214    )?;
215    let dsr = map_miette!(
216        con.read_dsr(),
217        format!("Failed to read DSR for port '{}'", port),
218        format!(
219            "{} {} [OPTIONS] {} <PORT>",
220            "USAGE:".bold().underlined(),
221            "sericom list-settings".bold(),
222            "--port".bold()
223        )
224    )?;
225    let ri = map_miette!(
226        con.read_ri(),
227        format!("Failed to read RI for port '{}'", port),
228        format!(
229            "{} {} [OPTIONS] {} <PORT>",
230            "USAGE:".bold().underlined(),
231            "sericom list-settings".bold(),
232            "--port".bold()
233        )
234    )?;
235    let cd = map_miette!(
236        con.read_cd(),
237        format!("Failed to read CD for port '{}'", port),
238        format!(
239            "{} {} [OPTIONS] {} <PORT>",
240            "USAGE:".bold().underlined(),
241            "sericom list-settings".bold(),
242            "--port".bold()
243        )
244    )?;
245
246    write!(stdout, "Baud rate: {b}\r\n")
247        .into_diagnostic()
248        .wrap_err("Failed to write to stdout.".red())?;
249    write!(stdout, "Char size: {c}\r\n")
250        .into_diagnostic()
251        .wrap_err("Failed to write to stdout.".red())?;
252    write!(stdout, "Stop bits: {s}\r\n")
253        .into_diagnostic()
254        .wrap_err("Failed to write to stdout.".red())?;
255    write!(stdout, "Parity mechanism: {p}\r\n")
256        .into_diagnostic()
257        .wrap_err("Failed to write to stdout.".red())?;
258    write!(stdout, "Flow control: {f}\r\n")
259        .into_diagnostic()
260        .wrap_err("Failed to write to stdout.".red())?;
261    write!(stdout, "Clear To Send line: {cts}\r\n")
262        .into_diagnostic()
263        .wrap_err("Failed to write to stdout.".red())?;
264    write!(stdout, "Data Set Ready line: {dsr}\r\n")
265        .into_diagnostic()
266        .wrap_err("Failed to write to stdout.".red())?;
267    write!(stdout, "Ring Indicator line: {ri}\r\n")
268        .into_diagnostic()
269        .wrap_err("Failed to write to stdout.".red())?;
270    write!(stdout, "Carrier Detect line: {cd}\r\n")
271        .into_diagnostic()
272        .wrap_err("Failed to write to stdout.".red())?;
273
274    Ok(())
275}
276
277/// Prints a list of available serial ports to stdout.
278///
279/// Ultimately a wrapper around [`SerialPort::available_ports()`] and may error
280/// if it is called on an unsupported platform as per [`SerialPort::available_ports()]s docs
281pub fn list_serial_ports() -> miette::Result<()> {
282    let mut stdout = io::stdout();
283    let ports = map_miette!(
284        SerialPort::available_ports(),
285        "Could not list available ports."
286    )?;
287    for path in ports {
288        if let Some(path) = path.to_str() {
289            let line = [path, "\r\n"].concat();
290            stdout
291                .write(line.as_bytes())
292                .into_diagnostic()
293                .wrap_err("Failed to write to stdout.".red())?
294        } else {
295            continue;
296        };
297    }
298    Ok(())
299}
300
301/// Used as a [`value_parser`](https://docs.rs/clap/latest/clap/struct.Arg.html#method.value_parser) for [`sericom`](https://crates.io/crates/sericom)s [`clap`](https://docs.rs/clap) CLI
302/// struct to validate and parse args into a baud rate.
303pub fn valid_baud_rate(s: &str) -> Result<u32, String> {
304    let baud: u32 = s
305        .parse()
306        .map_err(|_| format!("`{s}` isn't a valid baud rate"))?;
307    if serial2_tokio::COMMON_BAUD_RATES.contains(&baud) {
308        Ok(baud)
309    } else {
310        Err(format!(
311            "'{}' is not a valid baud rate; valid baud rates include {:?}",
312            baud,
313            serial2_tokio::COMMON_BAUD_RATES
314        ))
315    }
316}
317
318/// Used as a [`value_parser`](https://docs.rs/clap/latest/clap/struct.Arg.html#method.value_parser) for [`sericom`](https://crates.io/crates/sericom)s [`clap`](https://docs.rs/clap) CLI
319/// struct to validate and parse args into a [`SeriColor`][`crate::configs::SeriColor`].
320pub fn color_parser(input: &str) -> Result<crate::configs::SeriColor, String> {
321    use crate::configs::{NORMALIZER, SeriColor};
322    match SeriColor::parse_from_str(input, NORMALIZER) {
323        Ok(c) => Ok(c),
324        Err(valid_colors) => Err(format!("\n\nExpected one of: {}", valid_colors.join(", "))),
325    }
326}
327
328fn ensure_terminal_cleanup(mut stdout: io::Stdout) {
329    use crossterm::{
330        cursor::Show,
331        execute,
332        terminal::{LeaveAlternateScreen, disable_raw_mode},
333    };
334    let _ = execute!(
335        stdout,
336        event::DisableMouseCapture,
337        event::DisableBracketedPaste,
338        LeaveAlternateScreen,
339        Show
340    );
341    let _ = disable_raw_mode();
342    let _ = stdout.flush();
343}
344
345fn run_file_exit_script(config: &'static crate::configs::Config, file_path: PathBuf) {
346    let span = tracing::span!(Level::DEBUG, "Exit script");
347    let _enter = span.enter();
348
349    let Some(script_path) = config.defaults.exit_script.as_ref() else {
350        return;
351    };
352    let full_file_path = file_path
353        .canonicalize()
354        .expect("All error conditions have been checked");
355    let cmd = create_platform_cmd(script_path, full_file_path);
356    if let Ok(output) = cmd {
357        let msg = format!(
358            "stdout: {}, stderr: {}",
359            String::from_utf8_lossy(&output.stdout),
360            String::from_utf8_lossy(&output.stderr)
361        );
362        tracing::debug!(msg);
363    }
364}
365
366fn create_platform_cmd(
367    script: &std::path::Path,
368    file_path: std::path::PathBuf,
369) -> Result<std::process::Output, io::Error> {
370    use std::process::Command;
371
372    #[cfg(unix)]
373    {
374        Command::new(script)
375            .env("SERICOM_OUT_FILE", file_path)
376            .output()
377    }
378
379    #[cfg(windows)]
380    {
381        let ext = script.extension().expect("Validated in initialization");
382        match ext
383            .to_ascii_lowercase()
384            .to_str()
385            .expect("Converted to ascii")
386        {
387            "ps1" => Command::new("powershell.exe")
388                .arg("-File")
389                .arg(script)
390                .env("SERICOM_OUT_FILE", file_path)
391                .output(),
392            _ => Command::new("cmd.exe")
393                .arg("/C")
394                .arg(script)
395                .env("SERICOM_OUT_FILE", file_path)
396                .output(),
397        }
398    }
399}