Option to supply custom defmt formats (#818)

* Add output format argument

* Load defmt location data

* Use user-provided defmt format

* Changelog
This commit is contained in:
Dániel Buga
2025-03-25 15:34:36 +01:00
committed by GitHub
parent b028c5295e
commit 17aa6f16af
4 changed files with 111 additions and 28 deletions

View File

@@ -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

View File

@@ -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<LogFormat>,
/// The format of the printed defmt messages.
///
/// You can also use one of two presets: oneline (default) and full.
///
/// See <https://defmt.ferrous-systems.com/custom-log-output>
#[arg(long, short = 'F')]
output_format: Option<String>,
/// External log processors to use (comma separated executables)
#[arg(long)]
processors: Option<String>,

View File

@@ -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 =

View File

@@ -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<defmt_decoder::Locations>,
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<Table> {
fn load(elf: Option<&[u8]>, output_format: Option<String>) -> Result<Self> {
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> {
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<String>) -> Result<Self> {
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(),
});
}
}