1use 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
29pub 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 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 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 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 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
110pub 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
139pub fn get_settings(baud: u32, port: &str) -> miette::Result<()> {
141 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
277pub 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
301pub 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
318pub 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}