nros_core/logger.rs
1//! ROS 2 compatible logging utilities
2//!
3//! This module provides a `Logger` type that wraps the `log` crate with
4//! node-specific context, matching the rclrs logging patterns.
5//!
6//! # Example
7//!
8//! ```text
9//! let logger = node.logger();
10//! logger.info("Node started");
11//! logger.warn("Low battery");
12//! logger.error("Connection failed");
13//! ```
14//!
15//! # Conditional Logging
16//!
17//! The logger supports modifiers for conditional logging:
18//!
19//! ```
20//! use nros_core::{Logger, OnceFlag};
21//!
22//! let logger = Logger::new("my_node");
23//!
24//! // Log only once (requires a static flag)
25//! static LOGGED: OnceFlag = OnceFlag::new();
26//! logger.info_once(&LOGGED, "This logs only once");
27//!
28//! // Skip the first occurrence
29//! static SKIP: OnceFlag = OnceFlag::new();
30//! logger.warn_skip_first(&SKIP, "This skips the first call");
31//!
32//! // Rate-limited logging (requires tracking elapsed time)
33//! let mut last_log_ms: u64 = 0;
34//! let current_time_ms: u64 = 1000; // From your clock
35//! logger.info_throttle(
36//! &mut last_log_ms,
37//! current_time_ms,
38//! 1000,
39//! "Rate limited to 1Hz",
40//! );
41//! ```
42//!
43//! # Embedded Integration
44//!
45//! For embedded targets, this logger integrates with the `log` crate facade,
46//! allowing you to use any `log`-compatible backend:
47//!
48//! ## Desktop/std targets
49//! Use `env_logger` or similar:
50//! ```text
51//! env_logger::init();
52//! ```
53//!
54//! ## Embedded targets with defmt
55//! Use `defmt-log` to bridge `log` calls to `defmt` for minimal overhead:
56//! ```text
57//! // In Cargo.toml:
58//! // defmt = "0.3"
59//! // defmt-log = "0.1"
60//!
61//! // In your code:
62//! use defmt_log as _; // Links the logger
63//! ```
64//!
65//! This approach stores format strings on the host (not target), making it
66//! ideal for resource-constrained microcontrollers.
67//!
68//! ## RTIC/Embassy
69//! Both frameworks commonly use `defmt` directly. The `defmt-log` bridge
70//! allows nros logging to integrate seamlessly with your existing
71//! `defmt` setup.
72
73use core::sync::atomic::{AtomicBool, Ordering};
74
75/// A flag for tracking one-time logging
76///
77/// Use this with `Logger::*_once()` or `Logger::*_skip_first()` methods
78/// to control conditional logging. The flag should be declared as a static
79/// to ensure it persists across calls.
80///
81/// # Example
82///
83/// ```
84/// use nros_core::{Logger, OnceFlag};
85///
86/// static LOGGED: OnceFlag = OnceFlag::new();
87/// let logger = Logger::new("my_node");
88///
89/// // Only the first call will actually log
90/// for _ in 0..10 {
91/// logger.info_once(&LOGGED, "This only logs once");
92/// }
93/// ```
94pub struct OnceFlag {
95 triggered: AtomicBool,
96}
97
98impl OnceFlag {
99 /// Create a new once flag (not yet triggered)
100 pub const fn new() -> Self {
101 Self {
102 triggered: AtomicBool::new(false),
103 }
104 }
105
106 /// Check if this is the first time being called, and mark as triggered
107 ///
108 /// Returns `true` on the first call, `false` on subsequent calls.
109 ///
110 /// Uses load+store instead of compare_exchange for compatibility with
111 /// targets lacking CAS (e.g., riscv32imc without the A extension).
112 /// This is safe for nros's single-core embedded use cases.
113 pub fn check_first(&self) -> bool {
114 if !self.triggered.load(Ordering::Acquire) {
115 self.triggered.store(true, Ordering::Release);
116 true
117 } else {
118 false
119 }
120 }
121
122 /// Check if the flag has been triggered
123 pub fn is_triggered(&self) -> bool {
124 self.triggered.load(Ordering::SeqCst)
125 }
126
127 /// Reset the flag to untriggered state
128 pub fn reset(&self) {
129 self.triggered.store(false, Ordering::SeqCst);
130 }
131}
132
133impl Default for OnceFlag {
134 fn default() -> Self {
135 Self::new()
136 }
137}
138
139// OnceFlag is Send + Sync because AtomicBool is Send + Sync
140unsafe impl Send for OnceFlag {}
141unsafe impl Sync for OnceFlag {}
142
143impl core::fmt::Debug for OnceFlag {
144 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
145 f.debug_struct("OnceFlag")
146 .field("triggered", &self.is_triggered())
147 .finish()
148 }
149}
150
151/// A logger associated with a ROS node
152///
153/// Provides methods for logging at different severity levels.
154/// The logger includes the node name in log output for context.
155#[derive(Debug, Clone)]
156pub struct Logger<'a> {
157 /// The node name for log context
158 node_name: &'a str,
159}
160
161impl<'a> Logger<'a> {
162 /// Create a new logger for the given node name
163 pub const fn new(node_name: &'a str) -> Self {
164 Self { node_name }
165 }
166
167 /// Get the node name associated with this logger
168 pub const fn node_name(&self) -> &str {
169 self.node_name
170 }
171
172 /// Log a message at the DEBUG level
173 #[inline]
174 pub fn debug(&self, message: &str) {
175 log::debug!(target: self.node_name, "{}", message);
176 }
177
178 /// Log a message at the INFO level
179 #[inline]
180 pub fn info(&self, message: &str) {
181 log::info!(target: self.node_name, "{}", message);
182 }
183
184 /// Log a message at the WARN level
185 #[inline]
186 pub fn warn(&self, message: &str) {
187 log::warn!(target: self.node_name, "{}", message);
188 }
189
190 /// Log a message at the ERROR level
191 #[inline]
192 pub fn error(&self, message: &str) {
193 log::error!(target: self.node_name, "{}", message);
194 }
195
196 /// Log a message at the TRACE level
197 #[inline]
198 pub fn trace(&self, message: &str) {
199 log::trace!(target: self.node_name, "{}", message);
200 }
201
202 // --- Once modifiers (log only first occurrence) ---
203
204 /// Log a DEBUG message only once (first call only)
205 ///
206 /// The `flag` should be a static `OnceFlag` to track whether the message
207 /// has already been logged.
208 #[inline]
209 pub fn debug_once(&self, flag: &OnceFlag, message: &str) {
210 if flag.check_first() {
211 self.debug(message);
212 }
213 }
214
215 /// Log an INFO message only once (first call only)
216 #[inline]
217 pub fn info_once(&self, flag: &OnceFlag, message: &str) {
218 if flag.check_first() {
219 self.info(message);
220 }
221 }
222
223 /// Log a WARN message only once (first call only)
224 #[inline]
225 pub fn warn_once(&self, flag: &OnceFlag, message: &str) {
226 if flag.check_first() {
227 self.warn(message);
228 }
229 }
230
231 /// Log an ERROR message only once (first call only)
232 #[inline]
233 pub fn error_once(&self, flag: &OnceFlag, message: &str) {
234 if flag.check_first() {
235 self.error(message);
236 }
237 }
238
239 /// Log a TRACE message only once (first call only)
240 #[inline]
241 pub fn trace_once(&self, flag: &OnceFlag, message: &str) {
242 if flag.check_first() {
243 self.trace(message);
244 }
245 }
246
247 // --- Skip first modifiers (skip first occurrence, log all subsequent) ---
248
249 /// Log a DEBUG message, skipping the first occurrence
250 ///
251 /// The `flag` should be a static `OnceFlag` to track whether the first
252 /// call has been skipped.
253 #[inline]
254 pub fn debug_skip_first(&self, flag: &OnceFlag, message: &str) {
255 if !flag.check_first() {
256 self.debug(message);
257 }
258 }
259
260 /// Log an INFO message, skipping the first occurrence
261 #[inline]
262 pub fn info_skip_first(&self, flag: &OnceFlag, message: &str) {
263 if !flag.check_first() {
264 self.info(message);
265 }
266 }
267
268 /// Log a WARN message, skipping the first occurrence
269 #[inline]
270 pub fn warn_skip_first(&self, flag: &OnceFlag, message: &str) {
271 if !flag.check_first() {
272 self.warn(message);
273 }
274 }
275
276 /// Log an ERROR message, skipping the first occurrence
277 #[inline]
278 pub fn error_skip_first(&self, flag: &OnceFlag, message: &str) {
279 if !flag.check_first() {
280 self.error(message);
281 }
282 }
283
284 /// Log a TRACE message, skipping the first occurrence
285 #[inline]
286 pub fn trace_skip_first(&self, flag: &OnceFlag, message: &str) {
287 if !flag.check_first() {
288 self.trace(message);
289 }
290 }
291
292 // --- Throttle modifiers (rate-limit logging) ---
293
294 /// Log a DEBUG message with rate limiting
295 ///
296 /// # Arguments
297 /// * `last_log_time` - Mutable reference to track last log time (in milliseconds)
298 /// * `current_time_ms` - Current time in milliseconds (from your clock)
299 /// * `interval_ms` - Minimum interval between logs in milliseconds
300 /// * `message` - The message to log
301 ///
302 /// # Example
303 /// ```text
304 /// static mut LAST_LOG: u64 = 0;
305 /// let now_ms = clock.now_ms();
306 /// // Safety: only accessed from single thread
307 /// unsafe { logger.debug_throttle(&mut LAST_LOG, now_ms, 1000, "Rate limited"); }
308 /// ```
309 #[inline]
310 pub fn debug_throttle(
311 &self,
312 last_log_time: &mut u64,
313 current_time_ms: u64,
314 interval_ms: u64,
315 message: &str,
316 ) {
317 if Self::should_log_throttled(last_log_time, current_time_ms, interval_ms) {
318 self.debug(message);
319 }
320 }
321
322 /// Log an INFO message with rate limiting
323 #[inline]
324 pub fn info_throttle(
325 &self,
326 last_log_time: &mut u64,
327 current_time_ms: u64,
328 interval_ms: u64,
329 message: &str,
330 ) {
331 if Self::should_log_throttled(last_log_time, current_time_ms, interval_ms) {
332 self.info(message);
333 }
334 }
335
336 /// Log a WARN message with rate limiting
337 #[inline]
338 pub fn warn_throttle(
339 &self,
340 last_log_time: &mut u64,
341 current_time_ms: u64,
342 interval_ms: u64,
343 message: &str,
344 ) {
345 if Self::should_log_throttled(last_log_time, current_time_ms, interval_ms) {
346 self.warn(message);
347 }
348 }
349
350 /// Log an ERROR message with rate limiting
351 #[inline]
352 pub fn error_throttle(
353 &self,
354 last_log_time: &mut u64,
355 current_time_ms: u64,
356 interval_ms: u64,
357 message: &str,
358 ) {
359 if Self::should_log_throttled(last_log_time, current_time_ms, interval_ms) {
360 self.error(message);
361 }
362 }
363
364 /// Log a TRACE message with rate limiting
365 #[inline]
366 pub fn trace_throttle(
367 &self,
368 last_log_time: &mut u64,
369 current_time_ms: u64,
370 interval_ms: u64,
371 message: &str,
372 ) {
373 if Self::should_log_throttled(last_log_time, current_time_ms, interval_ms) {
374 self.trace(message);
375 }
376 }
377
378 /// Helper to check if enough time has passed for throttled logging
379 #[inline]
380 fn should_log_throttled(
381 last_log_time: &mut u64,
382 current_time_ms: u64,
383 interval_ms: u64,
384 ) -> bool {
385 if current_time_ms >= *last_log_time + interval_ms {
386 *last_log_time = current_time_ms;
387 true
388 } else {
389 false
390 }
391 }
392}
393
394#[cfg(test)]
395mod tests {
396 use super::*;
397
398 #[test]
399 fn test_logger_creation() {
400 let logger = Logger::new("test_node");
401 assert_eq!(logger.node_name(), "test_node");
402 }
403
404 #[test]
405 fn test_once_flag_first_call() {
406 let flag = OnceFlag::new();
407 assert!(!flag.is_triggered());
408
409 // First call should return true
410 assert!(flag.check_first());
411 assert!(flag.is_triggered());
412
413 // Subsequent calls should return false
414 assert!(!flag.check_first());
415 assert!(!flag.check_first());
416 }
417
418 #[test]
419 fn test_once_flag_reset() {
420 let flag = OnceFlag::new();
421
422 assert!(flag.check_first());
423 assert!(flag.is_triggered());
424
425 flag.reset();
426 assert!(!flag.is_triggered());
427
428 // After reset, check_first should return true again
429 assert!(flag.check_first());
430 }
431
432 #[test]
433 fn test_once_flag_default() {
434 let flag = OnceFlag::default();
435 assert!(!flag.is_triggered());
436 }
437
438 #[test]
439 fn test_once_flag_debug() {
440 // Just verify Debug is implemented and doesn't panic
441 let flag = OnceFlag::new();
442 let _ = core::format_args!("{:?}", flag);
443
444 flag.check_first();
445 let _ = core::format_args!("{:?}", flag);
446 }
447
448 #[test]
449 fn test_throttle_logic() {
450 let mut last_log_time: u64 = 0;
451
452 // First call after interval has passed should log
453 // (initializing to 0 means "log when time >= interval")
454 assert!(Logger::should_log_throttled(&mut last_log_time, 1000, 1000));
455 assert_eq!(last_log_time, 1000);
456
457 // Call at time 1500 should not log (within interval)
458 assert!(!Logger::should_log_throttled(
459 &mut last_log_time,
460 1500,
461 1000
462 ));
463 assert_eq!(last_log_time, 1000);
464
465 // Call at time 1999 should not log (within interval)
466 assert!(!Logger::should_log_throttled(
467 &mut last_log_time,
468 1999,
469 1000
470 ));
471 assert_eq!(last_log_time, 1000);
472
473 // Call at time 2000 should log (interval passed)
474 assert!(Logger::should_log_throttled(&mut last_log_time, 2000, 1000));
475 assert_eq!(last_log_time, 2000);
476
477 // Call at time 2500 should not log (within new interval)
478 assert!(!Logger::should_log_throttled(
479 &mut last_log_time,
480 2500,
481 1000
482 ));
483 assert_eq!(last_log_time, 2000);
484
485 // Call at time 3000 should log (new interval passed)
486 assert!(Logger::should_log_throttled(&mut last_log_time, 3000, 1000));
487 assert_eq!(last_log_time, 3000);
488 }
489
490 #[test]
491 fn test_throttle_zero_interval() {
492 let mut last_log_time: u64 = 0;
493
494 // With zero interval, every call should log
495 assert!(Logger::should_log_throttled(&mut last_log_time, 0, 0));
496 assert!(Logger::should_log_throttled(&mut last_log_time, 0, 0));
497 assert!(Logger::should_log_throttled(&mut last_log_time, 1, 0));
498 }
499}