diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b6634fa..71c2a0e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,11 +36,15 @@ jobs: run: cargo fmt -- --check - name: Build | Clippy run: cargo clippy --features std,embedded-svc,edge-nal-embassy --examples --no-deps -- -Dwarnings + - name: Build | Clippy - defmt + run: cargo clippy --features std,embedded-svc,edge-nal-embassy,defmt --no-deps -- -Dwarnings + - name: Build | Clippy - log + run: cargo clippy --features std,embedded-svc,edge-nal-embassy,log --examples --no-deps -- -Dwarnings - name: Build | Default - run: cargo build + run: cargo build --features log - name: Build | Non-default run: cargo build --no-default-features - name: Build | Embassy - run: cargo build --no-default-features --features embassy + run: cargo build --no-default-features --features embassy,defmt - name: Build | Examples - run: cargo build --examples + run: cargo build --examples --features log diff --git a/Cargo.toml b/Cargo.toml index ff7de62..eb3ec2f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,8 @@ async-io-mini = ["std", "edge-nal-std/async-io-mini"] std = ["io", "edge-captive/std", "edge-dhcp/std", "edge-http/std", "edge-mdns/std", "edge-raw/std", "edge-mqtt", "edge-ws/std", "edge-nal-std"] embassy = ["io", "edge-nal-embassy"] io = ["edge-captive/io", "edge-dhcp/io", "edge-http/io", "edge-mdns/io", "edge-raw/io", "edge-ws/io", "edge-nal"] +log = ["edge-captive/log", "edge-dhcp/log", "edge-http/log", "edge-mdns/log", "edge-raw/log", "edge-ws/log", "edge-nal-embassy?/log"] +defmt = ["edge-captive/defmt", "edge-dhcp/defmt", "edge-http/defmt", "edge-mdns/defmt", "edge-raw/defmt", "edge-ws/defmt", "edge-nal-embassy?/defmt"] embedded-svc = ["edge-http/embedded-svc", "edge-mqtt/embedded-svc", "edge-ws/embedded-svc"] nightly = [] @@ -48,47 +50,47 @@ async-compat = "0.2" # For the `mqtt_client` example [[example]] name = "captive_portal" -required-features = ["std"] +required-features = ["std", "log"] [[example]] name = "dhcp_client" -required-features = ["std"] +required-features = ["std", "log"] [[example]] name = "dhcp_server" -required-features = ["std"] +required-features = ["std", "log"] [[example]] name = "http_client" -required-features = ["std"] +required-features = ["std", "log"] [[example]] name = "http_server" -required-features = ["std"] +required-features = ["std", "log"] [[example]] name = "mdns_responder" -required-features = ["std"] +required-features = ["std", "log"] [[example]] name = "mdns_service_responder" -required-features = ["std"] +required-features = ["std", "log"] [[example]] name = "ws_client" -required-features = ["std"] +required-features = ["std", "log"] [[example]] name = "ws_server" -required-features = ["std"] +required-features = ["std", "log"] [[example]] name = "nal_std" -required-features = ["std"] +required-features = ["std", "log"] [[example]] name = "mqtt_client" -required-features = ["std", "embedded-svc"] +required-features = ["std", "embedded-svc", "log"] [workspace] members = [ @@ -111,7 +113,6 @@ embassy-sync = { version = "0.6", default-features = false } embassy-time = { version = "0.4", default-features = false } embedded-io-async = { version = "0.6", default-features = false } embedded-svc = { version = "0.28", default-features = false } -log = { version = "0.4", default-features = false } heapless = { version = "0.8", default-features = false } domain = { version = "0.10", default-features = false, features = ["heapless"] } diff --git a/edge-captive/CHANGELOG.md b/edge-captive/CHANGELOG.md index fdaf091..d727720 100644 --- a/edge-captive/CHANGELOG.md +++ b/edge-captive/CHANGELOG.md @@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +* Optional `defmt` support via two new features (one has to specify one, or the other, or neither, but not both): + * `log` - uses the `log` crate for all logging + * `defmt` - uses the `defmt` crate for all logging, and implements `defmt::Format` for all library types that otherwise implement `Debug` and/or `Display` ## [0.5.0] - 2025-01-15 * Updated dependencies for compatibility with `embassy-time-driver` v0.2 diff --git a/edge-captive/Cargo.toml b/edge-captive/Cargo.toml index 2deb4f7..357876f 100644 --- a/edge-captive/Cargo.toml +++ b/edge-captive/Cargo.toml @@ -20,6 +20,7 @@ std = ["io"] io = ["edge-nal"] [dependencies] -log = { workspace = true } +log = { version = "0.4", default-features = false, optional = true } +defmt = { version = "0.3", optional = true } domain = { workspace = true } edge-nal = { workspace = true, optional = true } \ No newline at end of file diff --git a/edge-captive/src/fmt.rs b/edge-captive/src/fmt.rs new file mode 100644 index 0000000..8bd7963 --- /dev/null +++ b/edge-captive/src/fmt.rs @@ -0,0 +1,340 @@ +//! A module that re-exports the `log` macros if `defmt` is not enabled, or `defmt` macros if `defmt` is enabled. +//! +//! The module also defines: +//! - Custom versions of the core assert macros (`assert!`, `assert_eq!`, etc.) that use the `defmt` macros if the `defmt` feature is enabled. +//! - Custom versions of the `panic!`, `todo!`, and `unreachable!` macros that use the `defmt` macros if the `defmt` feature is enabled. +//! - A custom `unwrap!` macro that uses the `defmt` macros if the `defmt` feature is enabled, otherwise it uses the standard library's `unwrap` method. +//! - A custom `Bytes` struct that formats byte slices as hex in a way compatible with `defmt`. +#![macro_use] +#![allow(unused)] + +use core::fmt::{Debug, Display, LowerHex}; + +#[collapse_debuginfo(yes)] +macro_rules! assert { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! assert_eq { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert_eq!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert_eq!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! assert_ne { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert_ne!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert_ne!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! debug_assert { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! debug_assert_eq { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert_eq!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert_eq!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! debug_assert_ne { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert_ne!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert_ne!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! todo { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::todo!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::todo!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! unreachable { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::unreachable!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::unreachable!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! panic { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::panic!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::panic!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! trace { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::trace!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::trace!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! debug { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::debug!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::debug!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! info { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::info!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::info!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! warn { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::warn!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::warn!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! error { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::error!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::error!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[cfg(feature = "defmt")] +#[collapse_debuginfo(yes)] +macro_rules! unwrap { + ($($x:tt)*) => { + ::defmt::unwrap!($($x)*) + }; +} + +#[cfg(not(feature = "defmt"))] +#[collapse_debuginfo(yes)] +macro_rules! unwrap { + ($arg:expr) => { + match $crate::fmt::Try::into_result($arg) { + ::core::result::Result::Ok(t) => t, + ::core::result::Result::Err(e) => { + ::core::panic!("unwrap of `{}` failed: {:?}", ::core::stringify!($arg), e); + } + } + }; + ($arg:expr, $($msg:expr),+ $(,)? ) => { + match $crate::fmt::Try::into_result($arg) { + ::core::result::Result::Ok(t) => t, + ::core::result::Result::Err(e) => { + ::core::panic!("unwrap of `{}` failed: {}: {:?}", ::core::stringify!($arg), ::core::format_args!($($msg,)*), e); + } + } + } +} + +#[cfg(feature = "defmt")] +#[collapse_debuginfo(yes)] +macro_rules! write_unwrap { + ($f:expr, $s:literal $(, $x:expr)* $(,)?) => { + { + unwrap!(write!($f, $s $(, $x)*).map_err($crate::fmt::FmtError)); + } + }; +} + +#[cfg(not(feature = "defmt"))] +#[collapse_debuginfo(yes)] +macro_rules! write_unwrap { + ($f:expr, $s:literal $(, $x:expr)* $(,)?) => { + { + unwrap!(write!($f, $s $(, $x)*)); + } + }; +} + +#[cfg(feature = "defmt")] +#[collapse_debuginfo(yes)] +macro_rules! display2format { + ($arg:expr) => { + ::defmt::Display2Format(&$arg) + }; +} + +#[cfg(not(feature = "defmt"))] +#[collapse_debuginfo(yes)] +macro_rules! display2format { + ($arg:expr) => { + $arg + }; +} + +#[cfg(feature = "defmt")] +#[collapse_debuginfo(yes)] +macro_rules! debug2format { + ($arg:expr) => { + ::defmt::Debug2Format(&$arg) + }; +} + +#[cfg(not(feature = "defmt"))] +#[collapse_debuginfo(yes)] +macro_rules! debug2format { + ($arg:expr) => { + $arg + }; +} + +/// A way to `{:x?}` format a byte slice which is compatible with `defmt` +pub struct Bytes<'a>(pub &'a [u8]); + +impl Debug for Bytes<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#02x?}", self.0) + } +} + +impl Display for Bytes<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#02x?}", self.0) + } +} + +impl LowerHex for Bytes<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#02x?}", self.0) + } +} + +#[cfg(feature = "defmt")] +impl defmt::Format for Bytes<'_> { + fn format(&self, fmt: defmt::Formatter) { + defmt::write!(fmt, "{:02x}", self.0) + } +} + +/// Support for the `unwrap!` macro for `Option` and `Result`. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct NoneError; + +/// Support for the `unwrap!` macro for `Option` and `Result`. +pub trait Try { + type Ok; + type Error; + #[allow(unused)] + fn into_result(self) -> Result; +} + +impl Try for Option { + type Ok = T; + type Error = NoneError; + + #[inline] + fn into_result(self) -> Result { + self.ok_or(NoneError) + } +} + +impl Try for Result { + type Ok = T; + type Error = E; + + #[inline] + fn into_result(self) -> Self { + self + } +} + +#[cfg(feature = "defmt")] +pub(crate) struct FmtError(pub(crate) core::fmt::Error); + +#[cfg(feature = "defmt")] +impl defmt::Format for FmtError { + fn format(&self, f: defmt::Formatter<'_>) { + defmt::write!(f, "{}", "FmtError") + } +} diff --git a/edge-captive/src/io.rs b/edge-captive/src/io.rs index 487e3c3..a9ee00f 100644 --- a/edge-captive/src/io.rs +++ b/edge-captive/src/io.rs @@ -4,8 +4,6 @@ use core::time::Duration; use edge_nal::{UdpBind, UdpReceive, UdpSend}; -use log::*; - use super::*; pub const DEFAULT_SOCKET: SocketAddr = SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), PORT); @@ -50,6 +48,19 @@ where } } +#[cfg(feature = "defmt")] +impl defmt::Format for DnsIoError +where + E: defmt::Format, +{ + fn format(&self, f: defmt::Formatter<'_>) { + match self { + Self::DnsError(err) => defmt::write!(f, "DNS error: {}", err), + Self::IoError(err) => defmt::write!(f, "IO error: {}", err), + } + } +} + #[cfg(feature = "std")] impl std::error::Error for DnsIoError where E: std::error::Error {} @@ -73,13 +84,13 @@ where let request = &rx_buf[..len]; - debug!("Received {} bytes from {remote}", request.len()); + debug!("Received {} bytes from {}", request.len(), remote); let len = match crate::reply(request, &ip.octets(), ttl, tx_buf) { Ok(len) => len, Err(err) => match err { DnsError::InvalidMessage => { - warn!("Got invalid message from {remote}, skipping"); + warn!("Got invalid message from {}, skipping", remote); continue; } other => Err(other)?, @@ -90,6 +101,6 @@ where .await .map_err(DnsIoError::IoError)?; - debug!("Sent {len} bytes to {remote}"); + debug!("Sent {} bytes to {}", len, remote); } } diff --git a/edge-captive/src/lib.rs b/edge-captive/src/lib.rs index 8a653e5..3bf8412 100644 --- a/edge-captive/src/lib.rs +++ b/edge-captive/src/lib.rs @@ -1,12 +1,11 @@ #![cfg_attr(not(feature = "std"), no_std)] #![warn(clippy::large_futures)] -use core::fmt::{self, Display}; +use core::fmt::Display; use core::time::Duration; use domain::base::wire::Composer; use domain::dep::octseq::{OctetsBuilder, Truncate}; -use log::debug; use domain::{ base::{ @@ -21,6 +20,9 @@ use domain::{ rdata::A, }; +// This mod MUST go first, so that the others see its macros. +pub(crate) mod fmt; + #[cfg(feature = "io")] pub mod io; @@ -31,7 +33,7 @@ pub enum DnsError { } impl Display for DnsError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Self::ShortBuf => write!(f, "ShortBuf"), Self::InvalidMessage => write!(f, "InvalidMessage"), @@ -39,6 +41,16 @@ impl Display for DnsError { } } +#[cfg(feature = "defmt")] +impl defmt::Format for DnsError { + fn format(&self, f: defmt::Formatter<'_>) { + match self { + Self::ShortBuf => defmt::write!(f, "ShortBuf"), + Self::InvalidMessage => defmt::write!(f, "InvalidMessage"), + } + } +} + #[cfg(feature = "std")] impl std::error::Error for DnsError {} diff --git a/edge-dhcp/CHANGELOG.md b/edge-dhcp/CHANGELOG.md index 47887ff..d09eb9c 100644 --- a/edge-dhcp/CHANGELOG.md +++ b/edge-dhcp/CHANGELOG.md @@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +* Optional `defmt` support via two new features (one has to specify one, or the other, or neither, but not both): + * `log` - uses the `log` crate for all logging + * `defmt` - uses the `defmt` crate for all logging, and implements `defmt::Format` for all library types that otherwise implement `Debug` and/or `Display` ## [0.5.0] - 2025-01-15 * Updated dependencies for compatibility with `embassy-time-driver` v0.2 diff --git a/edge-dhcp/Cargo.toml b/edge-dhcp/Cargo.toml index 5913307..052e4bf 100644 --- a/edge-dhcp/Cargo.toml +++ b/edge-dhcp/Cargo.toml @@ -18,10 +18,12 @@ categories = [ default = ["io"] std = ["io"] io = ["embassy-futures", "embassy-time", "edge-nal"] +defmt = ["dep:defmt", "heapless/defmt-03", "embassy-time?/defmt"] [dependencies] heapless = { workspace = true } -log = { workspace = true } +log = { version = "0.4", default-features = false, optional = true } +defmt = { version = "0.3", optional = true, features = ["ip_in_core"] } rand_core = "0.6" embassy-futures = { workspace = true, optional = true } embassy-time = { workspace = true, default-features = false, optional = true } diff --git a/edge-dhcp/src/fmt.rs b/edge-dhcp/src/fmt.rs new file mode 100644 index 0000000..8bd7963 --- /dev/null +++ b/edge-dhcp/src/fmt.rs @@ -0,0 +1,340 @@ +//! A module that re-exports the `log` macros if `defmt` is not enabled, or `defmt` macros if `defmt` is enabled. +//! +//! The module also defines: +//! - Custom versions of the core assert macros (`assert!`, `assert_eq!`, etc.) that use the `defmt` macros if the `defmt` feature is enabled. +//! - Custom versions of the `panic!`, `todo!`, and `unreachable!` macros that use the `defmt` macros if the `defmt` feature is enabled. +//! - A custom `unwrap!` macro that uses the `defmt` macros if the `defmt` feature is enabled, otherwise it uses the standard library's `unwrap` method. +//! - A custom `Bytes` struct that formats byte slices as hex in a way compatible with `defmt`. +#![macro_use] +#![allow(unused)] + +use core::fmt::{Debug, Display, LowerHex}; + +#[collapse_debuginfo(yes)] +macro_rules! assert { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! assert_eq { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert_eq!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert_eq!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! assert_ne { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert_ne!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert_ne!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! debug_assert { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! debug_assert_eq { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert_eq!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert_eq!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! debug_assert_ne { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert_ne!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert_ne!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! todo { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::todo!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::todo!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! unreachable { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::unreachable!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::unreachable!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! panic { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::panic!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::panic!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! trace { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::trace!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::trace!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! debug { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::debug!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::debug!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! info { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::info!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::info!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! warn { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::warn!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::warn!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! error { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::error!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::error!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[cfg(feature = "defmt")] +#[collapse_debuginfo(yes)] +macro_rules! unwrap { + ($($x:tt)*) => { + ::defmt::unwrap!($($x)*) + }; +} + +#[cfg(not(feature = "defmt"))] +#[collapse_debuginfo(yes)] +macro_rules! unwrap { + ($arg:expr) => { + match $crate::fmt::Try::into_result($arg) { + ::core::result::Result::Ok(t) => t, + ::core::result::Result::Err(e) => { + ::core::panic!("unwrap of `{}` failed: {:?}", ::core::stringify!($arg), e); + } + } + }; + ($arg:expr, $($msg:expr),+ $(,)? ) => { + match $crate::fmt::Try::into_result($arg) { + ::core::result::Result::Ok(t) => t, + ::core::result::Result::Err(e) => { + ::core::panic!("unwrap of `{}` failed: {}: {:?}", ::core::stringify!($arg), ::core::format_args!($($msg,)*), e); + } + } + } +} + +#[cfg(feature = "defmt")] +#[collapse_debuginfo(yes)] +macro_rules! write_unwrap { + ($f:expr, $s:literal $(, $x:expr)* $(,)?) => { + { + unwrap!(write!($f, $s $(, $x)*).map_err($crate::fmt::FmtError)); + } + }; +} + +#[cfg(not(feature = "defmt"))] +#[collapse_debuginfo(yes)] +macro_rules! write_unwrap { + ($f:expr, $s:literal $(, $x:expr)* $(,)?) => { + { + unwrap!(write!($f, $s $(, $x)*)); + } + }; +} + +#[cfg(feature = "defmt")] +#[collapse_debuginfo(yes)] +macro_rules! display2format { + ($arg:expr) => { + ::defmt::Display2Format(&$arg) + }; +} + +#[cfg(not(feature = "defmt"))] +#[collapse_debuginfo(yes)] +macro_rules! display2format { + ($arg:expr) => { + $arg + }; +} + +#[cfg(feature = "defmt")] +#[collapse_debuginfo(yes)] +macro_rules! debug2format { + ($arg:expr) => { + ::defmt::Debug2Format(&$arg) + }; +} + +#[cfg(not(feature = "defmt"))] +#[collapse_debuginfo(yes)] +macro_rules! debug2format { + ($arg:expr) => { + $arg + }; +} + +/// A way to `{:x?}` format a byte slice which is compatible with `defmt` +pub struct Bytes<'a>(pub &'a [u8]); + +impl Debug for Bytes<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#02x?}", self.0) + } +} + +impl Display for Bytes<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#02x?}", self.0) + } +} + +impl LowerHex for Bytes<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#02x?}", self.0) + } +} + +#[cfg(feature = "defmt")] +impl defmt::Format for Bytes<'_> { + fn format(&self, fmt: defmt::Formatter) { + defmt::write!(fmt, "{:02x}", self.0) + } +} + +/// Support for the `unwrap!` macro for `Option` and `Result`. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct NoneError; + +/// Support for the `unwrap!` macro for `Option` and `Result`. +pub trait Try { + type Ok; + type Error; + #[allow(unused)] + fn into_result(self) -> Result; +} + +impl Try for Option { + type Ok = T; + type Error = NoneError; + + #[inline] + fn into_result(self) -> Result { + self.ok_or(NoneError) + } +} + +impl Try for Result { + type Ok = T; + type Error = E; + + #[inline] + fn into_result(self) -> Self { + self + } +} + +#[cfg(feature = "defmt")] +pub(crate) struct FmtError(pub(crate) core::fmt::Error); + +#[cfg(feature = "defmt")] +impl defmt::Format for FmtError { + fn format(&self, f: defmt::Formatter<'_>) { + defmt::write!(f, "{}", "FmtError") + } +} diff --git a/edge-dhcp/src/io.rs b/edge-dhcp/src/io.rs index 5b4d280..66b612c 100644 --- a/edge-dhcp/src/io.rs +++ b/edge-dhcp/src/io.rs @@ -47,5 +47,18 @@ where } } +#[cfg(feature = "defmt")] +impl defmt::Format for Error +where + E: defmt::Format, +{ + fn format(&self, f: defmt::Formatter<'_>) { + match self { + Self::Io(err) => defmt::write!(f, "IO error: {}", err), + Self::Format(err) => defmt::write!(f, "Format error: {}", err), + } + } +} + #[cfg(feature = "std")] impl std::error::Error for Error where E: std::error::Error {} diff --git a/edge-dhcp/src/io/client.rs b/edge-dhcp/src/io/client.rs index a03c6bd..327f09e 100644 --- a/edge-dhcp/src/io/client.rs +++ b/edge-dhcp/src/io/client.rs @@ -5,8 +5,6 @@ use edge_nal::{UdpReceive, UdpSend}; use embassy_futures::select::{select, Either}; use embassy_time::{Duration, Instant, Timer}; -use log::{info, warn}; - use rand_core::RngCore; pub use super::*; @@ -16,6 +14,7 @@ use crate::{Options, Packet}; /// Represents the additional network-related information that might be returned by the DHCP server. #[derive(Debug, Clone)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[non_exhaustive] pub struct NetworkInfo<'a> { pub gateway: Option, @@ -30,6 +29,7 @@ pub struct NetworkInfo<'a> { /// This structure has a set of asynchronous methods that can utilize a supplied DHCP client instance and UDP socket to /// transparently implement all aspects of negotiating an IP with the DHCP server and then keeping the lease of that IP up to date. #[derive(Debug, Clone)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[non_exhaustive] pub struct Lease { pub ip: Ipv4Addr, @@ -54,7 +54,7 @@ impl Lease { { loop { let offer = Self::discover(client, socket, buf, Duration::from_secs(3)).await?; - let server_ip = offer.server_ip.unwrap(); + let server_ip = unwrap!(offer.server_ip); let ip = offer.ip; let now = Instant::now(); @@ -79,7 +79,7 @@ impl Lease { break Ok(( Self { ip: settings.ip, - server_ip: settings.server_ip.unwrap(), + server_ip: unwrap!(settings.server_ip), duration: Duration::from_secs( settings.lease_time_secs.unwrap_or(7200) as _ ), @@ -230,7 +230,7 @@ impl Lease { info!( "IP {} offered by DHCP server {}", settings.ip, - settings.server_ip.unwrap() + unwrap!(settings.server_ip) ); return Ok(settings); @@ -257,7 +257,7 @@ impl Lease { S: UdpReceive + UdpSend, { for _ in 0..retries { - info!("Requesting IP {ip} from DHCP server {server_ip}"); + info!("Requesting IP {} from DHCP server {}", ip, server_ip); let start = Instant::now(); diff --git a/edge-dhcp/src/io/server.rs b/edge-dhcp/src/io/server.rs index 3302414..bd7ca44 100644 --- a/edge-dhcp/src/io/server.rs +++ b/edge-dhcp/src/io/server.rs @@ -1,7 +1,6 @@ use core::net::Ipv4Addr; use edge_nal::{UdpReceive, UdpSend}; -use log::{info, warn}; use self::dhcp::{Options, Packet}; @@ -34,8 +33,8 @@ where F: FnMut() -> u64, { info!( - "Running DHCP server for addresses {}-{} with configuration {server_options:?}", - server.range_start, server.range_end + "Running DHCP server for addresses {}-{} with configuration {:?}", + server.range_start, server.range_end, server_options ); loop { diff --git a/edge-dhcp/src/lib.rs b/edge-dhcp/src/lib.rs index 23538f5..f449c49 100644 --- a/edge-dhcp/src/lib.rs +++ b/edge-dhcp/src/lib.rs @@ -2,7 +2,6 @@ #![warn(clippy::large_futures)] /// This code is a `no_std` and no-alloc modification of https://github.com/krolaw/dhcp4r -use core::fmt; use core::str::Utf8Error; pub use core::net::Ipv4Addr; @@ -11,6 +10,9 @@ use num_enum::TryFromPrimitive; use edge_raw::bytes::{self, BytesIn, BytesOut}; +// This mod MUST go first, so that the others see its macros. +pub(crate) mod fmt; + pub mod client; pub mod server; @@ -38,8 +40,8 @@ impl From for Error { } } -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +impl core::fmt::Display for Error { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { let str = match self { Self::DataUnderflow => "Data underflow", Self::BufferOverflow => "Buffer overflow", @@ -54,6 +56,23 @@ impl fmt::Display for Error { } } +#[cfg(feature = "defmt")] +impl defmt::Format for Error { + fn format(&self, f: defmt::Formatter<'_>) { + let str = match self { + Self::DataUnderflow => "Data underflow", + Self::BufferOverflow => "Buffer overflow", + Self::InvalidPacket => "Invalid packet", + Self::InvalidUtf8Str(_) => "Invalid Utf8 string", + Self::InvalidMessageType => "Invalid message type", + Self::MissingCookie => "Missing cookie", + Self::InvalidHlen => "Invalid hlen", + }; + + defmt::write!(f, "{}", str) + } +} + #[cfg(feature = "std")] impl std::error::Error for Error {} @@ -101,8 +120,8 @@ pub enum MessageType { Inform = 8, } -impl fmt::Display for MessageType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +impl core::fmt::Display for MessageType { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Self::Discover => "DHCPDISCOVER", Self::Offer => "DHCPOFFER", @@ -117,8 +136,26 @@ impl fmt::Display for MessageType { } } +#[cfg(feature = "defmt")] +impl defmt::Format for MessageType { + fn format(&self, f: defmt::Formatter<'_>) { + match self { + Self::Discover => "DHCPDISCOVER", + Self::Offer => "DHCPOFFER", + Self::Request => "DHCPREQUEST", + Self::Decline => "DHCPDECLINE", + Self::Ack => "DHCPACK", + Self::Nak => "DHCPNAK", + Self::Release => "DHCPRELEASE", + Self::Inform => "DHCPINFORM", + } + .format(f) + } +} + /// DHCP Packet Structure #[derive(Clone, PartialEq, Eq, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct Packet<'a> { pub reply: bool, pub hops: u8, @@ -287,6 +324,7 @@ impl<'a> Packet<'a> { } #[derive(Clone, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[non_exhaustive] pub struct Settings<'a> { pub ip: Ipv4Addr, @@ -357,6 +395,7 @@ impl<'a> Settings<'a> { } #[derive(Clone, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct Options<'a>(OptionsInner<'a>); impl<'a> Options<'a> { @@ -504,13 +543,14 @@ impl<'a> Options<'a> { } } -impl fmt::Debug for Options<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +impl core::fmt::Debug for Options<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_set().entries(self.iter()).finish() } } #[derive(Clone, PartialEq, Eq, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] enum OptionsInner<'a> { ByteSlice(&'a [u8]), DataSlice(&'a [DhcpOption<'a>]), @@ -543,7 +583,7 @@ impl<'a> OptionsInner<'a> { if self.0.is_empty() { None } else { - DhcpOption::decode(&mut self.0).unwrap() + unwrap!(DhcpOption::decode(&mut self.0)) } } } @@ -558,6 +598,7 @@ impl<'a> OptionsInner<'a> { } #[derive(Copy, Clone, PartialEq, Eq, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum DhcpOption<'a> { /// 53: DHCP Message Type MessageType(MessageType), @@ -707,6 +748,7 @@ impl DhcpOption<'_> { } #[derive(Copy, Clone, PartialEq, Eq, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct Ipv4Addrs<'a>(Ipv4AddrsInner<'a>); impl<'a> Ipv4Addrs<'a> { @@ -720,6 +762,7 @@ impl<'a> Ipv4Addrs<'a> { } #[derive(Copy, Clone, PartialEq, Eq, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] enum Ipv4AddrsInner<'a> { ByteSlice(&'a [u8]), DataSlice(&'a [Ipv4Addr]), @@ -730,7 +773,7 @@ impl<'a> Ipv4AddrsInner<'a> { match self { Self::ByteSlice(data) => { EitherIterator::First((0..data.len()).step_by(4).map(|offset| { - let octets: [u8; 4] = data[offset..offset + 4].try_into().unwrap(); + let octets: [u8; 4] = unwrap!(data[offset..offset + 4].try_into()); octets.into() })) diff --git a/edge-dhcp/src/server.rs b/edge-dhcp/src/server.rs index ac531db..7471ba1 100644 --- a/edge-dhcp/src/server.rs +++ b/edge-dhcp/src/server.rs @@ -1,16 +1,16 @@ use core::fmt::Debug; -use log::{debug, warn}; - use super::*; #[derive(Clone, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct Lease { mac: [u8; 16], expires: u64, } #[derive(Clone, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum Action<'a> { Discover(Option, &'a [u8; 16]), Request(Ipv4Addr, &'a [u8; 16]), @@ -19,6 +19,7 @@ pub enum Action<'a> { } #[derive(Clone, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[non_exhaustive] pub struct ServerOptions<'a> { pub ip: Ipv4Addr, @@ -64,7 +65,10 @@ impl<'a> ServerOptions<'a> { let message_type = if let Some(message_type) = message_type { message_type } else { - warn!("Ignoring DHCP request, no message type found: {request:?}"); + warn!( + "Ignoring DHCP request, no message type found: {:?}", + request + ); return None; }; @@ -77,11 +81,14 @@ impl<'a> ServerOptions<'a> { }); if server_identifier.is_some() && server_identifier != Some(self.ip) { - warn!("Ignoring {message_type} request, not addressed to this server: {request:?}"); + warn!( + "Ignoring {} request, not addressed to this server: {:?}", + message_type, request + ); return None; } - debug!("Received {message_type} request: {request:?}"); + debug!("Received {} request: {:?}", message_type, request); match message_type { MessageType::Discover => Some(Action::Discover( request.options.requested_ip(), @@ -156,7 +163,7 @@ impl<'a> ServerOptions<'a> { ), ); - debug!("Sending {message_type} reply: {reply:?}"); + debug!("Sending {} reply: {:?}", message_type, reply); reply } @@ -166,6 +173,7 @@ impl<'a> ServerOptions<'a> { /// The server is unaware of the IP/UDP transport layer and operates purely in terms of packets /// represented as Rust slices. #[derive(Clone, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct Server { pub now: F, pub range_start: Ipv4Addr, diff --git a/edge-http/CHANGELOG.md b/edge-http/CHANGELOG.md index e7dbbbb..01f79e1 100644 --- a/edge-http/CHANGELOG.md +++ b/edge-http/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] +* Optional `defmt` support via two new features (one has to specify one, or the other, or neither, but not both): + * `log` - uses the `log` crate for all logging + * `defmt` - uses the `defmt` crate for all logging, and implements `defmt::Format` for all library types that otherwise implement `Debug` and/or `Display` + ## [0.5.1] - 2025-02-02 * Fix multiple websocket-related connection issues (#58) diff --git a/edge-http/Cargo.toml b/edge-http/Cargo.toml index 00159e5..51a5ef6 100644 --- a/edge-http/Cargo.toml +++ b/edge-http/Cargo.toml @@ -19,13 +19,15 @@ categories = [ default = ["io"] std = ["io"] io = ["embedded-io-async", "edge-nal", "embassy-sync", "embassy-futures", "embassy-time"] +defmt = ["dep:defmt", "heapless/defmt-03", "embedded-svc?/defmt"] [dependencies] embedded-io-async = { workspace = true, optional = true } edge-nal = { workspace = true, optional = true } embedded-svc = { workspace = true, optional = true, default-features = false } heapless = { workspace = true } -log = { workspace = true } +log = { version = "0.4", default-features = false, optional = true } +defmt = { version = "0.3", optional = true } embassy-sync = { workspace = true, optional = true } embassy-futures = { workspace = true, optional = true } embassy-time = { workspace = true, optional = true } diff --git a/edge-http/src/fmt.rs b/edge-http/src/fmt.rs new file mode 100644 index 0000000..8bd7963 --- /dev/null +++ b/edge-http/src/fmt.rs @@ -0,0 +1,340 @@ +//! A module that re-exports the `log` macros if `defmt` is not enabled, or `defmt` macros if `defmt` is enabled. +//! +//! The module also defines: +//! - Custom versions of the core assert macros (`assert!`, `assert_eq!`, etc.) that use the `defmt` macros if the `defmt` feature is enabled. +//! - Custom versions of the `panic!`, `todo!`, and `unreachable!` macros that use the `defmt` macros if the `defmt` feature is enabled. +//! - A custom `unwrap!` macro that uses the `defmt` macros if the `defmt` feature is enabled, otherwise it uses the standard library's `unwrap` method. +//! - A custom `Bytes` struct that formats byte slices as hex in a way compatible with `defmt`. +#![macro_use] +#![allow(unused)] + +use core::fmt::{Debug, Display, LowerHex}; + +#[collapse_debuginfo(yes)] +macro_rules! assert { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! assert_eq { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert_eq!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert_eq!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! assert_ne { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert_ne!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert_ne!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! debug_assert { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! debug_assert_eq { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert_eq!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert_eq!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! debug_assert_ne { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert_ne!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert_ne!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! todo { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::todo!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::todo!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! unreachable { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::unreachable!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::unreachable!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! panic { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::panic!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::panic!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! trace { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::trace!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::trace!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! debug { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::debug!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::debug!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! info { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::info!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::info!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! warn { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::warn!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::warn!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! error { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::error!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::error!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[cfg(feature = "defmt")] +#[collapse_debuginfo(yes)] +macro_rules! unwrap { + ($($x:tt)*) => { + ::defmt::unwrap!($($x)*) + }; +} + +#[cfg(not(feature = "defmt"))] +#[collapse_debuginfo(yes)] +macro_rules! unwrap { + ($arg:expr) => { + match $crate::fmt::Try::into_result($arg) { + ::core::result::Result::Ok(t) => t, + ::core::result::Result::Err(e) => { + ::core::panic!("unwrap of `{}` failed: {:?}", ::core::stringify!($arg), e); + } + } + }; + ($arg:expr, $($msg:expr),+ $(,)? ) => { + match $crate::fmt::Try::into_result($arg) { + ::core::result::Result::Ok(t) => t, + ::core::result::Result::Err(e) => { + ::core::panic!("unwrap of `{}` failed: {}: {:?}", ::core::stringify!($arg), ::core::format_args!($($msg,)*), e); + } + } + } +} + +#[cfg(feature = "defmt")] +#[collapse_debuginfo(yes)] +macro_rules! write_unwrap { + ($f:expr, $s:literal $(, $x:expr)* $(,)?) => { + { + unwrap!(write!($f, $s $(, $x)*).map_err($crate::fmt::FmtError)); + } + }; +} + +#[cfg(not(feature = "defmt"))] +#[collapse_debuginfo(yes)] +macro_rules! write_unwrap { + ($f:expr, $s:literal $(, $x:expr)* $(,)?) => { + { + unwrap!(write!($f, $s $(, $x)*)); + } + }; +} + +#[cfg(feature = "defmt")] +#[collapse_debuginfo(yes)] +macro_rules! display2format { + ($arg:expr) => { + ::defmt::Display2Format(&$arg) + }; +} + +#[cfg(not(feature = "defmt"))] +#[collapse_debuginfo(yes)] +macro_rules! display2format { + ($arg:expr) => { + $arg + }; +} + +#[cfg(feature = "defmt")] +#[collapse_debuginfo(yes)] +macro_rules! debug2format { + ($arg:expr) => { + ::defmt::Debug2Format(&$arg) + }; +} + +#[cfg(not(feature = "defmt"))] +#[collapse_debuginfo(yes)] +macro_rules! debug2format { + ($arg:expr) => { + $arg + }; +} + +/// A way to `{:x?}` format a byte slice which is compatible with `defmt` +pub struct Bytes<'a>(pub &'a [u8]); + +impl Debug for Bytes<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#02x?}", self.0) + } +} + +impl Display for Bytes<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#02x?}", self.0) + } +} + +impl LowerHex for Bytes<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#02x?}", self.0) + } +} + +#[cfg(feature = "defmt")] +impl defmt::Format for Bytes<'_> { + fn format(&self, fmt: defmt::Formatter) { + defmt::write!(fmt, "{:02x}", self.0) + } +} + +/// Support for the `unwrap!` macro for `Option` and `Result`. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct NoneError; + +/// Support for the `unwrap!` macro for `Option` and `Result`. +pub trait Try { + type Ok; + type Error; + #[allow(unused)] + fn into_result(self) -> Result; +} + +impl Try for Option { + type Ok = T; + type Error = NoneError; + + #[inline] + fn into_result(self) -> Result { + self.ok_or(NoneError) + } +} + +impl Try for Result { + type Ok = T; + type Error = E; + + #[inline] + fn into_result(self) -> Self { + self + } +} + +#[cfg(feature = "defmt")] +pub(crate) struct FmtError(pub(crate) core::fmt::Error); + +#[cfg(feature = "defmt")] +impl defmt::Format for FmtError { + fn format(&self, f: defmt::Formatter<'_>) { + defmt::write!(f, "{}", "FmtError") + } +} diff --git a/edge-http/src/io.rs b/edge-http/src/io.rs index 487c73c..3fef17e 100644 --- a/edge-http/src/io.rs +++ b/edge-http/src/io.rs @@ -6,8 +6,6 @@ use embedded_io_async::{ErrorType, Read, Write}; use httparse::Status; -use log::trace; - use crate::ws::UpgradeError; use crate::{ BodyType, ConnectionType, Headers, HeadersMismatchError, Method, RequestHeaders, @@ -304,7 +302,7 @@ where raw::send_version(&mut output, http11).await?; output.write_all(b" ").await.map_err(Error::Io)?; - let status_str: heapless::String<5> = status.try_into().unwrap(); + let status_str: heapless::String<5> = unwrap!(status.try_into()); output .write_all(status_str.as_bytes()) .await @@ -1077,7 +1075,7 @@ where Err(Error::InvalidState) } else if !buf.is_empty() { let mut len_str = heapless::String::<8>::new(); - write!(&mut len_str, "{:x}", buf.len()).unwrap(); + write_unwrap!(&mut len_str, "{:x}", buf.len()); self.output .write_all(len_str.as_bytes()) @@ -1104,8 +1102,6 @@ mod raw { use embedded_io_async::{Read, Write}; - use log::warn; - use crate::{BodyType, ConnectionType}; use super::Error; @@ -1231,7 +1227,10 @@ mod raw { if let Some(header_connection) = header_connection { if let Some(connection) = connection { - warn!("Multiple Connection headers found. Current {connection} and new {header_connection}"); + warn!( + "Multiple Connection headers found. Current {} and new {}", + connection, header_connection + ); } // The last connection header wins @@ -1243,7 +1242,10 @@ mod raw { if let Some(header_body) = header_body { if let Some(body) = body { - warn!("Multiple body type headers found. Current {body} and new {header_body}"); + warn!( + "Multiple body type headers found. Current {} and new {}", + body, header_body + ); } // The last body header wins @@ -1339,7 +1341,7 @@ mod test { let len = r.read(&mut buf2).await; assert!(len.is_ok()); - assert_eq!(len.unwrap(), 0); + assert_eq!(unwrap!(len), 0); } else { assert!(r.read(&mut buf2).await.is_err()); } diff --git a/edge-http/src/io/client.rs b/edge-http/src/io/client.rs index b0663ac..a4ed9b8 100644 --- a/edge-http/src/io/client.rs +++ b/edge-http/src/io/client.rs @@ -60,7 +60,7 @@ where /// Reinitialize the connection with a new address. pub async fn reinitialize(&mut self, addr: SocketAddr) -> Result<(), Error> { let _ = self.complete().await; - self.unbound_mut().unwrap().addr = addr; + unwrap!(self.unbound_mut(), "Unreachable").addr = addr; Ok(()) } @@ -148,7 +148,7 @@ where pub fn release(mut self) -> (T::Socket<'b>, &'b mut [u8]) { let mut state = self.unbind(); - let io = state.io.take().unwrap(); + let io = unwrap!(state.io.take()); (io, state.buf) } @@ -174,7 +174,7 @@ where let mut state = self.unbind(); let result = async { - match send_request(http11, method, uri, state.io.as_mut().unwrap()).await { + match send_request(http11, method, uri, unwrap!(state.io.as_mut())).await { Ok(_) => (), Err(Error::Io(_)) => { if !fresh_connection { @@ -182,13 +182,13 @@ where state.io = None; state.io = Some(state.socket.connect(state.addr).await.map_err(Error::Io)?); - send_request(http11, method, uri, state.io.as_mut().unwrap()).await?; + send_request(http11, method, uri, unwrap!(state.io.as_mut())).await?; } } Err(other) => Err(other)?, } - let io = state.io.as_mut().unwrap(); + let io = unwrap!(state.io.as_mut()); send_headers(headers, None, true, http11, true, &mut *io).await } @@ -201,7 +201,7 @@ where socket: state.socket, addr: state.addr, connection_type, - io: SendBody::new(body_type, state.io.unwrap()), + io: SendBody::new(body_type, unwrap!(state.io)), }); Ok(()) @@ -278,14 +278,14 @@ where let mut response = ResponseHeaders::new(); match response - .receive(state.buf, &mut state.io.as_mut().unwrap(), true) + .receive(state.buf, &mut unwrap!(state.io.as_mut()), true) .await { Ok((buf, read_len)) => { let (connection_type, body_type) = response.resolve::(request_connection_type)?; - let io = Body::new(body_type, buf, read_len, state.io.unwrap()); + let io = Body::new(body_type, buf, read_len, unwrap!(state.io)); *self = Self::Response(ResponseState { buf: buf_ptr, @@ -300,7 +300,7 @@ where } Err(e) => { state.io = None; - state.buf = unsafe { buf_ptr.as_mut().unwrap() }; + state.buf = unwrap!(unsafe { buf_ptr.as_mut() }); *self = Self::Unbound(state); @@ -337,7 +337,7 @@ where fn unbind(&mut self) -> UnboundState<'b, T, N> { let state = mem::replace(self, Self::Transition(TransitionState(()))); - let unbound = match state { + match state { Self::Unbound(unbound) => unbound, Self::Request(request) => { let io = request.io.release(); @@ -353,16 +353,14 @@ where let io = response.io.release(); UnboundState { - buf: unsafe { response.buf.as_mut().unwrap() }, + buf: unwrap!(unsafe { response.buf.as_mut() }), socket: response.socket, addr: response.addr, io: Some(io), } } _ => unreachable!(), - }; - - unbound + } } fn unbound_mut(&mut self) -> Result<&mut UnboundState<'b, T, N>, Error> { @@ -399,7 +397,7 @@ where fn io_mut(&mut self) -> &mut T::Socket<'b> { match self { - Self::Unbound(unbound) => unbound.io.as_mut().unwrap(), + Self::Unbound(unbound) => unwrap!(unbound.io.as_mut()), Self::Request(request) => request.io.as_raw_writer(), Self::Response(response) => response.io.as_raw_reader(), _ => unreachable!(), diff --git a/edge-http/src/io/server.rs b/edge-http/src/io/server.rs index 4ce48d9..b89057d 100644 --- a/edge-http/src/io/server.rs +++ b/edge-http/src/io/server.rs @@ -11,8 +11,6 @@ use embassy_sync::mutex::Mutex; use embedded_io_async::{ErrorType, Read, Write}; -use log::{debug, info, warn}; - use super::{send_headers, send_status, Body, Error, RequestHeaders, SendBody}; use crate::ws::{upgrade_response_headers, MAX_BASE64_KEY_RESPONSE_LEN}; @@ -468,17 +466,23 @@ pub async fn handle_connection( T: Read + Write + Readable + TcpSplit + TcpShutdown, { let close = loop { - debug!("Handler task {task_id}: Waiting for a new request"); + debug!("Handler task {}: Waiting for a new request", task_id); if let Some(keepalive_timeout_ms) = keepalive_timeout_ms { let wait_data = with_timeout(keepalive_timeout_ms, io.readable()).await; match wait_data { Err(WithTimeoutError::Timeout) => { - info!("Handler task {task_id}: Closing connection due to inactivity"); + info!( + "Handler task {}: Closing connection due to inactivity", + task_id + ); break true; } Err(e) => { - warn!("Handler task {task_id}: Error when handling request: {e:?}"); + warn!( + "Handler task {}: Error when handling request: {:?}", + task_id, e + ); break true; } Ok(_) => {} @@ -489,19 +493,25 @@ pub async fn handle_connection( match result { Err(HandlerError::Connection(Error::ConnectionClosed)) => { - debug!("Handler task {task_id}: Connection closed"); + debug!("Handler task {}: Connection closed", task_id); break false; } Err(e) => { - warn!("Handler task {task_id}: Error when handling request: {e:?}"); + warn!( + "Handler task {}: Error when handling request: {:?}", + task_id, e + ); break true; } Ok(needs_close) => { if needs_close { - debug!("Handler task {task_id}: Request complete; closing connection"); + debug!( + "Handler task {}: Request complete; closing connection", + task_id + ); break true; } else { - debug!("Handler task {task_id}: Request complete"); + debug!("Handler task {}: Request complete", task_id); } } } @@ -509,7 +519,10 @@ pub async fn handle_connection( if close { if let Err(e) = io.close(Close::Both).await { - warn!("Handler task {task_id}: Error when closing the socket: {e:?}"); + warn!( + "Handler task {}: Error when closing the socket: {:?}", + task_id, e + ); } } else { let _ = io.abort().await; @@ -662,7 +675,8 @@ impl Server { let mut tasks = heapless::Vec::<_, P>::new(); info!( - "Creating {P} handler tasks, memory: {}B", + "Creating {} handler tasks, memory: {}B", + P, core::mem::size_of_val(&tasks) ); @@ -673,10 +687,10 @@ impl Server { let handler = &handler; let buf: *mut [u8; B] = &mut unsafe { self.0.assume_init_mut() }[index]; - tasks + unwrap!(tasks .push(async move { loop { - debug!("Handler task {task_id}: Waiting for connection"); + debug!("Handler task {}: Waiting for connection", task_id); let io = { let _guard = mutex.lock().await; @@ -684,11 +698,11 @@ impl Server { acceptor.accept().await.map_err(Error::Io)?.1 }; - debug!("Handler task {task_id}: Got connection request"); + debug!("Handler task {}: Got connection request", task_id); handle_connection::<_, _, N>( io, - unsafe { buf.as_mut() }.unwrap(), + unwrap!(unsafe { buf.as_mut() }), keepalive_timeout_ms, task_id, handler, @@ -696,13 +710,12 @@ impl Server { .await; } }) - .map_err(|_| ()) - .unwrap(); + .map_err(|_| ())); } let (result, _) = embassy_futures::select::select_slice(&mut tasks).await; - warn!("Server processing loop quit abruptly: {result:?}"); + warn!("Server processing loop quit abruptly: {:?}", result); result } @@ -815,7 +828,7 @@ mod embedded_svc_compat { // where // T: Read + Write, // { - // self.0.handle(connection).await.unwrap(); + // unwrap!(self.0.handle(connection).await); // Ok(()) // } diff --git a/edge-http/src/lib.rs b/edge-http/src/lib.rs index e9cbab1..f99aefa 100644 --- a/edge-http/src/lib.rs +++ b/edge-http/src/lib.rs @@ -2,15 +2,18 @@ #![allow(async_fn_in_trait)] #![warn(clippy::large_futures)] -use core::fmt::{self, Display}; +use core::fmt::Display; use core::str; use httparse::{Header, EMPTY_HEADER}; -use log::{debug, warn}; + use ws::{is_upgrade_accepted, is_upgrade_request, MAX_BASE64_KEY_RESPONSE_LEN, NONCE_LEN}; pub const DEFAULT_MAX_HEADERS_COUNT: usize = 64; +// This mod MUST go first, so that the others see its macros. +pub(crate) mod fmt; + #[cfg(feature = "io")] pub mod io; @@ -31,7 +34,7 @@ pub enum HeadersMismatchError { } impl Display for HeadersMismatchError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Self::ResponseConnectionTypeMismatchError => write!( f, @@ -194,7 +197,7 @@ impl Method { } impl Display for Method { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!(f, "{}", self.as_str()) } } @@ -212,8 +215,12 @@ impl<'b, const N: usize> Headers<'b, N> { /// Utility method to return the value of the `Content-Length` header, if present pub fn content_len(&self) -> Option { - self.get("Content-Length") - .map(|content_len_str| content_len_str.parse::().unwrap()) + self.get("Content-Length").map(|content_len_str| { + unwrap!( + content_len_str.parse::(), + "Invalid Content-Length header" + ) + }) } /// Utility method to return the value of the `Content-Type` header, if present @@ -327,7 +334,7 @@ impl<'b, const N: usize> Headers<'b, N> { content_len: u64, buf: &'b mut heapless::String<20>, ) -> &mut Self { - *buf = content_len.try_into().unwrap(); + *buf = unwrap!(content_len.try_into()); self.set("Content-Length", buf.as_str()) } @@ -522,7 +529,10 @@ impl ConnectionType { if let Some(header_connection) = header_connection { if let Some(connection) = connection { - warn!("Multiple Connection headers found. Current {connection} and new {header_connection}"); + warn!( + "Multiple Connection headers found. Current {} and new {}", + connection, header_connection + ); } // The last connection header wins @@ -546,7 +556,7 @@ impl ConnectionType { } impl Display for ConnectionType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Self::KeepAlive => write!(f, "Keep-Alive"), Self::Close => write!(f, "Close"), @@ -654,7 +664,10 @@ impl BodyType { return Some(Self::Chunked); } } else if "Content-Length".eq_ignore_ascii_case(name) { - return Some(Self::ContentLen(value.parse::().unwrap())); // TODO + return Some(Self::ContentLen(unwrap!( + value.parse::(), + "Invalid Content-Length header" + ))); // TODO } None @@ -675,7 +688,10 @@ impl BodyType { if let Some(header_body) = header_body { if let Some(body) = body { - warn!("Multiple body type headers found. Current {body} and new {header_body}"); + warn!( + "Multiple body type headers found. Current {} and new {}", + body, header_body + ); } // The last body header wins @@ -699,7 +715,7 @@ impl BodyType { buf.clear(); - write!(buf, "{}", len).unwrap(); + write_unwrap!(buf, "{}", len); Some(("Content-Length", buf.as_bytes())) } @@ -709,7 +725,7 @@ impl BodyType { } impl Display for BodyType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Self::Chunked => write!(f, "Chunked"), Self::ContentLen(len) => write!(f, "Content-Length: {len}"), @@ -757,7 +773,7 @@ impl Default for RequestHeaders<'_, N> { } impl Display for RequestHeaders<'_, N> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!(f, "{} ", if self.http11 { "HTTP/1.1" } else { "HTTP/1.0" })?; writeln!(f, "{} {}", self.method, self.path)?; @@ -818,7 +834,7 @@ impl Default for ResponseHeaders<'_, N> { } impl Display for ResponseHeaders<'_, N> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!(f, "{} ", if self.http11 { "HTTP/1.1 " } else { "HTTP/1.0" })?; writeln!(f, "{} {}", self.code, self.reason.unwrap_or(""))?; @@ -837,10 +853,6 @@ impl Display for ResponseHeaders<'_, N> { /// Websocket utilities pub mod ws { - use core::fmt; - - use log::debug; - use crate::Method; pub const NONCE_LEN: usize = 16; @@ -913,8 +925,8 @@ pub mod ws { UnsupportedVersion, } - impl fmt::Display for UpgradeError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + impl core::fmt::Display for UpgradeError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Self::NoVersion => write!(f, "No Sec-WebSocket-Version header"), Self::NoSecKey => write!(f, "No Sec-WebSocket-Key header"), @@ -1035,7 +1047,7 @@ pub mod ws { } fn sec_key_response_start(sec_key: &str, sha1: &mut sha1_smol::Sha1) { - debug!("Computing response for key: {sec_key}"); + debug!("Computing response for key: {}", sec_key); sha1.update(sec_key.as_bytes()); } @@ -1049,7 +1061,7 @@ pub mod ws { let sec_key_response = unsafe { core::str::from_utf8_unchecked(&buf[..len]) }; - debug!("Computed response: {sec_key_response}"); + debug!("Computed response: {}", sec_key_response); sec_key_response } @@ -1074,86 +1086,105 @@ mod test { fn test_resolve_conn() { // Default connection type resolution assert_eq!( - ConnectionType::resolve(None, None, true).unwrap(), + unwrap!(ConnectionType::resolve(None, None, true)), ConnectionType::KeepAlive ); assert_eq!( - ConnectionType::resolve(None, None, false).unwrap(), + unwrap!(ConnectionType::resolve(None, None, false)), ConnectionType::Close ); // Connection type resolution based on carry-over (for responses) assert_eq!( - ConnectionType::resolve(None, Some(ConnectionType::KeepAlive), false).unwrap(), + unwrap!(ConnectionType::resolve( + None, + Some(ConnectionType::KeepAlive), + false + )), ConnectionType::KeepAlive ); assert_eq!( - ConnectionType::resolve(None, Some(ConnectionType::KeepAlive), true).unwrap(), + unwrap!(ConnectionType::resolve( + None, + Some(ConnectionType::KeepAlive), + true + )), ConnectionType::KeepAlive ); // Connection type resoluton based on the header value assert_eq!( - ConnectionType::resolve(Some(ConnectionType::Close), None, false).unwrap(), + unwrap!(ConnectionType::resolve( + Some(ConnectionType::Close), + None, + false + )), ConnectionType::Close ); assert_eq!( - ConnectionType::resolve(Some(ConnectionType::KeepAlive), None, false).unwrap(), + unwrap!(ConnectionType::resolve( + Some(ConnectionType::KeepAlive), + None, + false + )), ConnectionType::KeepAlive ); assert_eq!( - ConnectionType::resolve(Some(ConnectionType::Close), None, true).unwrap(), + unwrap!(ConnectionType::resolve( + Some(ConnectionType::Close), + None, + true + )), ConnectionType::Close ); assert_eq!( - ConnectionType::resolve(Some(ConnectionType::KeepAlive), None, true).unwrap(), + unwrap!(ConnectionType::resolve( + Some(ConnectionType::KeepAlive), + None, + true + )), ConnectionType::KeepAlive ); // Connection type in the headers should aggree with the carry-over one assert_eq!( - ConnectionType::resolve( + unwrap!(ConnectionType::resolve( Some(ConnectionType::Close), Some(ConnectionType::Close), false - ) - .unwrap(), + )), ConnectionType::Close ); assert_eq!( - ConnectionType::resolve( + unwrap!(ConnectionType::resolve( Some(ConnectionType::KeepAlive), Some(ConnectionType::KeepAlive), false - ) - .unwrap(), + )), ConnectionType::KeepAlive ); assert_eq!( - ConnectionType::resolve( + unwrap!(ConnectionType::resolve( Some(ConnectionType::Close), Some(ConnectionType::Close), true - ) - .unwrap(), + )), ConnectionType::Close ); assert_eq!( - ConnectionType::resolve( + unwrap!(ConnectionType::resolve( Some(ConnectionType::KeepAlive), Some(ConnectionType::KeepAlive), true - ) - .unwrap(), + )), ConnectionType::KeepAlive ); assert_eq!( - ConnectionType::resolve( + unwrap!(ConnectionType::resolve( Some(ConnectionType::Close), Some(ConnectionType::KeepAlive), false - ) - .unwrap(), + )), ConnectionType::Close ); assert!(ConnectionType::resolve( @@ -1163,12 +1194,11 @@ mod test { ) .is_err()); assert_eq!( - ConnectionType::resolve( + unwrap!(ConnectionType::resolve( Some(ConnectionType::Close), Some(ConnectionType::KeepAlive), true - ) - .unwrap(), + )), ConnectionType::Close ); assert!(ConnectionType::resolve( @@ -1183,23 +1213,53 @@ mod test { fn test_resolve_body() { // Request with no body type specified means Content-Length=0 assert_eq!( - BodyType::resolve(None, ConnectionType::KeepAlive, true, true, false).unwrap(), + unwrap!(BodyType::resolve( + None, + ConnectionType::KeepAlive, + true, + true, + false + )), BodyType::ContentLen(0) ); assert_eq!( - BodyType::resolve(None, ConnectionType::Close, true, true, false).unwrap(), + unwrap!(BodyType::resolve( + None, + ConnectionType::Close, + true, + true, + false + )), BodyType::ContentLen(0) ); assert_eq!( - BodyType::resolve(None, ConnectionType::KeepAlive, true, false, false).unwrap(), + unwrap!(BodyType::resolve( + None, + ConnectionType::KeepAlive, + true, + false, + false + )), BodyType::ContentLen(0) ); assert_eq!( - BodyType::resolve(None, ConnectionType::Close, true, false, false).unwrap(), + unwrap!(BodyType::resolve( + None, + ConnectionType::Close, + true, + false, + false + )), BodyType::ContentLen(0) ); assert_eq!( - BodyType::resolve(None, ConnectionType::Upgrade, false, true, false).unwrap(), + unwrap!(BodyType::resolve( + None, + ConnectionType::Upgrade, + false, + true, + false + )), BodyType::ContentLen(0) ); @@ -1295,54 +1355,88 @@ mod test { // The same, but with a Close connection IS allowed assert_eq!( - BodyType::resolve( + unwrap!(BodyType::resolve( Some(BodyType::Raw), ConnectionType::Close, false, true, false - ) - .unwrap(), + )), BodyType::Raw ); assert_eq!( - BodyType::resolve( + unwrap!(BodyType::resolve( Some(BodyType::Raw), ConnectionType::Close, false, false, false - ) - .unwrap(), + )), BodyType::Raw ); // Request upgrades to chunked encoding should only work for HTTP1.1, and if there is no body type in the headers assert_eq!( - BodyType::resolve(None, ConnectionType::Close, true, true, true).unwrap(), + unwrap!(BodyType::resolve( + None, + ConnectionType::Close, + true, + true, + true + )), BodyType::Chunked ); assert_eq!( - BodyType::resolve(None, ConnectionType::KeepAlive, true, true, true).unwrap(), + unwrap!(BodyType::resolve( + None, + ConnectionType::KeepAlive, + true, + true, + true + )), BodyType::Chunked ); assert_eq!( - BodyType::resolve(None, ConnectionType::Close, true, false, true).unwrap(), + unwrap!(BodyType::resolve( + None, + ConnectionType::Close, + true, + false, + true + )), BodyType::ContentLen(0) ); assert_eq!( - BodyType::resolve(None, ConnectionType::KeepAlive, true, false, true).unwrap(), + unwrap!(BodyType::resolve( + None, + ConnectionType::KeepAlive, + true, + false, + true + )), BodyType::ContentLen(0) ); // Response upgrades to chunked encoding should only work for HTTP1.1, and if there is no body type in the headers, and if the connection is KeepAlive assert_eq!( - BodyType::resolve(None, ConnectionType::KeepAlive, false, true, true).unwrap(), + unwrap!(BodyType::resolve( + None, + ConnectionType::KeepAlive, + false, + true, + true + )), BodyType::Chunked ); // Response upgrades should not be honored if the connection is Close assert_eq!( - BodyType::resolve(None, ConnectionType::Close, false, true, true).unwrap(), + unwrap!(BodyType::resolve( + None, + ConnectionType::Close, + false, + true, + true + )), BodyType::Raw ); } diff --git a/edge-mdns/CHANGELOG.md b/edge-mdns/CHANGELOG.md index d6ae008..94186ab 100644 --- a/edge-mdns/CHANGELOG.md +++ b/edge-mdns/CHANGELOG.md @@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +* Optional `defmt` support via two new features (one has to specify one, or the other, or neither, but not both): + * `log` - uses the `log` crate for all logging + * `defmt` - uses the `defmt` crate for all logging, and implements `defmt::Format` for all library types that otherwise implement `Debug` and/or `Display` ## [0.5.0] - 2025-01-15 * Updated dependencies for compatibility with `embassy-time-driver` v0.2 diff --git a/edge-mdns/Cargo.toml b/edge-mdns/Cargo.toml index dd836b1..2ab63dc 100644 --- a/edge-mdns/Cargo.toml +++ b/edge-mdns/Cargo.toml @@ -17,9 +17,11 @@ categories = [ default = ["io"] std = ["io"] io = ["embassy-futures", "embassy-sync", "embassy-time", "edge-nal"] +defmt = ["dep:defmt", "heapless/defmt-03"] [dependencies] -log = { workspace = true } +log = { version = "0.4", default-features = false, optional = true } +defmt = { version = "0.3", optional = true, features = ["ip_in_core"] } heapless = { workspace = true } domain = { workspace = true } embassy-futures = { workspace = true, optional = true } diff --git a/edge-mdns/src/buf.rs b/edge-mdns/src/buf.rs index 71a97ed..6f796a2 100644 --- a/edge-mdns/src/buf.rs +++ b/edge-mdns/src/buf.rs @@ -96,7 +96,7 @@ where async fn get(&self) -> Option> { let mut guard = self.0.lock().await; - guard.resize_default(N).unwrap(); + unwrap!(guard.resize_default(N)); Some(VecBuf(guard)) } diff --git a/edge-mdns/src/fmt.rs b/edge-mdns/src/fmt.rs new file mode 100644 index 0000000..8bd7963 --- /dev/null +++ b/edge-mdns/src/fmt.rs @@ -0,0 +1,340 @@ +//! A module that re-exports the `log` macros if `defmt` is not enabled, or `defmt` macros if `defmt` is enabled. +//! +//! The module also defines: +//! - Custom versions of the core assert macros (`assert!`, `assert_eq!`, etc.) that use the `defmt` macros if the `defmt` feature is enabled. +//! - Custom versions of the `panic!`, `todo!`, and `unreachable!` macros that use the `defmt` macros if the `defmt` feature is enabled. +//! - A custom `unwrap!` macro that uses the `defmt` macros if the `defmt` feature is enabled, otherwise it uses the standard library's `unwrap` method. +//! - A custom `Bytes` struct that formats byte slices as hex in a way compatible with `defmt`. +#![macro_use] +#![allow(unused)] + +use core::fmt::{Debug, Display, LowerHex}; + +#[collapse_debuginfo(yes)] +macro_rules! assert { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! assert_eq { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert_eq!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert_eq!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! assert_ne { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert_ne!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert_ne!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! debug_assert { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! debug_assert_eq { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert_eq!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert_eq!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! debug_assert_ne { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert_ne!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert_ne!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! todo { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::todo!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::todo!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! unreachable { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::unreachable!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::unreachable!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! panic { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::panic!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::panic!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! trace { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::trace!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::trace!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! debug { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::debug!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::debug!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! info { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::info!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::info!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! warn { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::warn!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::warn!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! error { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::error!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::error!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[cfg(feature = "defmt")] +#[collapse_debuginfo(yes)] +macro_rules! unwrap { + ($($x:tt)*) => { + ::defmt::unwrap!($($x)*) + }; +} + +#[cfg(not(feature = "defmt"))] +#[collapse_debuginfo(yes)] +macro_rules! unwrap { + ($arg:expr) => { + match $crate::fmt::Try::into_result($arg) { + ::core::result::Result::Ok(t) => t, + ::core::result::Result::Err(e) => { + ::core::panic!("unwrap of `{}` failed: {:?}", ::core::stringify!($arg), e); + } + } + }; + ($arg:expr, $($msg:expr),+ $(,)? ) => { + match $crate::fmt::Try::into_result($arg) { + ::core::result::Result::Ok(t) => t, + ::core::result::Result::Err(e) => { + ::core::panic!("unwrap of `{}` failed: {}: {:?}", ::core::stringify!($arg), ::core::format_args!($($msg,)*), e); + } + } + } +} + +#[cfg(feature = "defmt")] +#[collapse_debuginfo(yes)] +macro_rules! write_unwrap { + ($f:expr, $s:literal $(, $x:expr)* $(,)?) => { + { + unwrap!(write!($f, $s $(, $x)*).map_err($crate::fmt::FmtError)); + } + }; +} + +#[cfg(not(feature = "defmt"))] +#[collapse_debuginfo(yes)] +macro_rules! write_unwrap { + ($f:expr, $s:literal $(, $x:expr)* $(,)?) => { + { + unwrap!(write!($f, $s $(, $x)*)); + } + }; +} + +#[cfg(feature = "defmt")] +#[collapse_debuginfo(yes)] +macro_rules! display2format { + ($arg:expr) => { + ::defmt::Display2Format(&$arg) + }; +} + +#[cfg(not(feature = "defmt"))] +#[collapse_debuginfo(yes)] +macro_rules! display2format { + ($arg:expr) => { + $arg + }; +} + +#[cfg(feature = "defmt")] +#[collapse_debuginfo(yes)] +macro_rules! debug2format { + ($arg:expr) => { + ::defmt::Debug2Format(&$arg) + }; +} + +#[cfg(not(feature = "defmt"))] +#[collapse_debuginfo(yes)] +macro_rules! debug2format { + ($arg:expr) => { + $arg + }; +} + +/// A way to `{:x?}` format a byte slice which is compatible with `defmt` +pub struct Bytes<'a>(pub &'a [u8]); + +impl Debug for Bytes<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#02x?}", self.0) + } +} + +impl Display for Bytes<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#02x?}", self.0) + } +} + +impl LowerHex for Bytes<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#02x?}", self.0) + } +} + +#[cfg(feature = "defmt")] +impl defmt::Format for Bytes<'_> { + fn format(&self, fmt: defmt::Formatter) { + defmt::write!(fmt, "{:02x}", self.0) + } +} + +/// Support for the `unwrap!` macro for `Option` and `Result`. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct NoneError; + +/// Support for the `unwrap!` macro for `Option` and `Result`. +pub trait Try { + type Ok; + type Error; + #[allow(unused)] + fn into_result(self) -> Result; +} + +impl Try for Option { + type Ok = T; + type Error = NoneError; + + #[inline] + fn into_result(self) -> Result { + self.ok_or(NoneError) + } +} + +impl Try for Result { + type Ok = T; + type Error = E; + + #[inline] + fn into_result(self) -> Self { + self + } +} + +#[cfg(feature = "defmt")] +pub(crate) struct FmtError(pub(crate) core::fmt::Error); + +#[cfg(feature = "defmt")] +impl defmt::Format for FmtError { + fn format(&self, f: defmt::Formatter<'_>) { + defmt::write!(f, "{}", "FmtError") + } +} diff --git a/edge-mdns/src/host.rs b/edge-mdns/src/host.rs index ae92218..1681499 100644 --- a/edge-mdns/src/host.rs +++ b/edge-mdns/src/host.rs @@ -10,6 +10,7 @@ use crate::{HostAnswer, HostAnswers, MdnsError, NameSlice, RecordDataChain, Txt, /// This structure implements the `HostAnswers` trait, which allows it to be used /// as a responder for mDNS queries coming from other network peers. #[derive(Debug, Clone)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct Host<'a> { /// The name of the host. I.e. a name "foo" will be pingable as "foo.local" pub hostname: &'a str, @@ -20,6 +21,7 @@ pub struct Host<'a> { /// Leaving it as `Ipv6Addr::UNSPECIFIED` means that the host will not aswer it to AAAA queries. pub ipv6: Ipv6Addr, /// The time-to-live of the mDNS answers. + #[cfg_attr(feature = "defmt", defmt(Debug2Format))] pub ttl: Ttl, } @@ -73,6 +75,7 @@ impl HostAnswers for Host<'_> { /// implements the `HostAnswers` trait, which allows it to be used as a responder for mDNS queries /// coming from other network peers. #[derive(Debug, Clone)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct Service<'a> { /// The name of the service. pub name: &'a str, diff --git a/edge-mdns/src/io.rs b/edge-mdns/src/io.rs index cd7ce6c..c11f418 100644 --- a/edge-mdns/src/io.rs +++ b/edge-mdns/src/io.rs @@ -1,5 +1,4 @@ use core::cell::RefCell; -use core::fmt; use core::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}; use core::pin::pin; @@ -16,8 +15,6 @@ use edge_nal::{MulticastV4, MulticastV6, Readable, UdpBind, UdpReceive, UdpSend} use embassy_time::{Duration, Timer}; -use log::{debug, warn}; - use super::*; /// Socket address that binds to any IPv4-configured interface available @@ -73,11 +70,11 @@ impl From for MdnsIoError { } } -impl fmt::Display for MdnsIoError +impl core::fmt::Display for MdnsIoError where - E: fmt::Display, + E: core::fmt::Display, { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Self::MdnsError(err) => write!(f, "mDNS error: {}", err), Self::NoRecvBufError => write!(f, "No recv buf available"), @@ -87,6 +84,21 @@ where } } +#[cfg(feature = "defmt")] +impl defmt::Format for MdnsIoError +where + E: defmt::Format, +{ + fn format(&self, f: defmt::Formatter<'_>) { + match self { + Self::MdnsError(err) => defmt::write!(f, "mDNS error: {}", err), + Self::NoRecvBufError => defmt::write!(f, "No recv buf available"), + Self::NoSendBufError => defmt::write!(f, "No send buf available"), + Self::IoError(err) => defmt::write!(f, "IO error: {}", err), + } + } +} + #[cfg(feature = "std")] impl std::error::Error for MdnsIoError where E: std::error::Error {} @@ -305,7 +317,7 @@ where .await .map_err(MdnsIoError::IoError)?; - debug!("Got mDNS query from {remote}"); + debug!("Got mDNS query from {}", remote); { let mut send_buf = self @@ -330,7 +342,7 @@ where Ok(len) => len, Err(err) => match err { MdnsError::InvalidMessage => { - warn!("Got invalid message from {remote}, skipping"); + warn!("Got invalid message from {}, skipping", remote); continue; } other => Err(other)?, @@ -342,10 +354,13 @@ where // Support one-shot legacy queries by replying privately // to the remote address, if the query was not sent from the mDNS port (as per the spec) - debug!("Replying privately to a one-shot mDNS query from {remote}"); + debug!( + "Replying privately to a one-shot mDNS query from {}", + remote + ); if let Err(err) = send.send(remote, data).await { - warn!("Failed to reply privately to {remote}: {err:?}"); + warn!("Failed to reply privately to {}: {:?}", remote, err); } } else { // Otherwise, re-broadcast the response @@ -354,7 +369,7 @@ where self.delay().await; } - debug!("Re-broadcasting due to mDNS query from {remote}"); + debug!("Re-broadcasting due to mDNS query from {}", remote); self.broadcast_once(send, data).await?; } @@ -382,7 +397,7 @@ where ) { if !data.is_empty() { - debug!("Broadcasting mDNS entry to {remote_addr}"); + debug!("Broadcasting mDNS entry to {}", remote_addr); let fut = pin!(send.send(remote_addr, data)); diff --git a/edge-mdns/src/lib.rs b/edge-mdns/src/lib.rs index d6748ed..7a4b488 100644 --- a/edge-mdns/src/lib.rs +++ b/edge-mdns/src/lib.rs @@ -3,7 +3,7 @@ #![allow(async_fn_in_trait)] use core::cmp::Ordering; -use core::fmt::{self, Display}; +use core::fmt::Display; use core::ops::RangeBounds; use domain::base::header::Flags; @@ -19,7 +19,8 @@ use domain::base::{ use domain::dep::octseq::{FreezeBuilder, FromBuilder, Octets, OctetsBuilder, ShortBuf, Truncate}; use domain::rdata::AllRecordData; -use log::debug; +// This mod MUST go first, so that the others see its macros. +pub(crate) mod fmt; #[cfg(feature = "io")] pub mod buf; // TODO: Maybe move to a generic `edge-buf` crate in future @@ -44,7 +45,7 @@ pub enum MdnsError { } impl Display for MdnsError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Self::ShortBuf => write!(f, "ShortBuf"), Self::InvalidMessage => write!(f, "InvalidMessage"), @@ -52,6 +53,16 @@ impl Display for MdnsError { } } +#[cfg(feature = "defmt")] +impl defmt::Format for MdnsError { + fn format(&self, f: defmt::Formatter<'_>) { + match self { + Self::ShortBuf => defmt::write!(f, "ShortBuf"), + Self::InvalidMessage => defmt::write!(f, "InvalidMessage"), + } + } +} + #[cfg(feature = "std")] impl std::error::Error for MdnsError {} @@ -99,8 +110,8 @@ impl<'a> NameSlice<'a> { } } -impl fmt::Display for NameSlice<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +impl core::fmt::Display for NameSlice<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { for label in self.0 { write!(f, "{}.", label)?; } @@ -109,6 +120,15 @@ impl fmt::Display for NameSlice<'_> { } } +#[cfg(feature = "defmt")] +impl defmt::Format for NameSlice<'_> { + fn format(&self, f: defmt::Formatter<'_>) { + for label in self.0 { + defmt::write!(f, "{}.", label); + } + } +} + impl ToName for NameSlice<'_> {} /// An iterator over the labels in a `NameSlice` instance. @@ -124,7 +144,10 @@ impl<'a> Iterator for NameSliceIter<'a> { fn next(&mut self) -> Option { match self.index.cmp(&self.name.0.len()) { Ordering::Less => { - let label = Label::from_slice(self.name.0[self.index].as_bytes()).unwrap(); + let label = unwrap!( + Label::from_slice(self.name.0[self.index].as_bytes()), + "Unreachable" + ); self.index += 1; Some(label) } @@ -146,7 +169,10 @@ impl DoubleEndedIterator for NameSliceIter<'_> { let label = Label::root(); Some(label) } else { - let label = Label::from_slice(self.name.0[self.index].as_bytes()).unwrap(); + let label = unwrap!( + Label::from_slice(self.name.0[self.index].as_bytes()), + "Unreachable" + ); Some(label) } } else { @@ -180,16 +206,16 @@ impl<'a> Txt<'a> { } } -impl fmt::Display for Txt<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +impl core::fmt::Display for Txt<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!(f, "Txt [")?; for (i, (k, v)) in self.0.iter().enumerate() { if i > 0 { - write!(f, ", ")?; + write!(f, ", {}={}", k, v)?; + } else { + write!(f, "{}={}", k, v)?; } - - write!(f, "{}={}", k, v)?; } write!(f, "]")?; @@ -198,6 +224,23 @@ impl fmt::Display for Txt<'_> { } } +#[cfg(feature = "defmt")] +impl defmt::Format for Txt<'_> { + fn format(&self, f: defmt::Formatter<'_>) { + defmt::write!(f, "Txt ["); + + for (i, (k, v)) in self.0.iter().enumerate() { + if i > 0 { + defmt::write!(f, ", {}={}", k, v); + } else { + defmt::write!(f, "{}={}", k, v); + } + } + + defmt::write!(f, "]"); + } +} + impl RecordData for Txt<'_> { fn rtype(&self) -> Rtype { Rtype::TXT @@ -244,12 +287,12 @@ pub enum RecordDataChain { Next(U), } -impl fmt::Display for RecordDataChain +impl core::fmt::Display for RecordDataChain where - T: fmt::Display, - U: fmt::Display, + T: core::fmt::Display, + U: core::fmt::Display, { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Self::This(data) => write!(f, "{}", data), Self::Next(data) => write!(f, "{}", data), @@ -257,6 +300,20 @@ where } } +#[cfg(feature = "defmt")] +impl defmt::Format for RecordDataChain +where + T: defmt::Format, + U: defmt::Format, +{ + fn format(&self, f: defmt::Formatter<'_>) { + match self { + Self::This(data) => defmt::write!(f, "{}", data), + Self::Next(data) => defmt::write!(f, "{}", data), + } + } +} + impl RecordData for RecordDataChain where T: RecordData, @@ -822,7 +879,7 @@ where } if question.qname().name_eq(&answer.owner()) { - debug!("Answering question [{question}] with: [{answer}]"); + debug!("Answering question [{}] with: [{}]", question, answer); ab.push(answer)?; @@ -847,7 +904,7 @@ where | RecordDataChain::Next(AllRecordData::Txt(_)) | RecordDataChain::This(Txt(_)) ) { - debug!("Additional answer: [{answer}]"); + debug!("Additional answer: [{}]", answer); aa.push(answer)?; diff --git a/edge-mqtt/Cargo.toml b/edge-mqtt/Cargo.toml index 47b1c77..03925a1 100644 --- a/edge-mqtt/Cargo.toml +++ b/edge-mqtt/Cargo.toml @@ -15,5 +15,5 @@ categories = [ [dependencies] rumqttc = "0.23" -log = { workspace = true } +log = { version = "0.4", default-features = false } embedded-svc = { workspace = true, optional = true, default-features = false, features = ["std"] } diff --git a/edge-nal-embassy/CHANGELOG.md b/edge-nal-embassy/CHANGELOG.md index b1b5abd..8dc0254 100644 --- a/edge-nal-embassy/CHANGELOG.md +++ b/edge-nal-embassy/CHANGELOG.md @@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +* Optional `defmt` support via two new features (one has to specify one, or the other, or neither, but not both): + * `log` - uses the `log` crate for all logging + * `defmt` - uses the `defmt` crate for all logging, and implements `defmt::Format` for all library types that otherwise implement `Debug` and/or `Display` ## [0.5.0] - 2025-01-15 * Updated dependencies for compatibility with `embassy-time-driver` v0.2 diff --git a/edge-nal-embassy/Cargo.toml b/edge-nal-embassy/Cargo.toml index 5f7fc52..6d403b6 100644 --- a/edge-nal-embassy/Cargo.toml +++ b/edge-nal-embassy/Cargo.toml @@ -14,7 +14,12 @@ categories = [ "network-programming", ] +[features] +defmt = ["dep:defmt", "heapless/defmt-03", "embassy-net/defmt"] + [dependencies] +log = { version = "0.4", default-features = false, optional = true } +defmt = { version = "0.3", optional = true } embedded-io-async = { workspace = true } edge-nal = { workspace = true } heapless = { workspace = true } diff --git a/edge-nal-embassy/src/dns.rs b/edge-nal-embassy/src/dns.rs index 865ff6b..efae38f 100644 --- a/edge-nal-embassy/src/dns.rs +++ b/edge-nal-embassy/src/dns.rs @@ -52,6 +52,7 @@ impl edge_nal::Dns for Dns<'_> { } #[derive(PartialEq, Eq, Clone, Copy, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct DnsError(Error); impl From for DnsError { diff --git a/edge-nal-embassy/src/fmt.rs b/edge-nal-embassy/src/fmt.rs new file mode 100644 index 0000000..8bd7963 --- /dev/null +++ b/edge-nal-embassy/src/fmt.rs @@ -0,0 +1,340 @@ +//! A module that re-exports the `log` macros if `defmt` is not enabled, or `defmt` macros if `defmt` is enabled. +//! +//! The module also defines: +//! - Custom versions of the core assert macros (`assert!`, `assert_eq!`, etc.) that use the `defmt` macros if the `defmt` feature is enabled. +//! - Custom versions of the `panic!`, `todo!`, and `unreachable!` macros that use the `defmt` macros if the `defmt` feature is enabled. +//! - A custom `unwrap!` macro that uses the `defmt` macros if the `defmt` feature is enabled, otherwise it uses the standard library's `unwrap` method. +//! - A custom `Bytes` struct that formats byte slices as hex in a way compatible with `defmt`. +#![macro_use] +#![allow(unused)] + +use core::fmt::{Debug, Display, LowerHex}; + +#[collapse_debuginfo(yes)] +macro_rules! assert { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! assert_eq { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert_eq!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert_eq!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! assert_ne { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert_ne!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert_ne!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! debug_assert { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! debug_assert_eq { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert_eq!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert_eq!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! debug_assert_ne { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert_ne!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert_ne!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! todo { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::todo!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::todo!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! unreachable { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::unreachable!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::unreachable!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! panic { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::panic!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::panic!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! trace { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::trace!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::trace!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! debug { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::debug!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::debug!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! info { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::info!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::info!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! warn { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::warn!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::warn!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! error { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::error!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::error!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[cfg(feature = "defmt")] +#[collapse_debuginfo(yes)] +macro_rules! unwrap { + ($($x:tt)*) => { + ::defmt::unwrap!($($x)*) + }; +} + +#[cfg(not(feature = "defmt"))] +#[collapse_debuginfo(yes)] +macro_rules! unwrap { + ($arg:expr) => { + match $crate::fmt::Try::into_result($arg) { + ::core::result::Result::Ok(t) => t, + ::core::result::Result::Err(e) => { + ::core::panic!("unwrap of `{}` failed: {:?}", ::core::stringify!($arg), e); + } + } + }; + ($arg:expr, $($msg:expr),+ $(,)? ) => { + match $crate::fmt::Try::into_result($arg) { + ::core::result::Result::Ok(t) => t, + ::core::result::Result::Err(e) => { + ::core::panic!("unwrap of `{}` failed: {}: {:?}", ::core::stringify!($arg), ::core::format_args!($($msg,)*), e); + } + } + } +} + +#[cfg(feature = "defmt")] +#[collapse_debuginfo(yes)] +macro_rules! write_unwrap { + ($f:expr, $s:literal $(, $x:expr)* $(,)?) => { + { + unwrap!(write!($f, $s $(, $x)*).map_err($crate::fmt::FmtError)); + } + }; +} + +#[cfg(not(feature = "defmt"))] +#[collapse_debuginfo(yes)] +macro_rules! write_unwrap { + ($f:expr, $s:literal $(, $x:expr)* $(,)?) => { + { + unwrap!(write!($f, $s $(, $x)*)); + } + }; +} + +#[cfg(feature = "defmt")] +#[collapse_debuginfo(yes)] +macro_rules! display2format { + ($arg:expr) => { + ::defmt::Display2Format(&$arg) + }; +} + +#[cfg(not(feature = "defmt"))] +#[collapse_debuginfo(yes)] +macro_rules! display2format { + ($arg:expr) => { + $arg + }; +} + +#[cfg(feature = "defmt")] +#[collapse_debuginfo(yes)] +macro_rules! debug2format { + ($arg:expr) => { + ::defmt::Debug2Format(&$arg) + }; +} + +#[cfg(not(feature = "defmt"))] +#[collapse_debuginfo(yes)] +macro_rules! debug2format { + ($arg:expr) => { + $arg + }; +} + +/// A way to `{:x?}` format a byte slice which is compatible with `defmt` +pub struct Bytes<'a>(pub &'a [u8]); + +impl Debug for Bytes<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#02x?}", self.0) + } +} + +impl Display for Bytes<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#02x?}", self.0) + } +} + +impl LowerHex for Bytes<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#02x?}", self.0) + } +} + +#[cfg(feature = "defmt")] +impl defmt::Format for Bytes<'_> { + fn format(&self, fmt: defmt::Formatter) { + defmt::write!(fmt, "{:02x}", self.0) + } +} + +/// Support for the `unwrap!` macro for `Option` and `Result`. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct NoneError; + +/// Support for the `unwrap!` macro for `Option` and `Result`. +pub trait Try { + type Ok; + type Error; + #[allow(unused)] + fn into_result(self) -> Result; +} + +impl Try for Option { + type Ok = T; + type Error = NoneError; + + #[inline] + fn into_result(self) -> Result { + self.ok_or(NoneError) + } +} + +impl Try for Result { + type Ok = T; + type Error = E; + + #[inline] + fn into_result(self) -> Self { + self + } +} + +#[cfg(feature = "defmt")] +pub(crate) struct FmtError(pub(crate) core::fmt::Error); + +#[cfg(feature = "defmt")] +impl defmt::Format for FmtError { + fn format(&self, f: defmt::Formatter<'_>) { + defmt::write!(f, "{}", "FmtError") + } +} diff --git a/edge-nal-embassy/src/lib.rs b/edge-nal-embassy/src/lib.rs index 9722dc6..8f3aced 100644 --- a/edge-nal-embassy/src/lib.rs +++ b/edge-nal-embassy/src/lib.rs @@ -13,6 +13,9 @@ pub use dns::*; pub use tcp::*; pub use udp::*; +// This mod MUST go first, so that the others see its macros. +pub(crate) mod fmt; + mod dns; mod tcp; mod udp; diff --git a/edge-nal-embassy/src/tcp.rs b/edge-nal-embassy/src/tcp.rs index fd8df39..d169d64 100644 --- a/edge-nal-embassy/src/tcp.rs +++ b/edge-nal-embassy/src/tcp.rs @@ -84,7 +84,7 @@ impl edge_nal::TcpAccept socket.socket.accept(to_emb_bind_socket(self.local)).await?; - let local_endpoint = socket.socket.local_endpoint().unwrap(); + let local_endpoint = unwrap!(socket.socket.local_endpoint()); Ok((to_net_socket(local_endpoint), socket)) } @@ -280,6 +280,7 @@ impl TcpSplit /// A shared error type that is used by the TCP factory traits implementation as well as the TCP socket #[derive(PartialEq, Eq, Clone, Copy, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum TcpError { General(Error), Connect(ConnectError), diff --git a/edge-nal-embassy/src/udp.rs b/edge-nal-embassy/src/udp.rs index 65b2cb5..a269555 100644 --- a/edge-nal-embassy/src/udp.rs +++ b/edge-nal-embassy/src/udp.rs @@ -73,7 +73,7 @@ impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize, const M: usize> stack_buffers: &'d UdpBuffers, ) -> Result { let mut socket_buffers = stack_buffers.pool.alloc().ok_or(UdpError::NoBuffers)?; - let mut socket_meta_buffers = stack_buffers.meta_pool.alloc().unwrap(); + let mut socket_meta_buffers = unwrap!(stack_buffers.meta_pool.alloc()); Ok(Self { stack, @@ -247,6 +247,7 @@ impl Rea /// A shared error type that is used by the UDP factory trait implementation as well as the UDP socket #[derive(PartialEq, Eq, Clone, Copy, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum UdpError { Recv(RecvError), Send(SendError), diff --git a/edge-nal-std/Cargo.toml b/edge-nal-std/Cargo.toml index 37728ed..5ad7a5e 100644 --- a/edge-nal-std/Cargo.toml +++ b/edge-nal-std/Cargo.toml @@ -15,6 +15,7 @@ categories = [ ] [dependencies] +log = { version = "0.4", default-features = true } embedded-io-async = { workspace = true, features = ["std"] } edge-nal = { workspace = true } async-io = "2" diff --git a/edge-raw/CHANGELOG.md b/edge-raw/CHANGELOG.md index 0b4e46f..5e43fe7 100644 --- a/edge-raw/CHANGELOG.md +++ b/edge-raw/CHANGELOG.md @@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +* Optional `defmt` support via two new features (one has to specify one, or the other, or neither, but not both): + * `log` - uses the `log` crate for all logging + * `defmt` - uses the `defmt` crate for all logging, and implements `defmt::Format` for all library types that otherwise implement `Debug` and/or `Display` ## [0.5.0] - 2025-01-15 * Updated dependencies for compatibility with `embassy-time-driver` v0.2 diff --git a/edge-raw/Cargo.toml b/edge-raw/Cargo.toml index c10633a..a410152 100644 --- a/edge-raw/Cargo.toml +++ b/edge-raw/Cargo.toml @@ -20,6 +20,7 @@ std = ["io"] io = ["embedded-io-async", "edge-nal"] [dependencies] -log = { workspace = true } +log = { version = "0.4", default-features = false, optional = true } +defmt = { version = "0.3", optional = true } embedded-io-async = { workspace = true, default-features = false, optional = true } edge-nal = { workspace = true, default-features = false, optional = true } diff --git a/edge-raw/src/bytes.rs b/edge-raw/src/bytes.rs index ae0e208..e646d20 100644 --- a/edge-raw/src/bytes.rs +++ b/edge-raw/src/bytes.rs @@ -48,7 +48,7 @@ impl<'a> BytesIn<'a> { } pub fn remaining(&mut self) -> &'a [u8] { - let data = self.slice(self.data.len() - self.offset).unwrap(); + let data = unwrap!(self.slice(self.data.len() - self.offset), "Unreachable"); self.offset = self.data.len(); diff --git a/edge-raw/src/fmt.rs b/edge-raw/src/fmt.rs new file mode 100644 index 0000000..8bd7963 --- /dev/null +++ b/edge-raw/src/fmt.rs @@ -0,0 +1,340 @@ +//! A module that re-exports the `log` macros if `defmt` is not enabled, or `defmt` macros if `defmt` is enabled. +//! +//! The module also defines: +//! - Custom versions of the core assert macros (`assert!`, `assert_eq!`, etc.) that use the `defmt` macros if the `defmt` feature is enabled. +//! - Custom versions of the `panic!`, `todo!`, and `unreachable!` macros that use the `defmt` macros if the `defmt` feature is enabled. +//! - A custom `unwrap!` macro that uses the `defmt` macros if the `defmt` feature is enabled, otherwise it uses the standard library's `unwrap` method. +//! - A custom `Bytes` struct that formats byte slices as hex in a way compatible with `defmt`. +#![macro_use] +#![allow(unused)] + +use core::fmt::{Debug, Display, LowerHex}; + +#[collapse_debuginfo(yes)] +macro_rules! assert { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! assert_eq { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert_eq!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert_eq!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! assert_ne { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert_ne!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert_ne!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! debug_assert { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! debug_assert_eq { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert_eq!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert_eq!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! debug_assert_ne { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert_ne!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert_ne!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! todo { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::todo!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::todo!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! unreachable { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::unreachable!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::unreachable!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! panic { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::panic!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::panic!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! trace { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::trace!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::trace!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! debug { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::debug!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::debug!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! info { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::info!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::info!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! warn { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::warn!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::warn!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! error { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::error!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::error!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[cfg(feature = "defmt")] +#[collapse_debuginfo(yes)] +macro_rules! unwrap { + ($($x:tt)*) => { + ::defmt::unwrap!($($x)*) + }; +} + +#[cfg(not(feature = "defmt"))] +#[collapse_debuginfo(yes)] +macro_rules! unwrap { + ($arg:expr) => { + match $crate::fmt::Try::into_result($arg) { + ::core::result::Result::Ok(t) => t, + ::core::result::Result::Err(e) => { + ::core::panic!("unwrap of `{}` failed: {:?}", ::core::stringify!($arg), e); + } + } + }; + ($arg:expr, $($msg:expr),+ $(,)? ) => { + match $crate::fmt::Try::into_result($arg) { + ::core::result::Result::Ok(t) => t, + ::core::result::Result::Err(e) => { + ::core::panic!("unwrap of `{}` failed: {}: {:?}", ::core::stringify!($arg), ::core::format_args!($($msg,)*), e); + } + } + } +} + +#[cfg(feature = "defmt")] +#[collapse_debuginfo(yes)] +macro_rules! write_unwrap { + ($f:expr, $s:literal $(, $x:expr)* $(,)?) => { + { + unwrap!(write!($f, $s $(, $x)*).map_err($crate::fmt::FmtError)); + } + }; +} + +#[cfg(not(feature = "defmt"))] +#[collapse_debuginfo(yes)] +macro_rules! write_unwrap { + ($f:expr, $s:literal $(, $x:expr)* $(,)?) => { + { + unwrap!(write!($f, $s $(, $x)*)); + } + }; +} + +#[cfg(feature = "defmt")] +#[collapse_debuginfo(yes)] +macro_rules! display2format { + ($arg:expr) => { + ::defmt::Display2Format(&$arg) + }; +} + +#[cfg(not(feature = "defmt"))] +#[collapse_debuginfo(yes)] +macro_rules! display2format { + ($arg:expr) => { + $arg + }; +} + +#[cfg(feature = "defmt")] +#[collapse_debuginfo(yes)] +macro_rules! debug2format { + ($arg:expr) => { + ::defmt::Debug2Format(&$arg) + }; +} + +#[cfg(not(feature = "defmt"))] +#[collapse_debuginfo(yes)] +macro_rules! debug2format { + ($arg:expr) => { + $arg + }; +} + +/// A way to `{:x?}` format a byte slice which is compatible with `defmt` +pub struct Bytes<'a>(pub &'a [u8]); + +impl Debug for Bytes<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#02x?}", self.0) + } +} + +impl Display for Bytes<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#02x?}", self.0) + } +} + +impl LowerHex for Bytes<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#02x?}", self.0) + } +} + +#[cfg(feature = "defmt")] +impl defmt::Format for Bytes<'_> { + fn format(&self, fmt: defmt::Formatter) { + defmt::write!(fmt, "{:02x}", self.0) + } +} + +/// Support for the `unwrap!` macro for `Option` and `Result`. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct NoneError; + +/// Support for the `unwrap!` macro for `Option` and `Result`. +pub trait Try { + type Ok; + type Error; + #[allow(unused)] + fn into_result(self) -> Result; +} + +impl Try for Option { + type Ok = T; + type Error = NoneError; + + #[inline] + fn into_result(self) -> Result { + self.ok_or(NoneError) + } +} + +impl Try for Result { + type Ok = T; + type Error = E; + + #[inline] + fn into_result(self) -> Self { + self + } +} + +#[cfg(feature = "defmt")] +pub(crate) struct FmtError(pub(crate) core::fmt::Error); + +#[cfg(feature = "defmt")] +impl defmt::Format for FmtError { + fn format(&self, f: defmt::Formatter<'_>) { + defmt::write!(f, "{}", "FmtError") + } +} diff --git a/edge-raw/src/io.rs b/edge-raw/src/io.rs index 5790462..89d2bf8 100644 --- a/edge-raw/src/io.rs +++ b/edge-raw/src/io.rs @@ -1,4 +1,4 @@ -use core::fmt::{self, Debug}; +use core::fmt::Debug; use core::mem::MaybeUninit; use core::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; @@ -35,15 +35,29 @@ where } } -impl fmt::Display for Error +impl core::fmt::Display for Error where - E: fmt::Display, + E: core::fmt::Display, { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { - Self::Io(err) => write!(f, "IO error: {err}"), + Self::Io(err) => write!(f, "IO error: {}", err), Self::UnsupportedProtocol => write!(f, "Unsupported protocol"), - Self::RawError(err) => write!(f, "Raw error: {err}"), + Self::RawError(err) => write!(f, "Raw error: {}", err), + } + } +} + +#[cfg(feature = "defmt")] +impl defmt::Format for Error +where + E: defmt::Format, +{ + fn format(&self, f: defmt::Formatter<'_>) { + match self { + Self::Io(err) => defmt::write!(f, "IO error: {}", err), + Self::UnsupportedProtocol => defmt::write!(f, "Unsupported protocol"), + Self::RawError(err) => defmt::write!(f, "Raw error: {}", err), } } } diff --git a/edge-raw/src/ip.rs b/edge-raw/src/ip.rs index 1c2a718..34ddd89 100644 --- a/edge-raw/src/ip.rs +++ b/edge-raw/src/ip.rs @@ -1,5 +1,3 @@ -use log::trace; - use core::net::Ipv4Addr; use super::bytes::{BytesIn, BytesOut}; diff --git a/edge-raw/src/lib.rs b/edge-raw/src/lib.rs index 569ab40..396978b 100644 --- a/edge-raw/src/lib.rs +++ b/edge-raw/src/lib.rs @@ -2,12 +2,13 @@ #![allow(async_fn_in_trait)] #![warn(clippy::large_futures)] -use core::fmt; - use core::net::{Ipv4Addr, SocketAddrV4}; use self::udp::UdpPacketHeader; +// This mod MUST go first, so that the others see its macros. +pub(crate) mod fmt; + #[cfg(feature = "io")] pub mod io; @@ -36,8 +37,8 @@ impl From for Error { } } -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +impl core::fmt::Display for Error { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { let str = match self { Self::DataUnderflow => "Data underflow", Self::BufferOverflow => "Buffer overflow", @@ -49,6 +50,20 @@ impl fmt::Display for Error { } } +#[cfg(feature = "defmt")] +impl defmt::Format for Error { + fn format(&self, f: defmt::Formatter<'_>) { + let str = match self { + Self::DataUnderflow => "Data underflow", + Self::BufferOverflow => "Buffer overflow", + Self::InvalidFormat => "Invalid format", + Self::InvalidChecksum => "Invalid checksum", + }; + + defmt::write!(f, "{}", str) + } +} + #[cfg(feature = "std")] impl std::error::Error for Error {} @@ -101,7 +116,7 @@ pub fn checksum_accumulate(bytes: &[u8], checksum_word: usize) -> u32 { let arr = bytes .arr() .ok() - .unwrap_or_else(|| [bytes.byte().unwrap(), 0]); + .unwrap_or_else(|| [unwrap!(bytes.byte(), "Unreachable"), 0]); let word = if skip { 0 } else { u16::from_be_bytes(arr) }; diff --git a/edge-raw/src/udp.rs b/edge-raw/src/udp.rs index 1269faa..21e48de 100644 --- a/edge-raw/src/udp.rs +++ b/edge-raw/src/udp.rs @@ -1,5 +1,3 @@ -use log::trace; - use core::net::{Ipv4Addr, SocketAddrV4}; use super::bytes::{BytesIn, BytesOut}; @@ -195,18 +193,27 @@ impl UdpPacketHeader { let mut buf = [0; 12]; // Pseudo IP-header for UDP checksum calculation - let len = BytesOut::new(&mut buf) - .push(&u32::to_be_bytes(src.into())) - .unwrap() - .push(&u32::to_be_bytes(dst.into())) - .unwrap() - .byte(0) - .unwrap() - .byte(UdpPacketHeader::PROTO) - .unwrap() - .push(&u16::to_be_bytes(packet.len() as u16)) - .unwrap() - .len(); + let len = unwrap!( + unwrap!( + unwrap!( + unwrap!( + unwrap!( + BytesOut::new(&mut buf).push(&u32::to_be_bytes(src.into())), + "Unreachable" + ) + .push(&u32::to_be_bytes(dst.into())), + "Unreachable" + ) + .byte(0), + "Unreachable" + ) + .byte(UdpPacketHeader::PROTO), + "Unreachable" + ) + .push(&u16::to_be_bytes(packet.len() as u16)), + "Unreachable" + ) + .len(); let sum = checksum_accumulate(&buf[..len], usize::MAX) + checksum_accumulate(packet, Self::CHECKSUM_WORD); diff --git a/edge-ws/CHANGELOG.md b/edge-ws/CHANGELOG.md index 38ba5e0..62f03f7 100644 --- a/edge-ws/CHANGELOG.md +++ b/edge-ws/CHANGELOG.md @@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +* Optional `defmt` support via two new features (one has to specify one, or the other, or neither, but not both): + * `log` - uses the `log` crate for all logging + * `defmt` - uses the `defmt` crate for all logging, and implements `defmt::Format` for all library types that otherwise implement `Debug` and/or `Display` * Respect payload length of control messages ## [0.4.0] - 2025-01-02 diff --git a/edge-ws/Cargo.toml b/edge-ws/Cargo.toml index 544d579..ae8bfbe 100644 --- a/edge-ws/Cargo.toml +++ b/edge-ws/Cargo.toml @@ -19,7 +19,10 @@ categories = [ default = ["io"] std = ["io"] io = ["embedded-io-async"] +defmt = ["dep:defmt", "embedded-svc?/defmt"] [dependencies] +log = { version = "0.4", default-features = false, optional = true } +defmt = { version = "0.3", optional = true } embedded-io-async = { workspace = true, optional = true } embedded-svc = { workspace = true, optional = true, default-features = false } diff --git a/edge-ws/src/fmt.rs b/edge-ws/src/fmt.rs new file mode 100644 index 0000000..8bd7963 --- /dev/null +++ b/edge-ws/src/fmt.rs @@ -0,0 +1,340 @@ +//! A module that re-exports the `log` macros if `defmt` is not enabled, or `defmt` macros if `defmt` is enabled. +//! +//! The module also defines: +//! - Custom versions of the core assert macros (`assert!`, `assert_eq!`, etc.) that use the `defmt` macros if the `defmt` feature is enabled. +//! - Custom versions of the `panic!`, `todo!`, and `unreachable!` macros that use the `defmt` macros if the `defmt` feature is enabled. +//! - A custom `unwrap!` macro that uses the `defmt` macros if the `defmt` feature is enabled, otherwise it uses the standard library's `unwrap` method. +//! - A custom `Bytes` struct that formats byte slices as hex in a way compatible with `defmt`. +#![macro_use] +#![allow(unused)] + +use core::fmt::{Debug, Display, LowerHex}; + +#[collapse_debuginfo(yes)] +macro_rules! assert { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! assert_eq { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert_eq!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert_eq!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! assert_ne { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert_ne!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert_ne!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! debug_assert { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! debug_assert_eq { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert_eq!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert_eq!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! debug_assert_ne { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert_ne!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert_ne!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! todo { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::todo!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::todo!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! unreachable { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::unreachable!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::unreachable!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! panic { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::panic!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::panic!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! trace { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::trace!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::trace!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! debug { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::debug!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::debug!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! info { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::info!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::info!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! warn { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::warn!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::warn!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! error { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::error!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::error!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[cfg(feature = "defmt")] +#[collapse_debuginfo(yes)] +macro_rules! unwrap { + ($($x:tt)*) => { + ::defmt::unwrap!($($x)*) + }; +} + +#[cfg(not(feature = "defmt"))] +#[collapse_debuginfo(yes)] +macro_rules! unwrap { + ($arg:expr) => { + match $crate::fmt::Try::into_result($arg) { + ::core::result::Result::Ok(t) => t, + ::core::result::Result::Err(e) => { + ::core::panic!("unwrap of `{}` failed: {:?}", ::core::stringify!($arg), e); + } + } + }; + ($arg:expr, $($msg:expr),+ $(,)? ) => { + match $crate::fmt::Try::into_result($arg) { + ::core::result::Result::Ok(t) => t, + ::core::result::Result::Err(e) => { + ::core::panic!("unwrap of `{}` failed: {}: {:?}", ::core::stringify!($arg), ::core::format_args!($($msg,)*), e); + } + } + } +} + +#[cfg(feature = "defmt")] +#[collapse_debuginfo(yes)] +macro_rules! write_unwrap { + ($f:expr, $s:literal $(, $x:expr)* $(,)?) => { + { + unwrap!(write!($f, $s $(, $x)*).map_err($crate::fmt::FmtError)); + } + }; +} + +#[cfg(not(feature = "defmt"))] +#[collapse_debuginfo(yes)] +macro_rules! write_unwrap { + ($f:expr, $s:literal $(, $x:expr)* $(,)?) => { + { + unwrap!(write!($f, $s $(, $x)*)); + } + }; +} + +#[cfg(feature = "defmt")] +#[collapse_debuginfo(yes)] +macro_rules! display2format { + ($arg:expr) => { + ::defmt::Display2Format(&$arg) + }; +} + +#[cfg(not(feature = "defmt"))] +#[collapse_debuginfo(yes)] +macro_rules! display2format { + ($arg:expr) => { + $arg + }; +} + +#[cfg(feature = "defmt")] +#[collapse_debuginfo(yes)] +macro_rules! debug2format { + ($arg:expr) => { + ::defmt::Debug2Format(&$arg) + }; +} + +#[cfg(not(feature = "defmt"))] +#[collapse_debuginfo(yes)] +macro_rules! debug2format { + ($arg:expr) => { + $arg + }; +} + +/// A way to `{:x?}` format a byte slice which is compatible with `defmt` +pub struct Bytes<'a>(pub &'a [u8]); + +impl Debug for Bytes<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#02x?}", self.0) + } +} + +impl Display for Bytes<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#02x?}", self.0) + } +} + +impl LowerHex for Bytes<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#02x?}", self.0) + } +} + +#[cfg(feature = "defmt")] +impl defmt::Format for Bytes<'_> { + fn format(&self, fmt: defmt::Formatter) { + defmt::write!(fmt, "{:02x}", self.0) + } +} + +/// Support for the `unwrap!` macro for `Option` and `Result`. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct NoneError; + +/// Support for the `unwrap!` macro for `Option` and `Result`. +pub trait Try { + type Ok; + type Error; + #[allow(unused)] + fn into_result(self) -> Result; +} + +impl Try for Option { + type Ok = T; + type Error = NoneError; + + #[inline] + fn into_result(self) -> Result { + self.ok_or(NoneError) + } +} + +impl Try for Result { + type Ok = T; + type Error = E; + + #[inline] + fn into_result(self) -> Self { + self + } +} + +#[cfg(feature = "defmt")] +pub(crate) struct FmtError(pub(crate) core::fmt::Error); + +#[cfg(feature = "defmt")] +impl defmt::Format for FmtError { + fn format(&self, f: defmt::Formatter<'_>) { + defmt::write!(f, "{}", "FmtError") + } +} diff --git a/edge-ws/src/io.rs b/edge-ws/src/io.rs index 8b94e8f..52b348f 100644 --- a/edge-ws/src/io.rs +++ b/edge-ws/src/io.rs @@ -63,7 +63,7 @@ impl FrameHeader { W: Write, { let mut header_buf = [0; FrameHeader::MAX_LEN]; - let header_len = self.serialize(&mut header_buf).unwrap(); + let header_len = unwrap!(self.serialize(&mut header_buf)); write .write_all(&header_buf[..header_len]) @@ -219,7 +219,7 @@ mod embedded_svc_compat { ) -> Result<(), Self::Error> { super::send( &mut self.0, - frame_type.try_into().unwrap(), + unwrap!(frame_type.try_into(), "Invalid frame type"), (self.1)(), frame_data, ) diff --git a/edge-ws/src/lib.rs b/edge-ws/src/lib.rs index eb050bd..fb38ecb 100644 --- a/edge-ws/src/lib.rs +++ b/edge-ws/src/lib.rs @@ -2,8 +2,6 @@ #![allow(async_fn_in_trait)] #![warn(clippy::large_futures)] -use core::fmt; - pub type Fragmented = bool; pub type Final = bool; @@ -11,6 +9,9 @@ pub type Final = bool; #[cfg(feature = "embedded-svc")] pub use embedded_svc_compat::*; +// This mod MUST go first, so that the others see its macros. +pub(crate) mod fmt; + #[cfg(feature = "io")] pub mod io; @@ -42,8 +43,8 @@ impl FrameType { } } -impl fmt::Display for FrameType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +impl core::fmt::Display for FrameType { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Self::Text(fragmented) => { write!(f, "Text{}", if *fragmented { " (fragmented)" } else { "" }) @@ -63,6 +64,28 @@ impl fmt::Display for FrameType { } } +#[cfg(feature = "defmt")] +impl defmt::Format for FrameType { + fn format(&self, f: defmt::Formatter<'_>) { + match self { + Self::Text(fragmented) => { + defmt::write!(f, "Text{}", if *fragmented { " (fragmented)" } else { "" }) + } + Self::Binary(fragmented) => defmt::write!( + f, + "Binary{}", + if *fragmented { " (fragmented)" } else { "" } + ), + Self::Ping => defmt::write!(f, "Ping"), + Self::Pong => defmt::write!(f, "Pong"), + Self::Close => defmt::write!(f, "Close"), + Self::Continue(ffinal) => { + defmt::write!(f, "Continue{}", if *ffinal { " (final)" } else { "" }) + } + } + } +} + #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] pub enum Error { Incomplete(usize), @@ -84,11 +107,11 @@ impl Error<()> { } } -impl fmt::Display for Error +impl core::fmt::Display for Error where - E: fmt::Display, + E: core::fmt::Display, { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Self::Incomplete(size) => write!(f, "Incomplete: {} bytes missing", size), Self::Invalid => write!(f, "Invalid"), @@ -99,6 +122,22 @@ where } } +#[cfg(feature = "defmt")] +impl defmt::Format for Error +where + E: defmt::Format, +{ + fn format(&self, f: defmt::Formatter<'_>) { + match self { + Self::Incomplete(size) => defmt::write!(f, "Incomplete: {} bytes missing", size), + Self::Invalid => defmt::write!(f, "Invalid"), + Self::BufferOverflow => defmt::write!(f, "Buffer overflow"), + Self::InvalidLen => defmt::write!(f, "Invalid length"), + Self::Io(err) => defmt::write!(f, "IO error: {}", err), + } + } +} + #[cfg(feature = "std")] impl std::error::Error for Error where E: std::error::Error {} @@ -294,8 +333,8 @@ impl FrameHeader { } } -impl fmt::Display for FrameHeader { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +impl core::fmt::Display for FrameHeader { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!( f, "Frame {{ {}, payload len {}, mask {:?} }}", @@ -304,6 +343,19 @@ impl fmt::Display for FrameHeader { } } +#[cfg(feature = "defmt")] +impl defmt::Format for FrameHeader { + fn format(&self, f: defmt::Formatter<'_>) { + defmt::write!( + f, + "Frame {{ {}, payload len {}, mask {:?} }}", + self.frame_type, + self.payload_len, + self.mask_key + ) + } +} + #[cfg(feature = "embedded-svc")] mod embedded_svc_compat { use core::convert::TryFrom;