From 17aa6f16af4f2a052d069f693f1f1b527925e304 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1niel=20Buga?= Date: Tue, 25 Mar 2025 15:34:36 +0100 Subject: [PATCH] Option to supply custom defmt formats (#818) * Add output format argument * Load defmt location data * Use user-provided defmt format * Changelog --- CHANGELOG.md | 1 + espflash/src/cli/mod.rs | 9 +- espflash/src/cli/monitor/mod.rs | 12 +- espflash/src/cli/monitor/parser/esp_defmt.rs | 117 +++++++++++++++---- 4 files changed, 111 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d04bbeb..0d0691b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `watchdog-reset` strategy to `--after` subcommand (#779) - Add `ROM` version of `read-flash` command (#812) - `espflash` can detect the log format automatically from ESP-HAL metadata. Reqires `esp-println` 0.14 (presumably, yet to be released) (#809) +- Add `--output-format` option to monitor (#818) ### Changed diff --git a/espflash/src/cli/mod.rs b/espflash/src/cli/mod.rs index 28a82ce..d850e4c 100644 --- a/espflash/src/cli/mod.rs +++ b/espflash/src/cli/mod.rs @@ -279,9 +279,16 @@ pub struct MonitorConfigArgs { /// Avoids restarting the device before monitoring #[arg(long, requires = "non_interactive")] no_reset: bool, - /// Logging format. + /// The encoding of the target's serial output. #[arg(long, short = 'L')] log_format: Option, + /// The format of the printed defmt messages. + /// + /// You can also use one of two presets: oneline (default) and full. + /// + /// See + #[arg(long, short = 'F')] + output_format: Option, /// External log processors to use (comma separated executables) #[arg(long)] processors: Option, diff --git a/espflash/src/cli/monitor/mod.rs b/espflash/src/cli/monitor/mod.rs index a86ac38..67b7b19 100644 --- a/espflash/src/cli/monitor/mod.rs +++ b/espflash/src/cli/monitor/mod.rs @@ -109,8 +109,16 @@ pub fn monitor( .log_format .unwrap_or_else(|| deduce_log_format(elf)) { - LogFormat::Defmt => Box::new(parser::esp_defmt::EspDefmt::new(elf)?), - LogFormat::Serial => Box::new(parser::serial::Serial), + LogFormat::Defmt => Box::new(parser::esp_defmt::EspDefmt::new( + elf, + monitor_args.output_format, + )?), + LogFormat::Serial => { + if monitor_args.output_format.is_some() { + warn!("Output format specified but log format is serial. Ignoring output format."); + } + Box::new(parser::serial::Serial) + } }; let mut external_processors = diff --git a/espflash/src/cli/monitor/parser/esp_defmt.rs b/espflash/src/cli/monitor/parser/esp_defmt.rs index 073b15e..e344026 100644 --- a/espflash/src/cli/monitor/parser/esp_defmt.rs +++ b/espflash/src/cli/monitor/parser/esp_defmt.rs @@ -1,8 +1,13 @@ use std::io::Write; use crossterm::{style::Print, QueueableCommand}; -use defmt_decoder::{Frame, Table}; -use miette::{bail, Context, Diagnostic, Result}; +use defmt_decoder::{ + log::format::{Formatter, FormatterConfig, FormatterFormat}, + Frame, + Table, +}; +use log::warn; +use miette::{bail, ensure, Context, Diagnostic, Result}; use thiserror::Error; use crate::cli::monitor::parser::InputParser; @@ -22,9 +27,13 @@ pub enum DefmtError { NoDefmtData, #[error("Failed to parse defmt data")] - #[diagnostic(code(espflash::monitor::defmt::parse_failed))] + #[diagnostic(code(espflash::monitor::defmt::table_parse_failed))] TableParseFailed, + #[error("Failed to parse defmt location data")] + #[diagnostic(code(espflash::monitor::defmt::location_parse_failed))] + LocationDataParseFailed, + #[error("Unsupported defmt encoding: {0:?}. Only rzcobs is supported.")] #[diagnostic(code(espflash::monitor::defmt::unsupported_encoding))] UnsupportedEncoding(defmt_decoder::Encoding), @@ -101,16 +110,16 @@ impl FrameDelimiter { } } -#[derive(Debug)] -pub struct EspDefmt { - delimiter: FrameDelimiter, +struct DefmtData { table: Table, + locs: Option, + formatter: Formatter, } -impl EspDefmt { +impl DefmtData { /// Loads symbols from the ELF file (if provided) and initializes the /// context. - fn load_table(elf: Option<&[u8]>) -> Result { + fn load(elf: Option<&[u8]>, output_format: Option) -> Result { let Some(elf) = elf else { bail!(DefmtError::NoElf); }; @@ -125,35 +134,93 @@ impl EspDefmt { // 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 { - Ok(table) - } else { - bail!(DefmtError::UnsupportedEncoding(encoding)) - } - } + ensure!( + encoding == defmt_decoder::Encoding::Rzcobs, + DefmtError::UnsupportedEncoding(encoding) + ); - pub fn new(elf: Option<&[u8]>) -> Result { - Self::load_table(elf).map(|table| Self { - delimiter: FrameDelimiter::new(), + let locs = table + .get_locations(elf) + .map_err(|_e| DefmtError::LocationDataParseFailed)?; + + let locs = if !table.is_empty() && locs.is_empty() { + warn!("Insufficient DWARF info; compile your program with `debug = 2` to enable location info."); + None + } else if table.indices().all(|idx| locs.contains_key(&(idx as u64))) { + Some(locs) + } else { + warn!("Location info is incomplete; it will be omitted from the output."); + None + }; + + let show_location = locs.is_some(); + let has_timestamp = table.has_timestamp(); + + let format = match output_format.as_deref() { + None | Some("oneline") => FormatterFormat::OneLine { + with_location: show_location, + }, + Some("full") => FormatterFormat::Default { + with_location: show_location, + }, + Some(format) => FormatterFormat::Custom(format), + }; + + Ok(Self { table, + locs, + formatter: Formatter::new(FormatterConfig { + format, + is_timestamp_available: has_timestamp, + }), }) } - fn handle_raw(bytes: &[u8], out: &mut dyn Write) { - out.write_all(bytes).unwrap(); - } + fn print(&self, frame: Frame<'_>, out: &mut dyn Write) { + let loc = self.locs.as_ref().and_then(|locs| locs.get(&frame.index())); + let (file, line, module) = if let Some(loc) = loc { + ( + Some(loc.file.display().to_string()), + Some(loc.line.try_into().unwrap()), + Some(loc.module.as_str()), + ) + } else { + (None, None, None) + }; + let s = self + .formatter + .format_frame(frame, file.as_deref(), line, module); - fn handle_defmt(frame: Frame<'_>, out: &mut dyn Write) { - out.queue(Print(frame.display(true).to_string())).unwrap(); + out.queue(Print(s)).unwrap(); out.queue(Print("\r\n")).unwrap(); out.flush().unwrap(); } } +pub struct EspDefmt { + delimiter: FrameDelimiter, + defmt_data: DefmtData, +} + +impl std::fmt::Debug for EspDefmt { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("EspDefmt").finish() + } +} + +impl EspDefmt { + pub fn new(elf: Option<&[u8]>, output_format: Option) -> Result { + DefmtData::load(elf, output_format).map(|defmt_data| Self { + delimiter: FrameDelimiter::new(), + defmt_data, + }) + } +} + impl InputParser for EspDefmt { fn feed(&mut self, bytes: &[u8], out: &mut dyn Write) { - let mut decoder = self.table.new_stream_decoder(); + let mut decoder = self.defmt_data.table.new_stream_decoder(); self.delimiter.feed(bytes, |frame| match frame { FrameKind::Defmt(frame) => { @@ -162,12 +229,12 @@ impl InputParser for EspDefmt { decoder.received(FRAME_END); if let Ok(frame) = decoder.decode() { - Self::handle_defmt(frame, out); + self.defmt_data.print(frame, out); } else { log::warn!("Failed to decode defmt frame"); } } - FrameKind::Raw(bytes) => Self::handle_raw(bytes, out), + FrameKind::Raw(bytes) => out.write_all(bytes).unwrap(), }); } }