diff --git a/CHANGELOG.md b/CHANGELOG.md index 713ef07e..0f484ae0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added — core log lines reach defmt + +- **Every core log line reaches the probe log on an Embassy device (#352).** + The `log_*!` macros forward to `defmt` under the `defmt` feature, next to + `tracing` and `log`; `LogDisplay` / `LogDebug` wrap types without + `defmt::Format`, and the hand-written `defmt` mirrors that duplicated log + lines are gone. ([aimdb-core](aimdb-core/CHANGELOG.md), + [aimdb-zenoh-connector](aimdb-zenoh-connector/CHANGELOG.md), + [aimdb-knx-connector](aimdb-knx-connector/CHANGELOG.md), + [aimdb-mqtt-connector](aimdb-mqtt-connector/CHANGELOG.md)) + ### Added — Zenoh connector and ROS 2 topics - **AimDB records as ROS 2 topics, with no ROS install on the AimDB side.** diff --git a/Cargo.lock b/Cargo.lock index 47dcbac5..d1498291 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -223,6 +223,7 @@ dependencies = [ "aimdb-core", "aimdb-knx-pico", "aimdb-tokio-adapter", + "defmt 1.1.1", "embassy-futures 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "heapless 0.8.0", "tokio", @@ -449,6 +450,7 @@ dependencies = [ "aimdb-core", "aimdb-data-contracts", "aimdb-tokio-adapter", + "defmt 1.1.1", "serde", "tokio", "trybuild", diff --git a/Makefile b/Makefile index b3942b0f..bfd0b06f 100644 --- a/Makefile +++ b/Makefile @@ -415,6 +415,10 @@ clippy: cargo clippy --package aimdb-mqtt-connector --target thumbv7em-none-eabihf --no-default-features --features "embassy-runtime,embassy-tls,defmt" -- -D warnings @printf "$(YELLOW) → Clippy on KNX connector (neutral + defmt)$(NC)\n" cargo clippy --package aimdb-knx-connector --target thumbv7em-none-eabihf --no-default-features --features "connector,defmt" -- -D warnings + @printf "$(YELLOW) → Clippy on aimdb-core (no_std + defmt log destination)$(NC)\n" + cargo clippy --package aimdb-core --target thumbv7em-none-eabihf --no-default-features --features "alloc,connector-session,remote,defmt" -- -D warnings + @printf "$(YELLOW) → Clippy on Zenoh connector (embedded + defmt)$(NC)\n" + cargo clippy --package aimdb-zenoh-connector --target thumbv7em-none-eabihf --no-default-features --features "embedded,defmt" -- -D warnings @printf "$(YELLOW) → Clippy on WebSocket connector$(NC)\n" cargo clippy --package aimdb-websocket-connector --features "tokio-runtime,client" --all-targets -- -D warnings @printf "$(YELLOW) → Clippy on UDS connector$(NC)\n" @@ -558,6 +562,10 @@ test-embedded: cargo check --package aimdb-core --target thumbv7em-none-eabihf --target-dir $(EMBEDDED_CHECK_TARGET_DIR) --no-default-features --features alloc @printf "$(YELLOW) → Checking aimdb-core (no_std + log destination) on thumbv7em-none-eabihf target$(NC)\n" cargo check --package aimdb-core --target thumbv7em-none-eabihf --target-dir $(EMBEDDED_CHECK_TARGET_DIR) --no-default-features --features "alloc,log" + @printf "$(YELLOW) → Checking aimdb-core (no_std + defmt log destination) on thumbv7em-none-eabihf target$(NC)\n" + cargo check --package aimdb-core --target thumbv7em-none-eabihf --target-dir $(EMBEDDED_CHECK_TARGET_DIR) --no-default-features --features "alloc,defmt" + @printf "$(YELLOW) → Checking aimdb-core (full no_std AimX server + defmt log destination) on thumbv7em-none-eabihf target$(NC)\n" + cargo check --package aimdb-core --target thumbv7em-none-eabihf --target-dir $(EMBEDDED_CHECK_TARGET_DIR) --no-default-features --features "alloc,connector-session,remote,defmt" @printf "$(YELLOW) → Checking aimdb-embassy-adapter on thumbv7em-none-eabihf target$(NC)\n" cargo check --package aimdb-embassy-adapter --target thumbv7em-none-eabihf --target-dir $(EMBEDDED_CHECK_TARGET_DIR) --no-default-features --features "embassy-runtime" @printf "$(YELLOW) → Checking aimdb-embassy-adapter with network support on thumbv7em-none-eabihf target$(NC)\n" @@ -597,6 +605,8 @@ test-embedded: @printf "$(BLUE)✓ mountain-mqtt is the codec alone$(NC)\n" @printf "$(YELLOW) → Checking aimdb-zenoh-connector (embedded backend) on thumbv7em-none-eabihf target$(NC)\n" cargo check --package aimdb-zenoh-connector --target thumbv7em-none-eabihf --target-dir $(EMBEDDED_CHECK_TARGET_DIR) --no-default-features --features "embedded" + @printf "$(YELLOW) → Checking aimdb-zenoh-connector (embedded backend + defmt) on thumbv7em-none-eabihf target$(NC)\n" + cargo check --package aimdb-zenoh-connector --target thumbv7em-none-eabihf --target-dir $(EMBEDDED_CHECK_TARGET_DIR) --no-default-features --features "embedded,defmt" @printf "$(YELLOW) → Asserting no std-only Zenoh crates in the embedded Zenoh backend$(NC)\n" @err=$$(mktemp); \ out=$$(cargo tree -p aimdb-zenoh-connector --target thumbv7em-none-eabihf --no-default-features --features "embedded" -e features,no-dev 2>$$err) || { \ diff --git a/aimdb-core/CHANGELOG.md b/aimdb-core/CHANGELOG.md index e3995b21..2370b554 100644 --- a/aimdb-core/CHANGELOG.md +++ b/aimdb-core/CHANGELOG.md @@ -9,6 +9,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **Core log lines reach `defmt` (#352).** `log_trace!` … `log_error!` forward + to `defmt` under the calling crate's `defmt` feature, as a third destination + next to `tracing` and `log`. On an Embassy device every core log line now + reaches the probe log, including the session engine's (session reset, + refused and ended subscriptions, `serve`'s accept-failure summary), which + had no `defmt` output. +- **`LogDisplay` / `LogDebug`** wrap a type that does not implement + `defmt::Format` in a log call (`log_warn!("… {}", LogDisplay(&e))`): through + `Display` / `Debug` everywhere, `defmt::Display2Format` / `Debug2Format` + under `defmt`. `tracing`/`log` output is unchanged. +- **`defmt::Format` under `defmt`** for `TransportError`, `CodecError`, + `AuthError`, `FrameFault` (derived) and `DbError` (through its `Display` + text, since its `std`-only variants carry `io::Error`). Core's `defmt` + feature enables `defmt/alloc`, for `String`, `Vec` and `Arc`. + - **Topic patterns on inbound links.** `{name}` captures one level, `{name..}` the rest; the syntax is checked at `build()`, and the connector's `TopicGrammar` compiles each pattern into a `TopicFilter` when it @@ -120,6 +135,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **The `log_*!` macros carry a `defmt` arm (#352).** A crate forwarding its + `defmt` feature to the facade needs `dep:defmt` itself + (`defmt = ["dep:defmt", "aimdb-core/defmt"]`; `defmt`'s macros expand to + absolute paths), and every argument must implement `defmt::Format` (or be + wrapped in `LogDisplay` / `LogDebug`). A crate that never builds with + `defmt` names the feature through `check-cfg`, or `unexpected_cfgs` fires + at every call. The router's and the session client's hand-written `defmt` + mirrors are gone; the device shows the `log_*!` text. + - **`build()` checks transform and join input types** (#330). An input read as a type other than the one its record holds — `.transform::("temp", …)` on a `Celsius` record, or `JoinBuilder::input::` likewise — is reported diff --git a/aimdb-core/Cargo.toml b/aimdb-core/Cargo.toml index 6b2da2c6..c664f375 100644 --- a/aimdb-core/Cargo.toml +++ b/aimdb-core/Cargo.toml @@ -52,7 +52,7 @@ connector-session = ["alloc"] # Observability features (available on both std/no_std) tracing = ["dep:tracing"] # Works in both std and no_std environments -defmt = ["dep:defmt"] # Embedded logging via probe (no_std) +defmt = ["dep:defmt", "defmt/alloc"] # Embedded logging via probe (no_std) # A second facade destination, for hosts that cannot be handed a global # `tracing` subscriber (design 050). Not in `default`: 66 call sites on an MCU # should not each grow a level check because an FFI layer wanted one. diff --git a/aimdb-core/src/error.rs b/aimdb-core/src/error.rs index c6d23747..6d2bdf9b 100644 --- a/aimdb-core/src/error.rs +++ b/aimdb-core/src/error.rs @@ -223,6 +223,16 @@ pub enum DbError { }, } +// Through `Display` (`thiserror`), not a derive: the `std`-only variants carry +// `io::Error` / `serde_json::Error`, which do not implement `defmt::Format`, +// and docs.rs builds with every feature on, `std` and `defmt` together. +#[cfg(feature = "defmt")] +impl defmt::Format for DbError { + fn format(&self, f: defmt::Formatter) { + defmt::write!(f, "{}", defmt::Display2Format(self)) + } +} + // ===== DbError Implementation ===== impl DbError { /// Builds a [`RuntimeError`](DbError::RuntimeError). diff --git a/aimdb-core/src/lib.rs b/aimdb-core/src/lib.rs index acab7040..e36b5536 100644 --- a/aimdb-core/src/lib.rs +++ b/aimdb-core/src/lib.rs @@ -75,6 +75,22 @@ pub mod __private { pub use ::tracing; } +// Every type the `log_*!` macros format must implement `defmt::Format` when +// `defmt` is on (#352); this fails the build if one stops doing so. +#[cfg(all(feature = "defmt", feature = "connector-session"))] +const _: () = { + fn format() {} + let _ = format::; + let _ = format::; + let _ = format::; + let _ = format::; + let _ = format::; + let _ = format::; + let _ = format::; + let _ = format::>; + let _ = format::>; +}; + pub mod buffer; pub mod builder; #[cfg(feature = "remote")] @@ -107,6 +123,7 @@ pub mod typed_record; pub use context::RuntimeContext; pub use error::{ConfigError, DbError, DbErrorKind, DbResult}; pub use extensions::Extensions; +pub use log::{LogDebug, LogDisplay}; // Runtime capability surface: the runtime travels as `Arc`, // the one trait an adapter implements. diff --git a/aimdb-core/src/log.rs b/aimdb-core/src/log.rs index fe2136f6..2fa76aba 100644 --- a/aimdb-core/src/log.rs +++ b/aimdb-core/src/log.rs @@ -2,18 +2,20 @@ //! //! `log_trace!`/`log_debug!`/`log_info!`/`log_warn!`/`log_error!` forward to every enabled //! *destination* — the `tracing` event macro under the `tracing` feature, the -//! `log` crate under the `log` feature — and otherwise expand to a no-op that -//! still borrows the arguments, so call sites compile identically (no -//! unused-variable warnings) under every feature combination. This replaces the -//! per-call-site `#[cfg(feature = "tracing")]` gates. +//! `log` crate under the `log` feature, `defmt` under the `defmt` feature — and +//! otherwise expand to a no-op that still borrows the arguments, so call sites +//! compile identically (no unused-variable warnings) under every feature +//! combination. This replaces the per-call-site `#[cfg(feature = "tracing")]` +//! gates. //! //! Notes: //! - The no-op branch *borrows* (and therefore evaluates) the arguments — keep //! them cheap (getters, lengths, references), never allocate in hot paths. -//! - `defmt` is intentionally not folded in: most call sites use `{:?}` with -//! types that do not implement `defmt::Format` (e.g. `DbError`, `String`, -//! `Vec`). The few sites that mirror events to defmt (router.rs) -//! keep explicit `#[cfg(feature = "defmt")]` gates next to these macros. +//! - Under `defmt`, every argument must implement `defmt::Format` (the format +//! strings are shared: `defmt` reads `{}` and `{:?}` alike). Core's own +//! types do (#352); a crate logging its own types derives or implements +//! `Format` for them under its `defmt` feature, and wraps a foreign type in +//! [`LogDisplay`] / [`LogDebug`]. //! //! # Mirroring the feature //! @@ -23,19 +25,30 @@ //! nowhere: //! //! ```toml -//! tracing = ["aimdb-core/tracing"] # no `dep:tracing` -//! log = ["aimdb-core/log"] # no `dep:log` +//! tracing = ["aimdb-core/tracing"] # no `dep:tracing` +//! log = ["aimdb-core/log"] # no `dep:log` +//! defmt = ["dep:defmt", "aimdb-core/defmt"] # `dep:defmt` required //! ``` //! -//! Neither arm needs the dependency declared: both go through `$crate::__private`. -//! The *feature* is what cannot be dropped — a `#[cfg]` in a `#[macro_export]`ed -//! macro is resolved where it expands, so a crate without it would emit nothing. +//! `tracing` and `log` need no dependency declared: both arms go through +//! `$crate::__private`. `defmt` does: its macros expand to absolute `defmt::` +//! paths, so the calling crate needs `defmt` itself. The *feature* is what +//! cannot be dropped — a `#[cfg]` in a `#[macro_export]`ed macro is resolved +//! where it expands, so a crate without it would emit nothing. //! //! That is not a silent failure any more: an undeclared feature name makes //! `unexpected_cfgs` fire at every call site, which is a warning in an ordinary //! build and an error under `-D warnings`, as `make clippy` runs it. A dropped //! mirror therefore breaks CI rather than quietly muting a crate. //! `aimdb-sync/tests/log_facade.rs` additionally proves delivery end to end. +//! +//! A crate that never builds with `defmt` (a `std`-only connector) declares the +//! name instead, so the `defmt` arm's `#[cfg]` is known to the lint: +//! +//! ```toml +//! [lints.rust] +//! unexpected_cfgs = { level = "warn", check-cfg = ['cfg(feature, values("defmt"))'] } +//! ``` #[macro_export] macro_rules! log_trace { @@ -44,7 +57,9 @@ macro_rules! log_trace { $crate::__private::tracing::trace!($s $(, $x)*); #[cfg(feature = "log")] $crate::__private::log::trace!($s $(, $x)*); - #[cfg(not(any(feature = "tracing", feature = "log")))] + #[cfg(feature = "defmt")] + ::defmt::trace!($s $(, $x)*); + #[cfg(not(any(feature = "tracing", feature = "log", feature = "defmt")))] { let _ = ($( & $x ),*); } }}; } @@ -56,7 +71,9 @@ macro_rules! log_debug { $crate::__private::tracing::debug!($s $(, $x)*); #[cfg(feature = "log")] $crate::__private::log::debug!($s $(, $x)*); - #[cfg(not(any(feature = "tracing", feature = "log")))] + #[cfg(feature = "defmt")] + ::defmt::debug!($s $(, $x)*); + #[cfg(not(any(feature = "tracing", feature = "log", feature = "defmt")))] { let _ = ($( & $x ),*); } }}; } @@ -68,7 +85,9 @@ macro_rules! log_info { $crate::__private::tracing::info!($s $(, $x)*); #[cfg(feature = "log")] $crate::__private::log::info!($s $(, $x)*); - #[cfg(not(any(feature = "tracing", feature = "log")))] + #[cfg(feature = "defmt")] + ::defmt::info!($s $(, $x)*); + #[cfg(not(any(feature = "tracing", feature = "log", feature = "defmt")))] { let _ = ($( & $x ),*); } }}; } @@ -80,7 +99,9 @@ macro_rules! log_warn { $crate::__private::tracing::warn!($s $(, $x)*); #[cfg(feature = "log")] $crate::__private::log::warn!($s $(, $x)*); - #[cfg(not(any(feature = "tracing", feature = "log")))] + #[cfg(feature = "defmt")] + ::defmt::warn!($s $(, $x)*); + #[cfg(not(any(feature = "tracing", feature = "log", feature = "defmt")))] { let _ = ($( & $x ),*); } }}; } @@ -92,7 +113,47 @@ macro_rules! log_error { $crate::__private::tracing::error!($s $(, $x)*); #[cfg(feature = "log")] $crate::__private::log::error!($s $(, $x)*); - #[cfg(not(any(feature = "tracing", feature = "log")))] + #[cfg(feature = "defmt")] + ::defmt::error!($s $(, $x)*); + #[cfg(not(any(feature = "tracing", feature = "log", feature = "defmt")))] { let _ = ($( & $x ),*); } }}; } + +/// Logs `T` through its `Display` impl in every destination, `defmt` included +/// (via `defmt::Display2Format`). +/// +/// For a foreign type that does not implement `defmt::Format` in a `log_*!` +/// call: `log_warn!("send failed: {}", LogDisplay(&e))`. The `tracing` and +/// `log` output is unchanged; under `defmt` the text is formatted on the +/// device, not interned, so keep it to error paths. +pub struct LogDisplay<'a, T: ?Sized>(pub &'a T); + +impl core::fmt::Display for LogDisplay<'_, T> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.0.fmt(f) + } +} + +#[cfg(feature = "defmt")] +impl defmt::Format for LogDisplay<'_, T> { + fn format(&self, f: defmt::Formatter) { + defmt::write!(f, "{}", defmt::Display2Format(self)) + } +} + +/// Like [`LogDisplay`], through `Debug` (`{:?}`; `defmt::Debug2Format`). +pub struct LogDebug<'a, T: ?Sized>(pub &'a T); + +impl core::fmt::Debug for LogDebug<'_, T> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.0.fmt(f) + } +} + +#[cfg(feature = "defmt")] +impl defmt::Format for LogDebug<'_, T> { + fn format(&self, f: defmt::Formatter) { + defmt::write!(f, "{}", defmt::Debug2Format(self)) + } +} diff --git a/aimdb-core/src/outbound/routes.rs b/aimdb-core/src/outbound/routes.rs index 2d2644e3..d598cd35 100644 --- a/aimdb-core/src/outbound/routes.rs +++ b/aimdb-core/src/outbound/routes.rs @@ -126,6 +126,15 @@ pub(crate) enum SerializeFailure { InvalidLength { len: usize, capacity: usize }, } +// Through `Display`: the variants carry a `SerializeError` and lengths, and +// one rendering keeps the defmt line identical to the tracing/log one. +#[cfg(feature = "defmt")] +impl defmt::Format for SerializeFailure { + fn format(&self, f: defmt::Formatter) { + defmt::write!(f, "{}", defmt::Display2Format(self)) + } +} + impl core::fmt::Display for SerializeFailure { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { diff --git a/aimdb-core/src/router.rs b/aimdb-core/src/router.rs index 38b34c2e..36c1e92a 100644 --- a/aimdb-core/src/router.rs +++ b/aimdb-core/src/router.rs @@ -154,13 +154,6 @@ impl Router { } Err(_e) => { log_warn!("Failed to ingest message on '{}': {}", resource_id, _e); - - #[cfg(feature = "defmt")] - defmt::warn!( - "Failed to ingest message on '{}': {}", - resource_id, - _e.as_str() - ); } } } @@ -172,14 +165,8 @@ impl Router { "Route matched for '{}' but message was not produced (ingest errors)", resource_id ); - - #[cfg(feature = "defmt")] - defmt::debug!("Route matched for '{}' but not produced", resource_id); } else { log_debug!("No route found for resource: '{}'", resource_id); - - #[cfg(feature = "defmt")] - defmt::debug!("No route found for resource: '{}'", resource_id); } } diff --git a/aimdb-core/src/session/client.rs b/aimdb-core/src/session/client.rs index 34d2c8aa..aef25a4b 100644 --- a/aimdb-core/src/session/client.rs +++ b/aimdb-core/src/session/client.rs @@ -927,13 +927,6 @@ async fn inbound_pump(handle: ClientHandle, inbound: crate::InboundDispatch, id: id.as_ref(), update.skipped ); - - #[cfg(feature = "defmt")] - defmt::warn!( - "mirror gap on '{}': {} update(s) lost (latest-state, no resync)", - id.as_ref(), - update.skipped - ); } inbound.dispatch(id.as_ref(), &update.data); } diff --git a/aimdb-core/src/session/io.rs b/aimdb-core/src/session/io.rs index c7344c80..0fd5b4f1 100644 --- a/aimdb-core/src/session/io.rs +++ b/aimdb-core/src/session/io.rs @@ -196,6 +196,7 @@ pub trait Delay { /// none, so nothing tells payload bytes from the next header. Only the framer /// knows which case it is in, so it says, rather than the connection guessing. #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum FrameFault { /// Bad frame, good link: skip it and keep reading. Recoverable, diff --git a/aimdb-core/src/session/mod.rs b/aimdb-core/src/session/mod.rs index 79a8caac..eaa25c0a 100644 --- a/aimdb-core/src/session/mod.rs +++ b/aimdb-core/src/session/mod.rs @@ -224,6 +224,7 @@ impl Default for SessionLimits { /// Transport-layer failure (Layer 1). #[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[non_exhaustive] pub enum TransportError { /// The connection was closed or reset by the peer. @@ -239,6 +240,7 @@ pub enum TransportError { /// Envelope-codec failure — a frame could not be decoded/encoded. #[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[non_exhaustive] pub enum CodecError { /// The frame was not valid for this envelope format. @@ -262,6 +264,7 @@ pub enum RpcError { /// Authentication failure raised by [`Dispatch::authenticate`]. #[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[non_exhaustive] pub enum AuthError { /// Credentials were missing or rejected. diff --git a/aimdb-knx-connector/CHANGELOG.md b/aimdb-knx-connector/CHANGELOG.md index a8224990..ce1aac4c 100644 --- a/aimdb-knx-connector/CHANGELOG.md +++ b/aimdb-knx-connector/CHANGELOG.md @@ -24,6 +24,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 consumers keep building. Nothing here needs a `critical-section` implementation any more. +### Changed + +- **The `defmt` feature depends on `defmt` directly (#352)**, and enables + `defmt/ip_in_core`: core's `log_*!` macros now forward to `defmt`, and the + gateway address (`core::net::SocketAddr`) is formatted there. + ## [0.5.0] - 2026-09-18 ### Changed diff --git a/aimdb-knx-connector/Cargo.toml b/aimdb-knx-connector/Cargo.toml index 198c074f..35f7a500 100644 --- a/aimdb-knx-connector/Cargo.toml +++ b/aimdb-knx-connector/Cargo.toml @@ -47,9 +47,10 @@ critical-section-std-impl = [] # where it expands, so without them this crate would emit nothing. tracing = ["aimdb-core/tracing"] log = ["aimdb-core/log"] -defmt = ["aimdb-core/defmt", "knx-pico/defmt"] +defmt = ["dep:defmt", "defmt/ip_in_core", "aimdb-core/defmt", "knx-pico/defmt"] [dependencies] +defmt = { workspace = true, optional = true } aimdb-core = { version = "2.0.0", path = "../aimdb-core", default-features = false } # aimdb-dev fork: upstream 0.3.0 panics on an npdu_length=1 telegram. A patch diff --git a/aimdb-mqtt-connector/CHANGELOG.md b/aimdb-mqtt-connector/CHANGELOG.md index 90cf6bf1..18a6bd2e 100644 --- a/aimdb-mqtt-connector/CHANGELOG.md +++ b/aimdb-mqtt-connector/CHANGELOG.md @@ -29,6 +29,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **No duplicate `defmt` lines (#352).** Core's `log_*!` macros forward to + `defmt` now, so the embedded backend's hand-written mirrors of its + `log_warn!` lines (qos=2 caps, oversized publish) are gone; the device shows + the full `log_*!` text. Its `defmt`-only lines (connect, TLS, SNTP) stay. The + native backend logs `rumqttc` errors through `LogDisplay` / `LogDebug`, so an + all-features build (`std` with `defmt`) compiles. + - **An invalid inbound `qos` fails `build()` (#268).** A value other than 0, 1 or 2 on an inbound link is reported, naming the route, like on an outbound link. It used to be ignored. diff --git a/aimdb-mqtt-connector/src/embedded/mod.rs b/aimdb-mqtt-connector/src/embedded/mod.rs index ff4b871c..2787c9b4 100644 --- a/aimdb-mqtt-connector/src/embedded/mod.rs +++ b/aimdb-mqtt-connector/src/embedded/mod.rs @@ -185,11 +185,6 @@ fn inbound_topics( "MQTT: inbound route '{}' asks for qos=2; this backend subscribes at QoS 1 (at-least-once). The std backend honours qos=2 on the same URL.", route.topic ); - #[cfg(feature = "defmt")] - defmt::warn!( - "MQTT: inbound route '{}' asks qos=2; subscribing at QoS 1 (at-least-once)", - &*route.topic - ); } } @@ -227,11 +222,6 @@ fn prepare_outbound( "MQTT: route '{}' asks for qos=2; this backend publishes it at QoS 1 (at-least-once). The std backend honours qos=2 on the same URL.", route.default_topic ); - #[cfg(feature = "defmt")] - defmt::warn!( - "MQTT: route '{}' asks qos=2; publishing at QoS 1 (at-least-once)", - &*route.default_topic - ); } opts.push(opt); } diff --git a/aimdb-mqtt-connector/src/embedded/session_loop.rs b/aimdb-mqtt-connector/src/embedded/session_loop.rs index 382f8c14..21515c7d 100644 --- a/aimdb-mqtt-connector/src/embedded/session_loop.rs +++ b/aimdb-mqtt-connector/src/embedded/session_loop.rs @@ -456,17 +456,6 @@ async fn publish_staged( "write ring's" } ); - #[cfg(feature = "defmt")] - defmt::warn!( - "MQTT: skipping publish to {}: {} bytes exceed the {} limit", - msg.topic, - len, - if over_broker { - "broker's" - } else { - "write ring's" - } - ); true } else { ring.put_sized(&packet, len, CONTROL_RESERVE).await?; diff --git a/aimdb-mqtt-connector/src/native.rs b/aimdb-mqtt-connector/src/native.rs index 3e66bcb1..72eb1dc7 100644 --- a/aimdb-mqtt-connector/src/native.rs +++ b/aimdb-mqtt-connector/src/native.rs @@ -8,6 +8,7 @@ use aimdb_core::connector::ConnectorUrl; use aimdb_core::{log_debug, log_error, log_info}; use aimdb_core::{InboundDispatch, OutboundRoutes}; +use aimdb_core::{LogDebug, LogDisplay}; use rumqttc::{AsyncClient, Event, EventLoop, MqttOptions, Packet}; use std::future::Future; use std::pin::Pin; @@ -212,7 +213,11 @@ async fn run_event_loop(mut event_loop: EventLoop, inbound: InboundDispatch, _br } Ok(_) => {} Err(_e) => { - log_error!("MQTT event loop error for {}: {:?}", _broker_key, _e); + log_error!( + "MQTT event loop error for {}: {:?}", + _broker_key, + LogDebug(&_e) + ); tokio::time::sleep(Duration::from_secs(5)).await; } } @@ -255,7 +260,7 @@ async fn run_publish_loop( log_error!( "MQTT publish on route '{}' failed: {}", outbound.routes()[id].default_topic, - _e + LogDisplay(&_e) ); outbound.reject(id); } diff --git a/aimdb-sync/Cargo.toml b/aimdb-sync/Cargo.toml index fb36c38c..18302528 100644 --- a/aimdb-sync/Cargo.toml +++ b/aimdb-sync/Cargo.toml @@ -66,3 +66,8 @@ libc = { version = "0.2", default-features = false, optional = true } [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] + +# `aimdb-core`'s log macros carry a `#[cfg(feature = "defmt")]` arm that +# expands here; this crate never builds with defmt, so it only names it. +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(feature, values("defmt"))'] } diff --git a/aimdb-uds-connector/Cargo.toml b/aimdb-uds-connector/Cargo.toml index c2f85219..6dd49be7 100644 --- a/aimdb-uds-connector/Cargo.toml +++ b/aimdb-uds-connector/Cargo.toml @@ -36,3 +36,8 @@ tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] } # For the crate-level doc example (a mirrored record needs a JSON shape) serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" + +# `aimdb-core`'s log macros carry a `#[cfg(feature = "defmt")]` arm that +# expands here; this crate never builds with defmt, so it only names it. +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(feature, values("defmt"))'] } diff --git a/aimdb-zenoh-connector/CHANGELOG.md b/aimdb-zenoh-connector/CHANGELOG.md index f0abd026..bc2546f8 100644 --- a/aimdb-zenoh-connector/CHANGELOG.md +++ b/aimdb-zenoh-connector/CHANGELOG.md @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **`defmt` feature (#352)** for the embedded build: the connector's log lines + reach `defmt` through core's `log_*!` macros. + - **First release: Zenoh links, and AimDB records as ROS 2 topics** through rmw_zenoh, on the `zenoh` crate (`std` feature). The `zenoh-nostd` backend for microcontrollers follows once zenoh-nostd is on crates.io. diff --git a/aimdb-zenoh-connector/Cargo.toml b/aimdb-zenoh-connector/Cargo.toml index fbe8091d..2ca97474 100644 --- a/aimdb-zenoh-connector/Cargo.toml +++ b/aimdb-zenoh-connector/Cargo.toml @@ -35,8 +35,11 @@ log = ["aimdb-core/log"] # Reserved for the `zenoh-nostd` backend (`zenoh://` only), once zenoh-nostd is # on crates.io; today the `no_std` parts such as `ZenohGrammar`. embedded = ["aimdb-core/alloc"] +# The log lines reach defmt (core's `log_*!` macros), for the embedded build. +defmt = ["dep:defmt", "aimdb-core/defmt"] [dependencies] +defmt = { workspace = true, optional = true } aimdb-core = { version = "2.0.0", path = "../aimdb-core", default-features = false } zenoh = { version = "1.10.1", default-features = false, features = [ "transport_tcp", diff --git a/aimdb-zenoh-connector/src/native.rs b/aimdb-zenoh-connector/src/native.rs index 982604b1..e2203626 100644 --- a/aimdb-zenoh-connector/src/native.rs +++ b/aimdb-zenoh-connector/src/native.rs @@ -10,6 +10,7 @@ use std::sync::Arc; use std::time::Duration; use std::vec::Vec; +use aimdb_core::LogDisplay; use aimdb_core::{ log_debug, log_error, log_info, log_warn, AimDb, DbError, DbResult, InboundDispatch, OutboundRoutes, RouteInfo, TopicFilter, TopicGrammar, TopicPattern, MAX_CAPTURES, @@ -171,7 +172,7 @@ impl ZenohParts { { Ok(publisher) => publishers.push(Some(publisher)), Err(_e) => { - log_error!("Zenoh: cannot declare a publisher: {}", _e); + log_error!("Zenoh: cannot declare a publisher: {}", LogDisplay(&_e)); publishers.push(None); } } @@ -200,7 +201,7 @@ impl ZenohParts { log_error!( "Zenoh: put on route '{}' failed: {}", outbound.routes()[id].default_topic, - _e + LogDisplay(&_e) ); outbound.reject(id); } @@ -238,7 +239,7 @@ pub(crate) async fn open(config: zenoh::Config) -> Session { } } }; - log_info!("Zenoh: session {} open", session.zid()); + log_info!("Zenoh: session {} open", LogDisplay(&session.zid())); session } @@ -323,7 +324,7 @@ async fn subscribe( subscribers.push(subscriber); earlier.push(Arc::from(compiled)); } - Err(_e) => log_error!("Zenoh: cannot declare a subscriber: {}", _e), + Err(_e) => log_error!("Zenoh: cannot declare a subscriber: {}", LogDisplay(&_e)), } } subscribers diff --git a/aimdb-zenoh-connector/src/ros2.rs b/aimdb-zenoh-connector/src/ros2.rs index 9ad4c187..7409e4e6 100644 --- a/aimdb-zenoh-connector/src/ros2.rs +++ b/aimdb-zenoh-connector/src/ros2.rs @@ -9,6 +9,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use std::vec::Vec; use aimdb_core::connector::ConnectorBuilder; +use aimdb_core::LogDisplay; use aimdb_core::{ log_error, log_warn, AimDb, ExactGrammar, InboundDispatch, OutboundRoutes, RouteInfo, }; @@ -487,7 +488,10 @@ impl Ros2Parts { for token in tokens { match session.liveliness().declare_token(token).await { Ok(token) => live.push(token), - Err(_e) => log_error!("ROS 2: cannot declare a liveliness token: {}", _e), + Err(_e) => log_error!( + "ROS 2: cannot declare a liveliness token: {}", + LogDisplay(&_e) + ), } } @@ -506,7 +510,7 @@ impl Ros2Parts { .await; match declared { Ok(subscriber) => subscribers.push(subscriber), - Err(_e) => log_error!("ROS 2: cannot declare a subscriber: {}", _e), + Err(_e) => log_error!("ROS 2: cannot declare a subscriber: {}", LogDisplay(&_e)), } } @@ -523,7 +527,7 @@ impl Ros2Parts { log_error!( "ROS 2: cannot declare a publisher on '{}': {}", publisher.link.topic, - _e + LogDisplay(&_e) ); senders.push(None); } @@ -551,7 +555,7 @@ impl Ros2Parts { log_error!( "ROS 2: put on '{}' failed: {}", publishers[id].link.topic, - _e + LogDisplay(&_e) ); outbound.reject(id); }