Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

decoder: improve default format #848

Merged
merged 10 commits into from
Jul 30, 2024
Merged
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- [#857]: Add an octal display hint (`:o`)
- [#855]: `defmt-print`: Now uses tokio to make tcp and stdin reads async (in preparation for a `watch elf` flag)
- [#852]: `CI`: Update mdbook to v0.4.40
- [#848]: `decoder`: add optional one-line format
- [#847]: `decoder`: Fix log format width specifier not working as expected
- [#845]: `decoder`: fix println!() records being printed with formatting
- [#843]: `defmt`: Sort IDs of log msgs by severity to allow runtime filtering by severity
Expand All @@ -22,6 +23,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
[#857]: https://github.com/knurling-rs/defmt/pull/857
[#855]: https://github.com/knurling-rs/defmt/pull/855
[#852]: https://github.com/knurling-rs/defmt/pull/852
[#848]: https://github.com/knurling-rs/defmt/pull/848
[#847]: https://github.com/knurling-rs/defmt/pull/847
[#845]: https://github.com/knurling-rs/defmt/pull/845
[#843]: https://github.com/knurling-rs/defmt/pull/843
Expand Down
10 changes: 6 additions & 4 deletions decoder/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
//! Decodes [`defmt`](https://github.com/knurling-rs/defmt) log frames
//!
//! NOTE: The decoder always runs on the host!
//!
//! This is an implementation detail of [`probe-run`](https://github.com/knurling-rs/probe-run) and
//! not meant to be consumed by other tools at the moment so all the API is unstable.

#![cfg(feature = "unstable")]
#![cfg_attr(docsrs, feature(doc_cfg))]
Expand Down Expand Up @@ -124,10 +121,13 @@ struct BitflagsKey {
crate_name: Option<String>,
}

/// How a defmt frame is encoded
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum Encoding {
/// raw data, that is no encoding.
Raw,
/// [Reverse Zero-compressing COBS encoding](https://github.com/Dirbaio/rzcobs)
Rzcobs,
}

Expand All @@ -144,6 +144,7 @@ impl FromStr for Encoding {
}

impl Encoding {
/// Can this encoding recover from missed bytes?
pub const fn can_recover(&self) -> bool {
match self {
Encoding::Raw => false,
Expand Down Expand Up @@ -322,11 +323,12 @@ struct FormatSliceElement<'t> {
args: Vec<Arg<'t>>,
}

/// Ways in which decoding a defmt frame can fail.
#[derive(Debug, Eq, PartialEq)]
pub enum DecodeError {
/// More data is needed to decode the next frame.
UnexpectedEof,

/// The frame was not in the expected format.
Malformed,
}

Expand Down
134 changes: 122 additions & 12 deletions decoder/src/log/format/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,17 +186,20 @@ impl LogSegment {
}
}

/// A formatter for microcontroller-generated frames
pub struct Formatter {
formatter: InternalFormatter,
}

impl Formatter {
/// Create a new formatter, using the given configuration.
pub fn new(config: FormatterConfig) -> Self {
Self {
formatter: InternalFormatter::new(config, Source::Defmt),
}
}

/// Format a defmt frame using this formatter.
pub fn format_frame<'a>(
&self,
frame: Frame<'a>,
Expand Down Expand Up @@ -227,22 +230,26 @@ impl Formatter {
}
}

/// Format the given [`DefmtRecord`] (which is an internal type).
pub(super) fn format(&self, record: &DefmtRecord) -> String {
self.formatter.format(&Record::Defmt(record))
}
}

/// A formatter for host-generated frames
pub struct HostFormatter {
formatter: InternalFormatter,
}

impl HostFormatter {
/// Create a new host formatter using the given config
pub fn new(config: FormatterConfig) -> Self {
Self {
formatter: InternalFormatter::new(config, Source::Host),
}
}

/// Format the given [`log::Record`].
pub fn format(&self, record: &LogRecord) -> String {
self.formatter.format(&Record::Host(record))
}
Expand All @@ -265,11 +272,53 @@ enum Record<'a> {
}

#[derive(Debug)]
#[non_exhaustive]
pub enum FormatterFormat<'a> {
Default { with_location: bool },
/// The classic defmt two-line format.
///
/// Looks like:
///
/// ```text
/// INFO This is a log message
/// └─ test_lib::hello @ /Users/jonathan/Documents/knurling/test-lib/src/lib.rs:8
/// ```
Default {
with_location: bool,
},
/// A one-line format.
///
/// Looks like:
///
/// ```text
/// [INFO ] This is a log message (crate_name test-lib/src/lib.rs:8)
/// ```
OneLine {
with_location: bool,
},
Custom(&'a str),
}

impl FormatterFormat<'static> {
/// Parse a string into a choice of [`FormatterFormat`].
///
/// Unknown strings return `None`.
pub fn from_string(s: &str, with_location: bool) -> Option<FormatterFormat<'static>> {
match s {
"default" => Some(FormatterFormat::Default { with_location }),
"oneline" => Some(FormatterFormat::OneLine { with_location }),
_ => None,
}
}

/// Get a list of valid string names for the various format options.
///
/// This will *not* include an entry for [`FormatterFormat::Custom`] because
/// that requires a format string argument.
pub fn get_options() -> impl Iterator<Item = &'static str> {
["default", "oneline"].iter().cloned()
}
}

impl Default for FormatterFormat<'_> {
fn default() -> Self {
FormatterFormat::Default {
Expand All @@ -278,29 +327,89 @@ impl Default for FormatterFormat<'_> {
}
}

/// Describes one of the fixed format string sets.
trait Style {
const FORMAT: &'static str;
const FORMAT_WITH_TS: &'static str;
const FORMAT_WITH_LOC: &'static str;
const FORMAT_WITH_TS_LOC: &'static str;

/// Return a suitable format string, given these options.
fn get_string(with_location: bool, has_timestamp: bool) -> &'static str {
match (with_location, has_timestamp) {
(false, false) => Self::FORMAT,
(false, true) => Self::FORMAT_WITH_TS,
(true, false) => Self::FORMAT_WITH_LOC,
(true, true) => Self::FORMAT_WITH_TS_LOC,
}
}
}

/// Implements the `FormatterFormat::Default` style.
struct DefaultStyle;

impl Style for DefaultStyle {
const FORMAT: &'static str = "{L} {s}";
const FORMAT_WITH_LOC: &'static str = "{L} {s}\n└─ {m} @ {F}:{l}";
const FORMAT_WITH_TS: &'static str = "{t} {L} {s}";
const FORMAT_WITH_TS_LOC: &'static str = "{t} {L} {s}\n└─ {m} @ {F}:{l}";
}

/// Implements the `FormatterFormat::OneLine` style.
struct OneLineStyle;

impl Style for OneLineStyle {
const FORMAT: &'static str = "{[{L}]%bold} {s}";
const FORMAT_WITH_LOC: &'static str = "{[{L}]%bold} {s} {({c:bold} {fff}:{l:1})%dimmed}";
const FORMAT_WITH_TS: &'static str = "{t} {[{L}]%bold} {s}";
const FORMAT_WITH_TS_LOC: &'static str = "{t} {[{L}]%bold} {s} {({c:bold} {fff}:{l:1})%dimmed}";
}

/// Settings that control how defmt frames should be formatted.
#[derive(Debug, Default)]
pub struct FormatterConfig<'a> {
/// The format to use
pub format: FormatterFormat<'a>,
/// If `true`, then the logs should include a timestamp.
///
/// Not all targets can supply a timestamp, and if not, it should be
/// omitted.
pub is_timestamp_available: bool,
}

impl<'a> FormatterConfig<'a> {
/// Create a new custom formatter config.
///
/// This allows the user to supply a custom log-format string. See the
/// "Custom Log Output" section of the defmt book for details of the format.
pub fn custom(format: &'a str) -> Self {
FormatterConfig {
format: FormatterFormat::Custom(format),
is_timestamp_available: false,
}
}

/// Modify a formatter configuration, setting the 'timestamp available' flag
/// to true.
pub fn with_timestamp(mut self) -> Self {
self.is_timestamp_available = true;
self
}

/// Modify a formatter configuration, setting the 'with_location' flag
/// to true.
///
/// Do not use this with a custom log formatter.
pub fn with_location(mut self) -> Self {
// TODO: Should we warn the user that trying to set a location
// for a custom format won't work?
match self.format {
FormatterFormat::OneLine { with_location: _ } => {
self.format = FormatterFormat::OneLine {
with_location: true,
};
self
}
FormatterFormat::Default { with_location: _ } => {
self.format = FormatterFormat::Default {
with_location: true,
Expand All @@ -314,20 +423,21 @@ impl<'a> FormatterConfig<'a> {

impl InternalFormatter {
fn new(config: FormatterConfig, source: Source) -> Self {
const FORMAT: &str = "{L} {s}";
const FORMAT_WITH_LOCATION: &str = "{L} {s}\n└─ {m} @ {F}:{l}";
const FORMAT_WITH_TIMESTAMP: &str = "{t} {L} {s}";
const FORMAT_WITH_TIMESTAMP_AND_LOCATION: &str = "{t} {L} {s}\n└─ {m} @ {F}:{l}";

let format = match config.format {
FormatterFormat::Default { with_location } => {
let mut format = match (with_location, config.is_timestamp_available) {
(false, false) => FORMAT,
(false, true) => FORMAT_WITH_TIMESTAMP,
(true, false) => FORMAT_WITH_LOCATION,
(true, true) => FORMAT_WITH_TIMESTAMP_AND_LOCATION,
let mut format =
DefaultStyle::get_string(with_location, config.is_timestamp_available)
.to_string();
if source == Source::Host {
format.insert_str(0, "(HOST) ");
}
.to_string();

format
}
FormatterFormat::OneLine { with_location } => {
let mut format =
OneLineStyle::get_string(with_location, config.is_timestamp_available)
.to_string();

if source == Source::Host {
format.insert_str(0, "(HOST) ");
Expand Down
4 changes: 3 additions & 1 deletion decoder/src/log/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
//! This module provides interoperability utilities between [`defmt`] and the [`log`] crate.
//! Handles the conversion of defmt frames to log messages (as strings).
//!
//! Also provides interoperability utilities between [`defmt`] and the [`log`] crate.
//!
//! If you are implementing a custom defmt decoding tool, this module can make it easier to
//! integrate it with logs produced with the [`log`] crate.
Expand Down
Loading