Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.**
Expand Down
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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) || { \
Expand Down
24 changes: 24 additions & 0 deletions aimdb-core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>` and `Arc<str>`.

- **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
Expand Down Expand Up @@ -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::<Pascal, _>("temp", …)`
on a `Celsius` record, or `JoinBuilder::input::<I>` likewise — is reported
Expand Down
2 changes: 1 addition & 1 deletion aimdb-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
10 changes: 10 additions & 0 deletions aimdb-core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
17 changes: 17 additions & 0 deletions aimdb-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T: defmt::Format + ?Sized>() {}
let _ = format::<DbError>;
let _ = format::<outbound::SerializeFailure>;
let _ = format::<session::TransportError>;
let _ = format::<session::CodecError>;
let _ = format::<session::AuthError>;
let _ = format::<session::FrameFault>;
let _ = format::<alloc::string::String>;
let _ = format::<alloc::vec::Vec<alloc::string::String>>;
let _ = format::<alloc::sync::Arc<str>>;
};

pub mod buffer;
pub mod builder;
#[cfg(feature = "remote")]
Expand Down Expand Up @@ -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<dyn RuntimeOps>`,
// the one trait an adapter implements.
Expand Down
97 changes: 79 additions & 18 deletions aimdb-core/src/log.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>`). 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
//!
Expand All @@ -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 {
Expand All @@ -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 ),*); }
}};
}
Expand All @@ -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 ),*); }
}};
}
Expand All @@ -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 ),*); }
}};
}
Expand All @@ -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 ),*); }
}};
}
Expand All @@ -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<T: core::fmt::Display + ?Sized> 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<T: core::fmt::Display + ?Sized> 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<T: core::fmt::Debug + ?Sized> 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<T: core::fmt::Debug + ?Sized> defmt::Format for LogDebug<'_, T> {
fn format(&self, f: defmt::Formatter) {
defmt::write!(f, "{}", defmt::Debug2Format(self))
}
}
9 changes: 9 additions & 0 deletions aimdb-core/src/outbound/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
13 changes: 0 additions & 13 deletions aimdb-core/src/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
);
}
}
}
Expand All @@ -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);
}
}

Expand Down
7 changes: 0 additions & 7 deletions aimdb-core/src/session/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
1 change: 1 addition & 0 deletions aimdb-core/src/session/io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading
Loading