diff --git a/espflash/src/cli/monitor/mod.rs b/espflash/src/cli/monitor/mod.rs index 21f6e650..946d77c7 100644 --- a/espflash/src/cli/monitor/mod.rs +++ b/espflash/src/cli/monitor/mod.rs @@ -17,79 +17,21 @@ use std::{ use crossterm::{ event::{poll, read, Event, KeyCode, KeyEvent, KeyModifiers}, - style::{Color, Print, PrintStyledContent, Stylize}, terminal::{disable_raw_mode, enable_raw_mode}, - QueueableCommand, }; -use defmt_decoder::{Frame, StreamDecoder, Table}; -use lazy_static::lazy_static; use log::error; use miette::{IntoDiagnostic, Result}; -use regex::Regex; -use self::{line_endings::normalized, symbols::Symbols}; -use crate::{connection::reset_after_flash, interface::Interface}; +use crate::{ + cli::monitor::parser::{esp_defmt::EspDefmt, InputParser, ResolvingPrinter}, + connection::reset_after_flash, + interface::Interface, +}; mod line_endings; +mod parser; mod symbols; -// Pattern to much a function address in serial output. -lazy_static! { - static ref RE_FN_ADDR: Regex = Regex::new(r"0x[[:xdigit:]]{8}").unwrap(); -} - -/// Context for the serial monitor -#[derive(Default)] -struct SerialContext<'ctx> { - symbols: Option>, - previous_frag: Option, - previous_line: Option, - incomplete_utf8_buffer: Vec, -} - -impl<'ctx> SerialContext<'ctx> { - fn new(symbols: Option>) -> Self { - Self { - symbols, - ..Self::default() - } - } - - fn process_utf8(&mut self, buff: &[u8]) -> String { - let mut buffer = std::mem::take(&mut self.incomplete_utf8_buffer); - buffer.extend(normalized(buff.iter().copied())); - - // look for longest slice that we can then lossily convert without introducing errors for - // partial sequences (#457) - let mut len = 0; - - loop { - match std::str::from_utf8(&buffer[len..]) { - // whole input is valid - Ok(str) if len == 0 => return String::from(str), - - // input is valid after the last error, and we could ignore the last error, so - // let's process the whole input - Ok(_) => return String::from_utf8_lossy(&buffer).to_string(), - - // input has some errors. We can ignore invalid sequences and replace them later, - // but we have to stop if we encounter an incomplete sequence. - Err(e) => { - len += e.valid_up_to(); - if let Some(error_len) = e.error_len() { - len += error_len; - } else { - // incomplete sequence. We split it off, save it for later - let (bytes, incomplete) = buffer.split_at(len); - self.incomplete_utf8_buffer = incomplete.to_vec(); - return String::from_utf8_lossy(bytes).to_string(); - } - } - } - } - } -} - /// Type that ensures that raw mode is disabled when dropped. struct RawModeGuard; @@ -108,72 +50,23 @@ impl Drop for RawModeGuard { } } -enum FrameKind<'a> { - Defmt(Frame<'a>), - Raw(&'a [u8]), -} - -struct FrameDelimiter<'a> { - buffer: Vec, - decoder: Option>, - in_frame: bool, -} - -const FRAME_START: &[u8] = &[0xFF, 0x00]; -const FRAME_END: &[u8] = &[0x00]; - -fn search(haystack: &[u8], look_for_end: bool) -> Option<(&[u8], usize)> { - let needle = if look_for_end { FRAME_END } else { FRAME_START }; - let start = if look_for_end { - // skip leading zeros - haystack.iter().position(|&b| b != 0)? - } else { - 0 - }; - - let end = haystack[start..] - .windows(needle.len()) - .position(|window| window == needle)?; - - Some((&haystack[start..][..end], start + end + needle.len())) -} - -impl FrameDelimiter<'_> { - pub fn feed(&mut self, buffer: &[u8], mut process: impl FnMut(FrameKind<'_>)) { - let Some(table) = self.decoder.as_mut() else { - process(FrameKind::Raw(buffer)); - return; - }; - - self.buffer.extend_from_slice(buffer); - - while let Some((frame, consumed)) = search(&self.buffer, self.in_frame) { - if !self.in_frame { - process(FrameKind::Raw(frame)); - self.in_frame = true; - } else { - table.received(frame); - // small reliance on rzcobs internals: we need to feed the terminating zero - table.received(FRAME_END); - if let Ok(frame) = table.decode() { - process(FrameKind::Defmt(frame)); - } else { - log::warn!("Failed to decode defmt frame"); - } - self.in_frame = false; - }; - - self.buffer.drain(..consumed); - } - } -} - /// Open a serial monitor on the given interface pub fn monitor( + serial: Interface, + elf: Option<&[u8]>, + pid: u16, + baud: u32, +) -> serialport::Result<()> { + monitor_with(serial, elf, pid, baud, EspDefmt::new(elf)) +} + +/// Open a serial monitor on the given interface, using the given input parser. +pub fn monitor_with( mut serial: Interface, elf: Option<&[u8]>, pid: u16, baud: u32, + mut parser: L, ) -> serialport::Result<()> { println!("Commands:"); println!(" CTRL+R Reset chip"); @@ -187,49 +80,13 @@ pub fn monitor( .serial_port_mut() .set_timeout(Duration::from_millis(5))?; - // Load symbols from the ELF file (if provided) and initialize the context. - let (symbols, defmt_data) = if let Some(bytes) = elf { - ( - Symbols::try_from(bytes).ok(), - Table::parse(bytes).ok().flatten(), - ) - } else { - (None, None) - }; - let mut ctx = SerialContext::new(symbols); - // We are in raw mode until `_raw_mode` is dropped (ie. this function returns). let _raw_mode = RawModeGuard::new(); let stdout = stdout(); - let mut stdout = stdout.lock(); - - let defmt_encoding = defmt_data - .map(|table| { - let encoding = table.encoding(); - (table, encoding) - }) - .and_then(|(table, encoding)| { - // We only support rzcobs encoding because it is the only way to multiplex - // a defmt stream and an ASCII log stream over the same serial port. - if encoding == defmt_decoder::Encoding::Rzcobs { - Some((table, encoding)) - } else { - log::warn!("Unsupported defmt encoding: {:?}", encoding); - None - } - }); + let mut stdout = ResolvingPrinter::new(elf, stdout.lock()); let mut buff = [0; 1024]; - - let mut delimiter = FrameDelimiter { - buffer: Vec::new(), - decoder: defmt_encoding - .as_ref() - .map(|(table, _)| table.new_stream_decoder()), - in_frame: false, - }; - loop { let read_count = match serial.serial_port_mut().read(&mut buff) { Ok(count) => Ok(count), @@ -238,10 +95,7 @@ pub fn monitor( err => err, }?; - delimiter.feed(&buff[0..read_count], |frame| match frame { - FrameKind::Defmt(frame) => handle_defmt(&mut ctx, frame, &mut stdout), - FrameKind::Raw(bytes) => handle_serial(&mut ctx, bytes, &mut stdout), - }); + parser.feed(&buff[0..read_count], &mut stdout); // Don't forget to flush the writer! stdout.flush().ok(); @@ -270,117 +124,6 @@ pub fn monitor( Ok(()) } -fn handle_defmt(ctx: &mut SerialContext, frame: Frame<'_>, out: &mut dyn Write) { - let message = frame.display_message().to_string(); - - match frame.level() { - Some(level) => { - let color = match level { - defmt_parser::Level::Trace => Color::Cyan, - defmt_parser::Level::Debug => Color::Blue, - defmt_parser::Level::Info => Color::Green, - defmt_parser::Level::Warn => Color::Yellow, - defmt_parser::Level::Error => Color::Red, - }; - out.queue(PrintStyledContent( - format!("[{}] - {}", level.as_str().to_uppercase(), message.as_str()).with(color), - )) - .ok() - } - None => out.queue(Print(message.as_str())).ok(), - }; - - // Remember to begin a new line after we have printed this one! - out.write_all(b"\r\n").ok(); - - // If we have loaded some symbols... - if let Some(symbols) = &ctx.symbols { - for line in message.lines() { - resolve_addresses(symbols, line, out); - } - } -} - -/// Handles and writes the received serial data to the given output stream. -fn handle_serial(ctx: &mut SerialContext, buff: &[u8], out: &mut dyn Write) { - let text = ctx.process_utf8(buff); - - // Split the text into lines, storing the last of which separately if it is - // incomplete (ie. does not end with '\n') because these need special handling. - let mut lines = text.lines().collect::>(); - let incomplete = if text.ends_with('\n') { - None - } else { - lines.pop() - }; - - // Iterate through all *complete* lines (ie. those ending with '\n') ... - for line in lines { - // ... and print the line. - out.queue(Print(line)).ok(); - - // If there is a previous line fragment, that means that the current line must - // be appended to it in order to form the complete line. Since we want to look - // for function addresses in the *entire* previous line we combine these prior - // to performing the symbol lookup(s). - ctx.previous_line = if let Some(frag) = &ctx.previous_frag { - Some(format!("{frag}{line}")) - } else { - Some(line.to_string()) - }; - - // The previous fragment has been completed (by this current line). - ctx.previous_frag = None; - - // Remember to begin a new line after we have printed this one! - out.write_all(b"\r\n").ok(); - - // If we have loaded some symbols... - if let Some(symbols) = &ctx.symbols { - // And there was previously a line printed to the terminal... - if let Some(line) = &ctx.previous_line { - resolve_addresses(symbols, line, out); - } - } - } - - // If there is an incomplete line we will still print it. However, we will not - // perform function name lookups or terminate it with a newline. - if let Some(line) = incomplete { - out.queue(Print(line)).ok(); - - if let Some(frag) = &ctx.previous_frag { - ctx.previous_frag = Some(format!("{frag}{line}")); - } else { - ctx.previous_frag = Some(line.to_string()); - } - } -} - -fn resolve_addresses(symbols: &Symbols<'_>, line: &str, out: &mut dyn Write) { - // Check the previous line for function addresses. For each address found, - // attempt to look up the associated function's name and location and write both - // to the terminal. - for matched in RE_FN_ADDR.find_iter(line).map(|m| m.as_str()) { - // Since our regular expression already confirms that this is a correctly - // formatted hex literal, we can (fairly) safely assume that it will parse - // successfully into an integer. - let addr = parse_int::parse::(matched).unwrap(); - - let name = symbols.get_name(addr).unwrap_or_else(|| "??".into()); - let (file, line_num) = if let Some((file, line_num)) = symbols.get_location(addr) { - (file, line_num.to_string()) - } else { - ("??".into(), "??".into()) - }; - - out.queue(PrintStyledContent( - format!("{matched} - {name}\r\n at {file}:{line_num}\r\n").with(Color::Yellow), - )) - .unwrap(); - } -} - // Converts key events from crossterm into appropriate character/escape // sequences which are then sent over the serial connection. // @@ -435,75 +178,3 @@ fn handle_key_event(key_event: KeyEvent) -> Option> { key_str.map(|slice| slice.into()) } - -#[cfg(test)] -mod test { - #[test] - fn returns_valid_strings_immediately() { - let mut ctx = super::SerialContext::default(); - let buff = b"Hello, world!"; - let text = ctx.process_utf8(buff); - assert_eq!(text, "Hello, world!"); - } - - #[test] - fn does_not_repeat_valid_strings() { - let mut ctx = super::SerialContext::default(); - let text = ctx.process_utf8(b"Hello, world!"); - assert_eq!(text, "Hello, world!"); - let text = ctx.process_utf8(b"Something else"); - assert_eq!(text, "Something else"); - } - - #[test] - fn replaces_invalid_sequence() { - let mut ctx = super::SerialContext::default(); - let text = ctx.process_utf8(b"Hello, \xFF world!"); - assert_eq!(text, "Hello, \u{FFFD} world!"); - } - - #[test] - fn can_replace_unfinished_incomplete_sequence() { - let mut ctx = super::SerialContext::default(); - let mut incomplete = Vec::from("Hello, ".as_bytes()); - let utf8 = "🙈".as_bytes(); - incomplete.extend_from_slice(&utf8[..utf8.len() - 1]); - let text = ctx.process_utf8(&incomplete); - assert_eq!(text, "Hello, "); - - let text = ctx.process_utf8(b" world!"); - assert_eq!(text, "\u{FFFD} world!"); - } - - #[test] - fn can_merge_incomplete_sequence() { - let mut ctx = super::SerialContext::default(); - let mut incomplete = Vec::from("Hello, ".as_bytes()); - let utf8 = "🙈".as_bytes(); - incomplete.extend_from_slice(&utf8[..utf8.len() - 1]); - - let text = ctx.process_utf8(&incomplete); - assert_eq!(text, "Hello, "); - - let text = ctx.process_utf8(&utf8[utf8.len() - 1..]); - assert_eq!(text, "🙈"); - } - - #[test] - fn issue_457() { - let mut ctx = super::SerialContext::default(); - let mut result = String::new(); - - result.push_str(&ctx.process_utf8(&[0x48])); - result.push_str(&ctx.process_utf8(&[0x65, 0x6C, 0x6C])); - result.push_str(&ctx.process_utf8(&[ - 0x6F, 0x20, 0x77, 0x6F, 0x72, 0x6C, 0x64, 0x21, 0x20, 0x77, 0x69, 0x74, - ])); - result.push_str(&ctx.process_utf8(&[ - 0x68, 0x20, 0x55, 0x54, 0x46, 0x3A, 0x20, 0x77, 0x79, 0x73, 0x79, - ])); - result.push_str(&ctx.process_utf8(&[0xC5, 0x82, 0x61, 0x6D, 0x0A])); - - assert_eq!(result, "Hello world! with UTF: wysyłam\r\n"); - } -} diff --git a/espflash/src/cli/monitor/parser/esp_defmt.rs b/espflash/src/cli/monitor/parser/esp_defmt.rs new file mode 100644 index 00000000..33e295d0 --- /dev/null +++ b/espflash/src/cli/monitor/parser/esp_defmt.rs @@ -0,0 +1,140 @@ +use std::io::Write; + +use crossterm::{ + style::{Color, Print, PrintStyledContent, Stylize}, + QueueableCommand, +}; +use defmt_decoder::{Frame, Table}; + +use crate::cli::monitor::parser::InputParser; + +enum FrameKind<'a> { + Defmt(Frame<'a>), + Raw(&'a [u8]), +} + +struct FrameDelimiter { + buffer: Vec, + table: Option, + in_frame: bool, +} + +// Framing info added by esp-println +const FRAME_START: &[u8] = &[0xFF, 0x00]; +const FRAME_END: &[u8] = &[0x00]; + +fn search(haystack: &[u8], look_for_end: bool) -> Option<(&[u8], usize)> { + let needle = if look_for_end { FRAME_END } else { FRAME_START }; + let start = if look_for_end { + // skip leading zeros + haystack.iter().position(|&b| b != 0)? + } else { + 0 + }; + + let end = haystack[start..] + .windows(needle.len()) + .position(|window| window == needle)?; + + Some((&haystack[start..][..end], start + end + needle.len())) +} + +impl FrameDelimiter { + pub fn feed(&mut self, buffer: &[u8], mut process: impl FnMut(FrameKind<'_>)) { + let Some(table) = self.table.as_mut() else { + process(FrameKind::Raw(buffer)); + return; + }; + + let mut decoder = table.new_stream_decoder(); + + self.buffer.extend_from_slice(buffer); + + while let Some((frame, consumed)) = search(&self.buffer, self.in_frame) { + if !self.in_frame { + process(FrameKind::Raw(frame)); + self.in_frame = true; + } else { + decoder.received(frame); + // small reliance on rzcobs internals: we need to feed the terminating zero + decoder.received(FRAME_END); + if let Ok(frame) = decoder.decode() { + process(FrameKind::Defmt(frame)); + } else { + log::warn!("Failed to decode defmt frame"); + } + self.in_frame = false; + }; + + self.buffer.drain(..consumed); + } + } +} + +pub struct EspDefmt { + delimiter: FrameDelimiter, +} + +impl EspDefmt { + fn load_table(elf: Option<&[u8]>) -> Option
{ + // Load symbols from the ELF file (if provided) and initialize the context. + Table::parse(elf?).ok().flatten().and_then(|table| { + let encoding = table.encoding(); + + // We only support rzcobs encoding because it is the only way to multiplex + // a defmt stream and an ASCII log stream over the same serial port. + if encoding == defmt_decoder::Encoding::Rzcobs { + Some(table) + } else { + log::warn!("Unsupported defmt encoding: {:?}", encoding); + None + } + }) + } + + pub fn new(elf: Option<&[u8]>) -> Self { + Self { + delimiter: FrameDelimiter { + buffer: Vec::new(), + table: Self::load_table(elf), + in_frame: false, + }, + } + } +} + +impl InputParser for EspDefmt { + fn feed(&mut self, bytes: &[u8], out: &mut impl Write) { + self.delimiter.feed(bytes, |frame| match frame { + FrameKind::Defmt(frame) => { + match frame.level() { + Some(level) => { + let color = match level { + defmt_parser::Level::Trace => Color::Cyan, + defmt_parser::Level::Debug => Color::Blue, + defmt_parser::Level::Info => Color::Green, + defmt_parser::Level::Warn => Color::Yellow, + defmt_parser::Level::Error => Color::Red, + }; + out.queue(PrintStyledContent( + format!( + "[{}] - {}", + level.as_str().to_uppercase(), + frame.display_message() + ) + .with(color), + )) + .unwrap(); + } + None => { + out.queue(Print(frame.display_message())).unwrap(); + } + }; + + // Remember to begin a new line after we have printed this one! + out.write_all(b"\r\n").unwrap(); + } + FrameKind::Raw(bytes) => out.write_all(bytes).unwrap(), + }); + } +} diff --git a/espflash/src/cli/monitor/parser/mod.rs b/espflash/src/cli/monitor/parser/mod.rs new file mode 100644 index 00000000..1ae9c85a --- /dev/null +++ b/espflash/src/cli/monitor/parser/mod.rs @@ -0,0 +1,247 @@ +pub mod esp_defmt; +pub mod serial; + +use std::{borrow::Cow, io::Write}; + +use crossterm::{ + style::{Color, Print, PrintStyledContent, Stylize}, + QueueableCommand, +}; +use lazy_static::lazy_static; +use regex::Regex; + +use crate::cli::monitor::{line_endings::normalized, symbols::Symbols}; + +pub trait InputParser { + fn feed(&mut self, bytes: &[u8], out: &mut impl Write); +} + +// Pattern to much a function address in serial output. +lazy_static! { + static ref RE_FN_ADDR: Regex = Regex::new(r"0x[[:xdigit:]]{8}").unwrap(); +} + +fn resolve_addresses( + symbols: &Symbols<'_>, + line: &str, + out: &mut dyn Write, +) -> std::io::Result<()> { + // Check the previous line for function addresses. For each address found, + // attempt to look up the associated function's name and location and write both + // to the terminal. + for matched in RE_FN_ADDR.find_iter(line).map(|m| m.as_str()) { + // Since our regular expression already confirms that this is a correctly + // formatted hex literal, we can (fairly) safely assume that it will parse + // successfully into an integer. + let addr = parse_int::parse::(matched).unwrap(); + + let name = symbols.get_name(addr); + let location = symbols.get_location(addr); + + let name = name.as_deref().unwrap_or("??"); + let output = if let Some((file, line_num)) = location { + format!("{matched} - {name}\r\n at {file}:{line_num}\r\n") + } else { + format!("{matched} - {name}\r\n at ??:??\r\n") + }; + + out.queue(PrintStyledContent(output.with(Color::Yellow)))?; + } + + Ok(()) +} + +struct Utf8Merger { + incomplete_utf8_buffer: Vec, +} + +impl Utf8Merger { + fn new() -> Self { + Self { + incomplete_utf8_buffer: Vec::new(), + } + } + + fn process_utf8(&mut self, buff: &[u8]) -> String { + let mut buffer = std::mem::take(&mut self.incomplete_utf8_buffer); + buffer.extend(normalized(buff.iter().copied())); + + // look for longest slice that we can then lossily convert without introducing errors for + // partial sequences (#457) + let mut len = 0; + + loop { + match std::str::from_utf8(&buffer[len..]) { + // whole input is valid + Ok(str) if len == 0 => return String::from(str), + + // input is valid after the last error, and we could ignore the last error, so + // let's process the whole input + Ok(_) => return String::from_utf8_lossy(&buffer).to_string(), + + // input has some errors. We can ignore invalid sequences and replace them later, + // but we have to stop if we encounter an incomplete sequence. + Err(e) => { + len += e.valid_up_to(); + if let Some(error_len) = e.error_len() { + len += error_len; + } else { + // incomplete sequence. We split it off, save it for later + let (bytes, incomplete) = buffer.split_at(len); + self.incomplete_utf8_buffer = incomplete.to_vec(); + return String::from_utf8_lossy(bytes).to_string(); + } + } + } + } + } +} + +pub struct ResolvingPrinter<'ctx, W: Write> { + writer: W, + symbols: Option>, + merger: Utf8Merger, + line_fragment: String, +} + +impl<'ctx, W: Write> ResolvingPrinter<'ctx, W> { + pub fn new(elf: Option<&'ctx [u8]>, writer: W) -> Self { + Self { + writer, + symbols: elf.and_then(|elf| Symbols::try_from(elf).ok()), + merger: Utf8Merger::new(), + line_fragment: String::new(), + } + } +} + +impl Write for ResolvingPrinter<'_, W> { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let text = self.merger.process_utf8(buf); + + // Split the text into lines, storing the last of which separately if it is + // incomplete (ie. does not end with '\n') because these need special handling. + let mut lines = text.lines().collect::>(); + let incomplete = if text.ends_with('\n') { + None + } else { + lines.pop() + }; + + // Iterate through all *complete* lines (ie. those ending with '\n') ... + for line in lines { + // ... and print the line. + self.writer.queue(Print(line))?; + + // If there is a previous line fragment, that means that the current line must + // be appended to it in order to form the complete line. Since we want to look + // for function addresses in the *entire* previous line we combine these prior + // to performing the symbol lookup(s). + let fragment = std::mem::take(&mut self.line_fragment); + let line = if fragment.is_empty() { + Cow::from(line) + } else { + // The previous fragment has been completed (by this current line). + Cow::from(format!("{fragment}{line}")) + }; + + // Remember to begin a new line after we have printed this one! + self.writer.write_all(b"\r\n")?; + + // If we have loaded some symbols... + if let Some(symbols) = self.symbols.as_ref() { + // Try to print the names of addresses in the current line. + resolve_addresses(symbols, &line, &mut self.writer)?; + } + } + + // If there is an incomplete line we will still print it. However, we will not + // perform function name lookups or terminate it with a newline. + if let Some(line) = incomplete { + self.writer.queue(Print(line))?; + + let fragment = std::mem::take(&mut self.line_fragment); + self.line_fragment = format!("{fragment}{line}"); + } + + Ok(text.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.writer.flush() + } +} + +#[cfg(test)] +mod test { + use super::Utf8Merger; + + #[test] + fn returns_valid_strings_immediately() { + let mut ctx = Utf8Merger::new(); + let buff = b"Hello, world!"; + let text = ctx.process_utf8(buff); + assert_eq!(text, "Hello, world!"); + } + + #[test] + fn does_not_repeat_valid_strings() { + let mut ctx = Utf8Merger::new(); + let text = ctx.process_utf8(b"Hello, world!"); + assert_eq!(text, "Hello, world!"); + let text = ctx.process_utf8(b"Something else"); + assert_eq!(text, "Something else"); + } + + #[test] + fn replaces_invalid_sequence() { + let mut ctx = Utf8Merger::new(); + let text = ctx.process_utf8(b"Hello, \xFF world!"); + assert_eq!(text, "Hello, \u{FFFD} world!"); + } + + #[test] + fn can_replace_unfinished_incomplete_sequence() { + let mut ctx = Utf8Merger::new(); + let mut incomplete = Vec::from("Hello, ".as_bytes()); + let utf8 = "🙈".as_bytes(); + incomplete.extend_from_slice(&utf8[..utf8.len() - 1]); + let text = ctx.process_utf8(&incomplete); + assert_eq!(text, "Hello, "); + + let text = ctx.process_utf8(b" world!"); + assert_eq!(text, "\u{FFFD} world!"); + } + + #[test] + fn can_merge_incomplete_sequence() { + let mut ctx = Utf8Merger::new(); + let mut incomplete = Vec::from("Hello, ".as_bytes()); + let utf8 = "🙈".as_bytes(); + incomplete.extend_from_slice(&utf8[..utf8.len() - 1]); + + let text = ctx.process_utf8(&incomplete); + assert_eq!(text, "Hello, "); + + let text = ctx.process_utf8(&utf8[utf8.len() - 1..]); + assert_eq!(text, "🙈"); + } + + #[test] + fn issue_457() { + let mut ctx = Utf8Merger::new(); + let mut result = String::new(); + + result.push_str(&ctx.process_utf8(&[0x48])); + result.push_str(&ctx.process_utf8(&[0x65, 0x6C, 0x6C])); + result.push_str(&ctx.process_utf8(&[ + 0x6F, 0x20, 0x77, 0x6F, 0x72, 0x6C, 0x64, 0x21, 0x20, 0x77, 0x69, 0x74, + ])); + result.push_str(&ctx.process_utf8(&[ + 0x68, 0x20, 0x55, 0x54, 0x46, 0x3A, 0x20, 0x77, 0x79, 0x73, 0x79, + ])); + result.push_str(&ctx.process_utf8(&[0xC5, 0x82, 0x61, 0x6D, 0x0A])); + + assert_eq!(result, "Hello world! with UTF: wysyłam\r\n"); + } +} diff --git a/espflash/src/cli/monitor/parser/serial.rs b/espflash/src/cli/monitor/parser/serial.rs new file mode 100644 index 00000000..5a2adb05 --- /dev/null +++ b/espflash/src/cli/monitor/parser/serial.rs @@ -0,0 +1,11 @@ +use std::io::Write; + +use crate::cli::monitor::parser::InputParser; + +pub struct Serial; + +impl InputParser for Serial { + fn feed(&mut self, bytes: &[u8], out: &mut impl Write) { + out.write_all(bytes).unwrap(); + } +}