Skip to main content

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}