dlt_rs/
lib.rs

1/*
2 * Copyright (c) 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS)
3 *
4 * See the NOTICE file(s) distributed with this work for additional
5 * information regarding copyright ownership.
6 *
7 * This program and the accompanying materials are made available under the
8 * terms of the Apache License Version 2.0 which is available at
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * SPDX-License-Identifier: Apache-2.0
12 */
13
14//! Low-level Rust bindings for COVESA DLT (Diagnostic Log and Trace)
15//!
16//! Safe Rust API for the DLT C library with RAII semantics, enabling applications to send
17//! diagnostic logs and traces to the DLT daemon for centralized logging and analysis.
18//!
19//! # Quick Start
20//!
21//! ```no_run
22//! use dlt_rs::{DltApplication, DltId, DltLogLevel};
23//!
24//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
25//! // Register application (one per process)
26//! let app = DltApplication::register(&DltId::new(b"MBTI")?, "Measurement & Bus Trace Interface")?;
27//! let ctx = app.create_context(&DltId::new(b"MEAS")?, "Measurement Context")?;
28//!
29//! // Simple logging
30//! ctx.log(DltLogLevel::Info, "Hello DLT!")?;
31//!
32//! // Structured logging with typed fields
33//! let mut writer = ctx.log_write_start(DltLogLevel::Info)?;
34//! writer.write_string("Temperature:")?
35//!     .write_float32(87.5)?
36//!     .write_string("°C")?;
37//! writer.finish()?;
38//! # Ok(())
39//! # }
40//! ```
41//!
42//! # Core Types
43//!
44//! - [`DltApplication`] - Application registration
45//! - [`DltContextHandle`] - Context for logging with specific ID
46//! - [`DltLogWriter`] - Builder for structured multi-field messages
47//! - [`DltLogLevel`] - Log severity (Fatal, Error, Warn, Info, Debug, Verbose)
48//! - [`DltId`] - Type-safe 1-4 byte ASCII identifiers
49//!
50//! # Features
51//!
52//! - **RAII cleanup** - Automatic resource management
53//! - **Structured logging** - Structured logging messages via [`DltLogWriter`]
54//! - **Dynamic control** - Runtime log level changes via
55//!   [`DltContextHandle::register_log_level_changed_listener()`]
56//! - **Thread-safe** - All types are `Send + Sync`
57//!
58//! # Log Level Control
59//!
60//! DLT log levels can be changed at runtime by the DLT daemon or other tools.
61//! Applications can listen for log level changes.
62//! See [`DltLogLevel`] for all available levels and [`LogLevelChangedEvent`] to listen for changes.
63//!
64//! # See Also
65//!
66//! - [COVESA DLT](https://github.com/COVESA/dlt-daemon)
67use std::{
68    collections::HashMap,
69    ffi::CString,
70    ptr,
71    sync::{Arc, OnceLock, RwLock, atomic::AtomicBool},
72};
73
74use thiserror::Error;
75use tokio::sync::broadcast;
76
77#[rustfmt::skip]
78#[allow(clippy::all,
79    dead_code,
80    warnings,
81    clippy::arithmetic_side_effects,
82    clippy::indexing_slicing,
83)]
84pub use dlt_sys::{DLT_ID_SIZE, DltContext, DltContextData};
85
86/// DLT log level
87///
88/// Severity level of a log message, ordered from most severe ([`DltLogLevel::Fatal`])
89/// to least severe ([`DltLogLevel::Verbose`]).
90///
91/// Use with [`DltContextHandle::log()`] or [`DltContextHandle::log_write_start()`].
92/// The DLT daemon filters messages based on the configured threshold
93#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
94pub enum DltLogLevel {
95    /// Default log level (determined by DLT daemon configuration)
96    Default,
97    /// Logging is disabled
98    Off,
99    /// Fatal system error - system is unusable
100    Fatal,
101    /// Error conditions - operation failed
102    Error,
103    /// Warning conditions - something unexpected but recoverable
104    Warn,
105    /// Informational messages - normal operation (default level)
106    Info,
107    /// Debug-level messages - detailed diagnostic information
108    Debug,
109    /// Verbose/trace-level messages - very detailed execution traces
110    Verbose,
111}
112
113impl From<i32> for DltLogLevel {
114    fn from(value: i32) -> Self {
115        match value {
116            dlt_sys::DltLogLevelType_DLT_LOG_OFF => DltLogLevel::Off,
117            dlt_sys::DltLogLevelType_DLT_LOG_FATAL => DltLogLevel::Fatal,
118            dlt_sys::DltLogLevelType_DLT_LOG_ERROR => DltLogLevel::Error,
119            dlt_sys::DltLogLevelType_DLT_LOG_WARN => DltLogLevel::Warn,
120            dlt_sys::DltLogLevelType_DLT_LOG_INFO => DltLogLevel::Info,
121            dlt_sys::DltLogLevelType_DLT_LOG_DEBUG => DltLogLevel::Debug,
122            dlt_sys::DltLogLevelType_DLT_LOG_VERBOSE => DltLogLevel::Verbose,
123            _ => DltLogLevel::Default,
124        }
125    }
126}
127
128impl From<DltLogLevel> for i32 {
129    fn from(value: DltLogLevel) -> Self {
130        match value {
131            DltLogLevel::Default => dlt_sys::DltLogLevelType_DLT_LOG_DEFAULT,
132            DltLogLevel::Off => dlt_sys::DltLogLevelType_DLT_LOG_OFF,
133            DltLogLevel::Fatal => dlt_sys::DltLogLevelType_DLT_LOG_FATAL,
134            DltLogLevel::Error => dlt_sys::DltLogLevelType_DLT_LOG_ERROR,
135            DltLogLevel::Warn => dlt_sys::DltLogLevelType_DLT_LOG_WARN,
136            DltLogLevel::Info => dlt_sys::DltLogLevelType_DLT_LOG_INFO,
137            DltLogLevel::Debug => dlt_sys::DltLogLevelType_DLT_LOG_DEBUG,
138            DltLogLevel::Verbose => dlt_sys::DltLogLevelType_DLT_LOG_VERBOSE,
139        }
140    }
141}
142
143/// Internal error types for Rust-side operations (not from libdlt)
144#[derive(Error, Debug, Clone, PartialEq, Eq)]
145pub enum DltError {
146    #[error("Data cannot be converted to a DLT compatible string: {0}")]
147    InvalidString(String),
148    #[error("Failed to register DLT context")]
149    ContextRegistrationFailed(String),
150    #[error("Failed to register DLT application")]
151    ApplicationRegistrationFailed(String),
152    #[error("Failed to register a log event change listener")]
153    LogLevelListenerRegistrationFailed(String),
154    #[error("A pointer or memory is invalid")]
155    InvalidMemory,
156    #[error("Failed to acquire a lock")]
157    BadLock,
158    #[error("Input value is invalid")]
159    InvalidInput,
160}
161
162/// DLT return value error types (from libdlt C library)
163#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)]
164pub enum DltSysError {
165    #[cfg(feature = "trace_load_ctrl")]
166    #[error("DLT load exceeded")]
167    /// Only available with the `trace_load_ctrl` feature enabled.
168    LoadExceeded,
169
170    #[error("DLT file size error")]
171    FileSizeError,
172
173    #[error("DLT logging disabled")]
174    LoggingDisabled,
175
176    #[error("DLT user buffer full")]
177    UserBufferFull,
178
179    #[error("DLT wrong parameter")]
180    WrongParameter,
181
182    #[error("DLT buffer full")]
183    BufferFull,
184
185    #[error("DLT pipe full")]
186    PipeFull,
187
188    #[error("DLT pipe error")]
189    PipeError,
190
191    #[error("DLT general error")]
192    Error,
193
194    #[error("DLT unknown error")]
195    Unknown,
196}
197
198impl DltSysError {
199    fn from_return_code(code: i32) -> Result<(), Self> {
200        #[allow(unreachable_patterns)]
201        match code {
202            dlt_sys::DltReturnValue_DLT_RETURN_TRUE | dlt_sys::DltReturnValue_DLT_RETURN_OK => {
203                Ok(())
204            }
205            dlt_sys::DltReturnValue_DLT_RETURN_ERROR => Err(DltSysError::Error),
206            dlt_sys::DltReturnValue_DLT_RETURN_PIPE_ERROR => Err(DltSysError::PipeError),
207            dlt_sys::DltReturnValue_DLT_RETURN_PIPE_FULL => Err(DltSysError::PipeFull),
208            dlt_sys::DltReturnValue_DLT_RETURN_BUFFER_FULL => Err(DltSysError::BufferFull),
209            dlt_sys::DltReturnValue_DLT_RETURN_WRONG_PARAMETER => Err(DltSysError::WrongParameter),
210            dlt_sys::DltReturnValue_DLT_RETURN_USER_BUFFER_FULL => Err(DltSysError::UserBufferFull),
211            dlt_sys::DltReturnValue_DLT_RETURN_LOGGING_DISABLED => {
212                Err(DltSysError::LoggingDisabled)
213            }
214            dlt_sys::DltReturnValue_DLT_RETURN_FILESZERR => Err(DltSysError::FileSizeError),
215            #[cfg(feature = "trace_load_ctrl")]
216            dlt_sys::DltReturnValue_DLT_RETURN_LOAD_EXCEEDED => Err(DltSysError::LoadExceeded),
217            _ => Err(DltSysError::Unknown),
218        }
219    }
220}
221
222/// Size of DLT ID fields (Application ID, Context ID) - re-exported from bindings as usize
223pub const DLT_ID_SIZE_USIZE: usize = DLT_ID_SIZE as usize;
224
225/// A DLT identifier (Application ID or Context ID)
226///
227/// DLT IDs are 1-4 ASCII bytes. Create with `DltId::new(b"APP")?`.
228/// Shorter IDs are internally padded with nulls
229#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
230pub struct DltId {
231    bytes: [u8; DLT_ID_SIZE_USIZE],
232    len: usize,
233}
234
235impl DltId {
236    /// Create a new DLT ID from a byte slice of 1 to 4 bytes
237    ///
238    /// The ID will be validated as ASCII.
239    /// IDs shorter than 4 bytes are right-padded with null bytes internally.
240    ///
241    /// # Errors
242    /// Returns [`DltError::InvalidInput`] if the byte slice is empty, longer than 4 bytes,
243    /// or contains non-ASCII characters.
244    ///
245    /// # Examples
246    /// ```no_run
247    /// # use dlt_rs::{DltId, DltError};
248    /// # fn main() -> Result<(), DltError> {
249    /// let id = DltId::new(b"APP")?;
250    /// assert_eq!(id.as_str()?, "APP");
251    ///
252    /// // Too long
253    /// assert!(DltId::new(b"TOOLONG").is_err());
254    ///
255    /// // Empty
256    /// assert!(DltId::new(b"").is_err());
257    /// # Ok(())
258    /// # }
259    /// ```
260    pub fn new(bytes: &[u8]) -> Result<Self, DltError> {
261        // Validate that N is between 1 and 4
262        let len = bytes.len();
263        if bytes.is_empty() || len > DLT_ID_SIZE_USIZE {
264            return Err(DltError::InvalidInput);
265        }
266
267        // Validate ASCII
268        if !bytes.is_ascii() {
269            return Err(DltError::InvalidInput);
270        }
271
272        let mut padded = [0u8; DLT_ID_SIZE_USIZE];
273        // Indexing is safe here: function ensures N <= DLT_ID_SIZE by validation
274        #[allow(clippy::indexing_slicing)]
275        padded[..len].copy_from_slice(&bytes[..len]);
276
277        Ok(Self { bytes: padded, len })
278    }
279
280    /// Construct a `DltId` from a string slice, clamping to 4 bytes
281    /// # Errors
282    /// Returns an error if the string is empty
283    /// # Example
284    /// ```no_run
285    /// # use dlt_rs::{DltId, DltError};
286    /// # fn main() -> Result<(), DltError> {
287    /// let id = DltId::from_str_clamped("APPTOOLONG")?;
288    /// assert_eq!(id.as_str()?, "APPT");
289    /// # Ok(())
290    /// # }
291    /// ```
292    pub fn from_str_clamped(id: &str) -> Result<Self, DltError> {
293        if id.is_empty() {
294            return Err(DltError::InvalidInput);
295        }
296        let bytes = id.as_bytes();
297        let len = bytes.len().clamp(1, DLT_ID_SIZE_USIZE);
298
299        DltId::new(bytes.get(0..len).ok_or(DltError::InvalidInput)?)
300    }
301
302    /// Get the ID as a string slice
303    ///
304    /// # Errors
305    /// Returns an error if the bytes are not valid UFT-8.
306    /// This should never happen due to construction constraints.
307    pub fn as_str(&self) -> Result<&str, DltError> {
308        let slice = self
309            .bytes
310            .get(..self.len)
311            .ok_or_else(|| DltError::InvalidString("Invalid length".to_string()))?;
312        let s = std::str::from_utf8(slice).map_err(|e| DltError::InvalidString(e.to_string()))?;
313        Ok(s)
314    }
315}
316
317/// Convert a string slice to a DLT ID, will yield an error if the string is too long or empty
318impl TryFrom<&str> for DltId {
319    type Error = DltError;
320
321    fn try_from(value: &str) -> Result<Self, Self::Error> {
322        let bytes = value.as_bytes();
323        if bytes.is_empty() || bytes.len() > DLT_ID_SIZE_USIZE {
324            return Err(DltError::InvalidInput);
325        }
326        let mut padded = [0u8; DLT_ID_SIZE_USIZE];
327        padded
328            .get_mut(..bytes.len())
329            .ok_or(DltError::InvalidInput)?
330            .copy_from_slice(bytes);
331        Ok(DltId {
332            bytes: padded,
333            len: bytes.len(),
334        })
335    }
336}
337
338/// DLT trace status
339///
340/// Controls whether network trace messages (like packet captures) are enabled.
341/// This is separate from log levels. Most applications only use log levels and can
342/// ignore trace status.
343///
344/// Trace status is included in [`LogLevelChangedEvent`] notifications
345#[derive(Debug, Clone, Copy, PartialEq, Eq)]
346pub enum DltTraceStatus {
347    /// Use default trace status from DLT daemon configuration
348    Default,
349    /// Trace messages are disabled
350    Off,
351    /// Trace messages are enabled
352    On,
353}
354
355impl From<i32> for DltTraceStatus {
356    fn from(value: i32) -> Self {
357        match value {
358            dlt_sys::DltTraceStatusType_DLT_TRACE_STATUS_OFF => DltTraceStatus::Off,
359            dlt_sys::DltTraceStatusType_DLT_TRACE_STATUS_ON => DltTraceStatus::On,
360            _ => DltTraceStatus::Default,
361        }
362    }
363}
364
365impl From<DltTraceStatus> for i32 {
366    fn from(value: DltTraceStatus) -> Self {
367        match value {
368            DltTraceStatus::Default => dlt_sys::DltTraceStatusType_DLT_TRACE_STATUS_DEFAULT,
369            DltTraceStatus::Off => dlt_sys::DltTraceStatusType_DLT_TRACE_STATUS_OFF,
370            DltTraceStatus::On => dlt_sys::DltTraceStatusType_DLT_TRACE_STATUS_ON,
371        }
372    }
373}
374
375/// Event sent when DLT log level or trace status changes
376///
377/// Emitted when the DLT daemon changes the log level or trace status for a context.
378///
379/// Register a listener with [`DltContextHandle::register_log_level_changed_listener()`]
380/// to receive these events
381#[derive(Debug, Clone, Copy)]
382pub struct LogLevelChangedEvent {
383    /// The DLT context ID that this change applies to
384    pub context_id: DltId,
385    /// The new log level for the context
386    pub log_level: DltLogLevel,
387    /// The new trace status for the context
388    pub trace_status: DltTraceStatus,
389}
390
391struct LogLevelChangedBroadcaster {
392    sender: broadcast::Sender<LogLevelChangedEvent>,
393    receiver: broadcast::Receiver<LogLevelChangedEvent>,
394}
395
396// Global registry for log level change callbacks
397static CALLBACK_REGISTRY: OnceLock<RwLock<HashMap<DltId, LogLevelChangedBroadcaster>>> =
398    OnceLock::new();
399
400static APP_REGISTERED: AtomicBool = AtomicBool::new(false);
401
402/// Internal C callback that forwards to the Rust channel
403unsafe extern "C" fn internal_log_level_callback(
404    context_id: *mut std::os::raw::c_char,
405    log_level: u8,
406    trace_status: u8,
407) {
408    if context_id.is_null() {
409        return;
410    }
411
412    let Some(registry) = CALLBACK_REGISTRY.get() else {
413        return;
414    };
415
416    let id = unsafe {
417        let mut ctx_id = [0u8; DLT_ID_SIZE_USIZE];
418        ptr::copy(
419            context_id.cast::<u8>(),
420            ctx_id.as_mut_ptr(),
421            DLT_ID_SIZE_USIZE,
422        );
423        match DltId::new(&ctx_id) {
424            Ok(id) => id,
425            Err(_) => return, // Invalid context ID from DLT daemon
426        }
427    };
428
429    let Ok(lock) = registry.read() else {
430        return;
431    };
432
433    let Some(broadcaster) = lock.get(&id) else {
434        return;
435    };
436
437    let event = LogLevelChangedEvent {
438        context_id: id,
439        log_level: DltLogLevel::from(i32::from(log_level)),
440        trace_status: DltTraceStatus::from(i32::from(trace_status)),
441    };
442
443    let _ = broadcaster.sender.send(event);
444}
445
446/// Internal shared state for the DLT application
447///
448/// This ensures contexts can keep the application alive through reference counting.
449/// When the last reference (either from `DltApplication` or `DltContextHandle`) is
450/// dropped, the application is automatically unregistered from DLT.
451struct DltApplicationHandle {
452    _private: (),
453}
454
455impl Drop for DltApplicationHandle {
456    fn drop(&mut self) {
457        unsafe {
458            // unregister from dlt, but ignore errors
459            dlt_sys::unregisterApplicationFlushBufferedLogs();
460            dlt_sys::dltFree();
461            APP_REGISTERED.store(false, std::sync::atomic::Ordering::SeqCst);
462        }
463    }
464}
465
466/// Singleton guard for DLT application registration
467///
468/// Only one DLT application can be registered per process. Automatically unregistered
469/// when dropped and a new application can be registered.
470///
471/// **Lifetime Guarantee**: Contexts maintain an internal reference, keeping the application
472/// registered. Safe to drop the application handle before contexts.
473///
474/// Cheaply cloneable for sharing across threads
475pub struct DltApplication {
476    inner: Arc<DltApplicationHandle>,
477}
478
479impl DltApplication {
480    /// Register a DLT application
481    ///
482    /// Only one application can be registered per process. If you need to register
483    /// a different application, drop this instance first.
484    ///
485    /// The returned handle can be cloned to share the application across threads.
486    ///
487    /// # Errors
488    /// Returns `DltError` if the registration fails
489    pub fn register(app_id: &DltId, app_description: &str) -> Result<Self, DltError> {
490        if APP_REGISTERED
491            .compare_exchange(
492                false,
493                true,
494                std::sync::atomic::Ordering::SeqCst,
495                std::sync::atomic::Ordering::SeqCst,
496            )
497            .is_err()
498        {
499            return Err(DltError::ApplicationRegistrationFailed(
500                "An application is already registered in this process".to_string(),
501            ));
502        }
503
504        let app_id_str = app_id.as_str()?;
505        let app_id_c = CString::new(app_id_str).map_err(|_| {
506            DltError::InvalidString("App id could not be converted to string".to_owned())
507        })?;
508        let app_desc_c = CString::new(app_description).map_err(|_| {
509            DltError::InvalidString("Context id could not be converted to string".to_owned())
510        })?;
511
512        unsafe {
513            let ret = dlt_sys::registerApplication(app_id_c.as_ptr(), app_desc_c.as_ptr());
514            DltSysError::from_return_code(ret).map_err(|_| {
515                DltError::ApplicationRegistrationFailed(format!(
516                    "Failed to register application: {ret}"
517                ))
518            })?;
519        }
520        Ok(DltApplication {
521            inner: Arc::new(DltApplicationHandle { _private: () }),
522        })
523    }
524
525    /// Create a new DLT context within this application
526    ///
527    /// The created context maintains an internal reference to the application,
528    /// ensuring the application remains registered as long as the context exists.
529    ///
530    /// # Errors
531    /// Returns `DltError` if registration fails
532    pub fn create_context(
533        &self,
534        context_id: &DltId,
535        context_description: &str,
536    ) -> Result<DltContextHandle, DltError> {
537        DltContextHandle::new(context_id, context_description, Arc::clone(&self.inner))
538    }
539}
540
541impl Clone for DltApplication {
542    fn clone(&self) -> Self {
543        Self {
544            inner: Arc::clone(&self.inner),
545        }
546    }
547}
548
549// Safe to send between threads (application registration is process-wide)
550unsafe impl Send for DltApplication {}
551unsafe impl Sync for DltApplication {}
552
553/// Safe wrapper around C DLT context with RAII semantics
554///
555/// The context holds an internal reference to the application, ensuring the
556/// application remains registered as long as any context exists.
557pub struct DltContextHandle {
558    context: *mut DltContext,
559    _app: Arc<DltApplicationHandle>,
560}
561
562impl DltContextHandle {
563    /// Register a new DLT context
564    ///
565    /// # Errors
566    /// Returns `DltError` if registration fails
567    fn new(
568        context_id: &DltId,
569        context_description: &str,
570        app: Arc<DltApplicationHandle>,
571    ) -> Result<Self, DltError> {
572        let context_id_str = context_id.as_str()?;
573        let ctx_id_c = CString::new(context_id_str)
574            .map_err(|_| DltError::InvalidString("Context ID is not a valid string".to_owned()))?;
575        let ctx_desc_c = CString::new(context_description).map_err(|_| {
576            DltError::InvalidString("Context description is not a valid string".to_owned())
577        })?;
578
579        unsafe {
580            let mut context = Box::new(std::mem::zeroed::<DltContext>());
581            let rv =
582                dlt_sys::registerContext(ctx_id_c.as_ptr(), ctx_desc_c.as_ptr(), context.as_mut());
583            DltSysError::from_return_code(rv)
584                .map_err(|e| DltError::ContextRegistrationFailed(format!("{e}")))?;
585
586            Ok(DltContextHandle {
587                context: Box::into_raw(context),
588                _app: app,
589            })
590        }
591    }
592
593    fn raw_context(&self) -> Result<DltContext, DltError> {
594        let context = unsafe {
595            if self.context.is_null() {
596                return Err(DltError::ContextRegistrationFailed(
597                    "Context pointer is null".to_string(),
598                ));
599            }
600            *self.context
601        };
602        Ok(context)
603    }
604
605    /// Get the context ID
606    /// # Errors
607    /// Returns `DltError` if the context is invalid or the context is null
608    pub fn context_id(&self) -> Result<DltId, DltError> {
609        let ctx_id = unsafe {
610            // this is a false positive of clippy.
611            // raw_context.contextID is of type [::std::os::raw::c_char; 4usize], which
612            // cannot be directly used as &[u8; 4].
613            #[allow(clippy::useless_transmute)]
614            std::mem::transmute::<[std::os::raw::c_char; 4], [u8; 4]>(self.raw_context()?.contextID)
615        };
616        DltId::new(&ctx_id)
617    }
618
619    /// Get the current trace status of the context
620    /// # Errors
621    /// Returns `DltError` if the context is invalid or the context is null
622    #[must_use]
623    pub fn trace_status(&self) -> DltTraceStatus {
624        self.raw_context()
625            .ok()
626            .and_then(|rc| {
627                if rc.log_level_ptr.is_null() {
628                    None
629                } else {
630                    Some(DltTraceStatus::from(i32::from(unsafe {
631                        *rc.trace_status_ptr
632                    })))
633                }
634            })
635            .unwrap_or(DltTraceStatus::Default)
636    }
637
638    /// Get the current log level of the context
639    #[must_use]
640    pub fn log_level(&self) -> DltLogLevel {
641        self.raw_context()
642            .ok()
643            .and_then(|rc| {
644                if rc.log_level_ptr.is_null() {
645                    None
646                } else {
647                    Some(DltLogLevel::from(i32::from(unsafe { *rc.log_level_ptr })))
648                }
649            })
650            .unwrap_or(DltLogLevel::Default)
651    }
652
653    /// Log a simple string message
654    ///
655    /// # Errors
656    /// Returns `DltError` if logging fails
657    pub fn log(&self, log_level: DltLogLevel, message: &str) -> Result<(), DltSysError> {
658        let msg_c = CString::new(message).map_err(|_| DltSysError::WrongParameter)?;
659
660        unsafe {
661            let ret = dlt_sys::logDlt(self.context, log_level.into(), msg_c.as_ptr());
662            DltSysError::from_return_code(ret)
663        }
664    }
665
666    /// Start a complex log message with a custom timestamp.
667    /// Can be used to hide original timestamps or to log event recorded earlier.
668    /// The timestamp is a steady clock, starting from an arbitrary point in time,
669    /// usually system start.
670    ///
671    /// # Errors
672    /// Returns `DltError` if starting the log message fails
673    pub fn log_write_start_custom_timestamp(
674        &self,
675        log_level: DltLogLevel,
676        timestamp_microseconds: u64,
677    ) -> Result<DltLogWriter, DltSysError> {
678        let mut log_writer = self.log_write_start(log_level)?;
679        // timestamp resolution in dlt is .1 milliseconds.
680        let timestamp =
681            u32::try_from(timestamp_microseconds / 100).map_err(|_| DltSysError::WrongParameter)?;
682        log_writer.log_data.use_timestamp = dlt_sys::DltTimestampType_DLT_USER_TIMESTAMP;
683        log_writer.log_data.user_timestamp = timestamp;
684        Ok(log_writer)
685    }
686
687    /// Start a complex log message
688    ///
689    /// # Errors
690    /// Returns `DltError` if starting the log message fails
691    pub fn log_write_start(&self, log_level: DltLogLevel) -> Result<DltLogWriter, DltSysError> {
692        let mut log_data = DltContextData::default();
693
694        unsafe {
695            let ret =
696                dlt_sys::dltUserLogWriteStart(self.context, &raw mut log_data, log_level.into());
697
698            DltSysError::from_return_code(ret)?;
699            Ok(DltLogWriter { log_data })
700        }
701    }
702
703    /// Register a channel to receive log level change notifications
704    ///
705    /// Returns a receiver that will get `LogLevelChangeEvent`
706    /// when the DLT daemon changes log levels
707    ///
708    /// # Errors
709    /// Returns `InternalError` if callback registration with DLT fails
710    pub fn register_log_level_changed_listener(
711        &self,
712    ) -> Result<broadcast::Receiver<LogLevelChangedEvent>, DltError> {
713        let rwlock = CALLBACK_REGISTRY.get_or_init(|| RwLock::new(HashMap::new()));
714        let mut guard = rwlock.write().map_err(|_| DltError::BadLock)?;
715        let ctx_id = self.context_id()?;
716
717        if let Some(broadcaster) = guard.get_mut(&ctx_id) {
718            Ok(broadcaster.receiver.resubscribe())
719        } else {
720            unsafe {
721                let ret = dlt_sys::registerLogLevelChangedCallback(
722                    self.context,
723                    Some(internal_log_level_callback),
724                );
725                DltSysError::from_return_code(ret)
726                    .map_err(|e| DltError::LogLevelListenerRegistrationFailed(format!("{e}")))?;
727            }
728            let (tx, rx) = broadcast::channel(5);
729            let rx_clone = rx.resubscribe();
730            guard.insert(
731                ctx_id,
732                LogLevelChangedBroadcaster {
733                    sender: tx,
734                    receiver: rx,
735                },
736            );
737            Ok(rx_clone)
738        }
739    }
740}
741
742impl Drop for DltContextHandle {
743    fn drop(&mut self) {
744        let context_id = self.context_id();
745        if let Some(lock) = CALLBACK_REGISTRY.get()
746            && let Ok(mut guard) = lock.write()
747            && let Ok(ctx_id) = context_id
748        {
749            guard.remove(&ctx_id);
750        }
751
752        unsafe {
753            dlt_sys::unregisterContext(self.context);
754            // free the memory allocated for the context
755            // not done in the C wrapper, because the wrapper also does not init it
756            let _ = Box::from_raw(self.context);
757        }
758    }
759}
760
761// Safe to send between threads, per DLT documentation
762unsafe impl Send for DltContextHandle {}
763unsafe impl Sync for DltContextHandle {}
764
765/// Builder for structured log messages with multiple typed fields
766///
767/// Construct log messages with typed data fields sent in binary format for efficiency.
768/// Each field retains type information for proper display in DLT viewers.
769///
770/// # Usage
771///
772/// 1. Start with [`DltContextHandle::log_write_start()`]
773/// 2. Chain `write_*` methods to add fields
774/// 3. Call [`finish()`](DltLogWriter::finish()) to send
775///
776/// Auto-finishes on drop if [`finish()`](DltLogWriter::finish()) not called (errors ignored).
777///
778/// # Example
779///
780/// ```no_run
781/// # use dlt_rs::{DltApplication, DltId, DltLogLevel};
782/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
783/// # let app = DltApplication::register(&DltId::new(b"MBTI")?,
784///     "Measurement and Bus Trace Interface")?;
785/// # let ctx = app.create_context(&DltId::new(b"MEAS")?, "Context")?;
786/// let mut writer = ctx.log_write_start(DltLogLevel::Info)?;
787/// writer.write_string("Temperature:")?
788///    .write_float32(87.5)?
789///    .write_string("°C")?;
790/// writer.finish()?;
791/// # Ok(())
792/// # }
793/// ```
794///
795/// # Available Methods
796///
797/// [`write_string()`](DltLogWriter::write_string()) |
798/// [`write_i32()`](DltLogWriter::write_i32()) |
799/// [`write_u32()`](DltLogWriter::write_u32()) |
800/// [`write_int64()`](DltLogWriter::write_int64()) |
801/// [`write_uint64()`](DltLogWriter::write_uint64()) |
802/// [`write_float32()`](DltLogWriter::write_float32()) |
803/// [`write_float64()`](DltLogWriter::write_float64()) |
804/// [`write_bool()`](DltLogWriter::write_bool())
805pub struct DltLogWriter {
806    log_data: DltContextData,
807}
808
809impl DltLogWriter {
810    /// Write a string to the log message
811    ///
812    /// # Errors
813    /// Returns `DltError` if writing fails
814    pub fn write_string(&mut self, text: &str) -> Result<&mut Self, DltSysError> {
815        let text_c = CString::new(text).map_err(|_| DltSysError::WrongParameter)?;
816
817        unsafe {
818            let ret = dlt_sys::dltUserLogWriteString(&raw mut self.log_data, text_c.as_ptr());
819            DltSysError::from_return_code(ret)?;
820        }
821
822        Ok(self)
823    }
824
825    /// Write an unsigned integer to the log message
826    ///
827    /// # Errors
828    /// Returns `DltError` if writing fails
829    pub fn write_u32(&mut self, value: u32) -> Result<&mut Self, DltSysError> {
830        unsafe {
831            let ret = dlt_sys::dltUserLogWriteUint(&raw mut self.log_data, value);
832            DltSysError::from_return_code(ret)?;
833        }
834
835        Ok(self)
836    }
837
838    /// Write a signed integer to the log message
839    ///
840    /// # Errors
841    /// Returns `DltError` if writing fails
842    pub fn write_i32(&mut self, value: i32) -> Result<&mut Self, DltSysError> {
843        unsafe {
844            let ret = dlt_sys::dltUserLogWriteInt(&raw mut self.log_data, value);
845            DltSysError::from_return_code(ret)?;
846        }
847
848        Ok(self)
849    }
850
851    /// Write an unsigned 64-bit integer to the log message
852    ///
853    /// # Errors
854    /// Returns `DltError` if writing fails
855    pub fn write_uint64(&mut self, value: u64) -> Result<&mut Self, DltSysError> {
856        unsafe {
857            let ret = dlt_sys::dltUserLogWriteUint64(&raw mut self.log_data, value);
858            DltSysError::from_return_code(ret)?;
859        }
860
861        Ok(self)
862    }
863
864    /// Write a signed 64-bit integer to the log message
865    ///
866    /// # Errors
867    /// Returns `DltError` if writing fails
868    pub fn write_int64(&mut self, value: i64) -> Result<&mut Self, DltSysError> {
869        unsafe {
870            let ret = dlt_sys::dltUserLogWriteInt64(&raw mut self.log_data, value);
871            DltSysError::from_return_code(ret)?;
872        }
873
874        Ok(self)
875    }
876
877    /// Write a 32-bit float to the log message
878    ///
879    /// # Errors
880    /// Returns `DltError` if writing fails
881    pub fn write_float32(&mut self, value: f32) -> Result<&mut Self, DltSysError> {
882        unsafe {
883            let ret = dlt_sys::dltUserLogWriteFloat32(&raw mut self.log_data, value);
884            DltSysError::from_return_code(ret)?;
885        }
886
887        Ok(self)
888    }
889
890    /// Write a 64-bit float to the log message
891    ///
892    /// # Errors
893    /// Returns `DltError` if writing fails
894    pub fn write_float64(&mut self, value: f64) -> Result<&mut Self, DltSysError> {
895        unsafe {
896            let ret = dlt_sys::dltUserLogWriteFloat64(&raw mut self.log_data, value);
897            DltSysError::from_return_code(ret)?;
898        }
899
900        Ok(self)
901    }
902
903    /// Write a boolean to the log message
904    ///
905    /// # Errors
906    /// Returns `DltError` if writing fails
907    pub fn write_bool(&mut self, value: bool) -> Result<&mut Self, DltSysError> {
908        unsafe {
909            let ret = dlt_sys::dltUserLogWriteBool(&raw mut self.log_data, u8::from(value));
910            DltSysError::from_return_code(ret)?;
911        }
912
913        Ok(self)
914    }
915
916    /// Finish and send the log message
917    ///
918    /// Explicitly finishes the log message. If not called, the message will be
919    /// automatically finished when the `DltLogWriter` is dropped, but errors will be ignored.
920    ///
921    /// # Errors
922    /// Returns `DltError` if finishing fails
923    pub fn finish(mut self) -> Result<(), DltSysError> {
924        let ret = unsafe { dlt_sys::dltUserLogWriteFinish(&raw mut self.log_data) };
925        // Prevent Drop from running since we've already finished
926        std::mem::forget(self);
927        DltSysError::from_return_code(ret)
928    }
929}
930
931impl Drop for DltLogWriter {
932    fn drop(&mut self) {
933        // Auto-finish the log message if finish() wasn't called explicitly
934        unsafe {
935            let _ = dlt_sys::dltUserLogWriteFinish(&raw mut self.log_data);
936        }
937    }
938}
939
940#[cfg(test)]
941mod tests {
942    use super::*;
943
944    #[test]
945    fn test_dlt_error_from_return_code() {
946        assert!(DltSysError::from_return_code(0).is_ok());
947        assert!(DltSysError::from_return_code(1).is_ok());
948        assert_eq!(DltSysError::from_return_code(-1), Err(DltSysError::Error));
949        assert_eq!(
950            DltSysError::from_return_code(-5),
951            Err(DltSysError::WrongParameter)
952        );
953    }
954
955    #[test]
956    fn test_dlt_id_creation() {
957        // 1 byte ID
958        let short_id = DltId::new(b"A").unwrap();
959        assert_eq!(short_id.as_str().unwrap(), "A");
960
961        // 3 byte IDs
962        let app_id = DltId::new(b"APP").unwrap();
963        assert_eq!(app_id.as_str().unwrap(), "APP");
964
965        let ctx_id = DltId::new(b"CTX").unwrap();
966        assert_eq!(ctx_id.as_str().unwrap(), "CTX");
967
968        // 4 byte ID (maximum)
969        let full_id = DltId::new(b"ABCD").unwrap();
970        assert_eq!(full_id.as_str().unwrap(), "ABCD");
971    }
972
973    #[test]
974    fn test_dlt_id_too_long() {
975        let result = DltId::new(b"TOOLONG");
976        assert_eq!(result.unwrap_err(), DltError::InvalidInput);
977    }
978
979    #[test]
980    fn test_dlt_id_empty() {
981        let result = DltId::new(b"");
982        assert_eq!(result.unwrap_err(), DltError::InvalidInput);
983    }
984
985    #[test]
986    fn test_dlt_id_non_ascii() {
987        let result = DltId::new(b"\xFF\xFE");
988        assert_eq!(result.unwrap_err(), DltError::InvalidInput);
989    }
990
991    #[test]
992    fn test_dlt_id_equality() {
993        let id1 = DltId::new(b"APP").unwrap();
994        let id2 = DltId::new(b"APP").unwrap();
995        let id3 = DltId::new(b"CTX").unwrap();
996
997        assert_eq!(id1, id2);
998        assert_ne!(id1, id3);
999
1000        // Different lengths are not equal
1001        let id4 = DltId::new(b"A").unwrap();
1002        assert_ne!(id1, id4);
1003    }
1004
1005    #[test]
1006    fn test_dlt_id_try_from_str() {
1007        let id = DltId::try_from("APP").unwrap();
1008        assert_eq!(id.as_str().unwrap(), "APP");
1009
1010        let long_id_result = DltId::try_from("TOOLONG");
1011        assert_eq!(long_id_result.unwrap_err(), DltError::InvalidInput);
1012
1013        let empty_id_result = DltId::try_from("");
1014        assert_eq!(empty_id_result.unwrap_err(), DltError::InvalidInput);
1015    }
1016}