diff --git a/ravedude/README.md b/ravedude/README.md index b1dc564a4d..e081f35e35 100644 --- a/ravedude/README.md +++ b/ravedude/README.md @@ -132,6 +132,22 @@ port = "/dev/ttyACM0" # ravedude should open a serial console after flashing open-console = true +# console output mode. Can be ascii, hex, dec or bin +output-mode = "ascii" + +# Print a newline after this byte +# not used with output_mode ascii +# hex (0x) and bin (0b) notations are supported. +# matching chars/bytes are NOT removed +# to add newlines after \n (in non-ascii mode), use \n, 0x0a or 0b00001010 +# newline-on = '\n' + +# Print a newline after n bytes +# not used with output_mode ascii +# defaults to 16 for hex and dec and 8 for bin +# if dividable by 4, bytes will be grouped to 4 +# newline-after = 16 + # baudrate for the serial console (this is **not** the avrdude flashing baudrate) serial-baudrate = 57600 diff --git a/ravedude/src/config.rs b/ravedude/src/config.rs index 8e688f22ff..eed8f84f35 100644 --- a/ravedude/src/config.rs +++ b/ravedude/src/config.rs @@ -1,5 +1,6 @@ +use anyhow::Context as _; use serde::{Deserialize, Serialize}; -use std::num::NonZeroU32; +use std::{num::NonZeroU32, str::FromStr}; #[derive(serde::Serialize, serde::Deserialize, Debug, Default)] #[serde(rename_all = "kebab-case")] @@ -48,12 +49,42 @@ impl RavedudeConfig { port: args.port.clone(), reset_delay: args.reset_delay, board: args.legacy_board_name().clone(), + output_mode: args.output_mode.unwrap_or_default(), + newline_after: None, + newline_on: None, }, board_config: Default::default(), }) } } +impl RavedudeGeneralConfig { + pub fn newline_mode(&self) -> anyhow::Result { + if self.output_mode == OutputMode::Ascii { + if self.newline_on.is_some() || self.newline_on.is_some() { + anyhow::bail!( + "newline_on and newline_after cannot be used with output_mode = \"ascii\"" + ) + } + + return Ok(NewlineMode::Off); + } + + Ok(match (self.newline_on.as_ref(), self.newline_after) { + (Some(_), Some(_)) => { + anyhow::bail!("newline_on and newline_after cannot be used at the same time") + } + (Some(on_str), None) => NewlineMode::On(parse_newline_on(on_str)?), + (None, Some(after)) => NewlineMode::After(after), + (None, None) => NewlineMode::After(match self.output_mode { + OutputMode::Hex | OutputMode::Dec => 16, + OutputMode::Bin => 8, + OutputMode::Ascii => unreachable!(), + }), + }) + } +} + #[derive(serde::Serialize, serde::Deserialize, Debug, Default)] #[serde(rename_all = "kebab-case")] pub struct RavedudeGeneralConfig { @@ -63,6 +94,10 @@ pub struct RavedudeGeneralConfig { pub port: Option, pub reset_delay: Option, pub board: Option, + #[serde(default)] + pub output_mode: OutputMode, + pub newline_on: Option, + pub newline_after: Option, } impl RavedudeGeneralConfig { @@ -83,6 +118,9 @@ impl RavedudeGeneralConfig { if let Some(reset_delay) = args.reset_delay { self.reset_delay = Some(reset_delay); } + if let Some(output_mode) = args.output_mode { + self.output_mode = output_mode; + } Ok(()) } } @@ -181,3 +219,87 @@ impl BoardConfig { } } } + +#[derive(Debug, Clone, Copy, Deserialize, Serialize, Default, PartialEq)] +pub enum OutputMode { + #[default] + Ascii, + Hex, + Dec, + Bin, +} + +impl FromStr for OutputMode { + type Err = anyhow::Error; + fn from_str(s: &str) -> Result { + match s { + "ascii" => Ok(Self::Ascii), + "hex" => Ok(Self::Hex), + "dec" => Ok(Self::Dec), + "bin" => Ok(Self::Bin), + _ => Err(anyhow::anyhow!("unknown output mode")), + } + } +} + +#[derive(Copy, Clone)] +pub enum NewlineMode { + /// Break lines when encountering this byte + On(u8), + /// Break lines after this many bytes + After(u8), + Off, +} + +impl NewlineMode { + pub fn space_after(&self) -> Option { + if let NewlineMode::After(bytes) = self { + if bytes % 4 == 0 { + return Some(4); + } + }; + None + } +} + +fn parse_newline_on(s: &str) -> Result { + if let Ok(c) = s.parse::() { + return u8::try_from(c).context("non-byte character in `newline-on`"); + } + + // if it starts with 0x then parse the hex byte + if &s[0..2] == "0x" { + if s.len() != 4 { + anyhow::bail!("hex byte must have 2 digits"); + } + return u8::from_str_radix(&s[2..4], 16).context("invalid hex byte"); + } + + // if it starts with 0b then parse the binary byte + if &s[0..2] == "0b" { + if s.len() != 10 { + anyhow::bail!("binary byte must have 8 digits"); + } + return u8::from_str_radix(&s[2..10], 2).context("invalid binary byte"); + } + + anyhow::bail!("must be a single character or a byte in hex or binary notation"); +} + +#[cfg(test)] +mod tests { + use super::parse_newline_on; + + #[test] + fn test_parse_newline_on() { + assert_eq!(parse_newline_on("a").unwrap(), 'a' as u8); + assert_eq!(parse_newline_on("\n").unwrap(), '\n' as u8); + assert_eq!(parse_newline_on("0x41").unwrap(), 0x41); + assert_eq!(parse_newline_on("0b01000001").unwrap(), 0b01000001); + assert!(parse_newline_on("not a char").is_err()); + assert!(parse_newline_on("0x").is_err()); + assert!(parse_newline_on("0xzz").is_err()); + assert!(parse_newline_on("0b").is_err()); + assert!(parse_newline_on("0b0a0a0a0a").is_err()); + } +} diff --git a/ravedude/src/console.rs b/ravedude/src/console.rs index 221e47d4c0..33374d54e1 100644 --- a/ravedude/src/console.rs +++ b/ravedude/src/console.rs @@ -1,8 +1,19 @@ -use anyhow::Context as _; use std::io::Read as _; use std::io::Write as _; -pub fn open(port: &std::path::Path, baudrate: u32) -> anyhow::Result<()> { +use anyhow::Context as _; + +use crate::config::NewlineMode; +use crate::config::OutputMode; +use crate::config::OutputMode::*; + +pub fn open( + port: &std::path::PathBuf, + baudrate: u32, + output_mode: OutputMode, + newline_mode: NewlineMode, + space_after: Option, +) -> anyhow::Result<()> { let mut rx = serialport::new(port.to_string_lossy(), baudrate) .timeout(std::time::Duration::from_secs(2)) .open_native() @@ -14,12 +25,14 @@ pub fn open(port: &std::path::Path, baudrate: u32) -> anyhow::Result<()> { // Set a CTRL+C handler to terminate cleanly instead of with an error. ctrlc::set_handler(move || { - eprintln!(""); + eprintln!(); eprintln!("Exiting."); std::process::exit(0); }) .context("failed setting a CTRL+C handler")?; + let mut byte_count = 0; + // Spawn a thread for the receiving end because stdio is not portably non-blocking... std::thread::spawn(move || loop { #[cfg(not(target_os = "windows"))] @@ -41,7 +54,38 @@ pub fn open(port: &std::path::Path, baudrate: u32) -> anyhow::Result<()> { } } } - stdout.write(&buf[..count]).unwrap(); + if output_mode == Ascii { + stdout.write(&buf[..count]).unwrap(); + } else { + for byte in &buf[..count] { + byte_count += 1; + match output_mode { + Ascii => unreachable!(), + Hex => write!(stdout, "{:02x} ", byte).unwrap(), + Dec => write!(stdout, "{:03} ", byte).unwrap(), + Bin => write!(stdout, "{:08b} ", byte).unwrap(), + } + + if let Some(space_after) = space_after { + if byte_count % space_after == 0 { + write!(stdout, " ").unwrap(); + } + } + match newline_mode { + NewlineMode::On(newline_on) => { + if *byte == newline_on { + writeln!(stdout).unwrap() + } + } + NewlineMode::After(newline_after) => { + if byte_count % newline_after == 0 { + writeln!(stdout).unwrap(); + } + } + NewlineMode::Off => {} + } + } + } stdout.flush().unwrap(); } Err(e) => { diff --git a/ravedude/src/main.rs b/ravedude/src/main.rs index 8fe1c4c9b3..2000f39a89 100644 --- a/ravedude/src/main.rs +++ b/ravedude/src/main.rs @@ -93,6 +93,7 @@ //! For reference, take a look at [`boards.toml`](https://github.com/Rahix/avr-hal/blob/main/ravedude/src/boards.toml). use anyhow::Context as _; use colored::Colorize as _; +use config::OutputMode; use std::path::Path; use std::thread; @@ -159,7 +160,13 @@ struct Args { /// Should not be used in newer configurations. #[clap(name = "LEGACY BINARY", value_parser)] bin_legacy: Option, + + /// Output mode. + /// Can be ascii, hex, dec or bin + #[clap(short = 'o', long = "output-mode")] + output_mode: Option, } + impl Args { /// Get the board name for legacy configurations. /// `None` if the configuration isn't a legacy configuration or the board name doesn't exist. @@ -257,6 +264,12 @@ fn ravedude() -> anyhow::Result<()> { anyhow::bail!("no named board given and no board options provided"); }; + if ravedude_config.general_options.newline_on.is_some() + && ravedude_config.general_options.newline_after.is_some() + { + anyhow::bail!("newline_on and newline_after cannot be used at the same time"); + } + let board_avrdude_options = board .avrdude .take() @@ -269,7 +282,7 @@ fn ravedude() -> anyhow::Result<()> { ); let port = match ravedude_config.general_options.port { - Some(port) => Ok(Some(port)), + Some(ref port) => Ok(Some(port.clone())), None => match board.guess_port() { Some(Ok(port)) => Ok(Some(port)), p @ Some(Err(_)) => p.transpose().context( @@ -335,12 +348,19 @@ fn ravedude() -> anyhow::Result<()> { })?; let port = port.context("console can only be opened for devices with USB-to-Serial")?; + let newline_mode = ravedude_config.general_options.newline_mode()?; task_message!("Console", "{} at {} baud", port.display(), baudrate); task_message!("", "{}", "CTRL+C to exit.".dimmed()); // Empty line for visual consistency eprintln!(); - console::open(&port, baudrate.get())?; + console::open( + &port, + baudrate.get(), + ravedude_config.general_options.output_mode, + newline_mode, + newline_mode.space_after(), + )?; } else if args.bin.is_none() && port.is_some() { warning!("you probably meant to add -c/--open-console?"); }