dbc_rs/
fast_dbc.rs

1//! High-performance DBC wrapper for fast message lookup and decoding.
2//!
3//! This module provides [`FastDbc`], a wrapper around [`Dbc`] that adds:
4//! - O(1) message lookup by CAN ID using `HashMap`
5//! - Zero-allocation decoding via [`Message::decode_into`]
6//! - Helper methods for buffer sizing
7//!
8//! # Example
9//!
10//! ```rust,ignore
11//! use dbc_rs::{Dbc, FastDbc};
12//!
13//! let dbc = Dbc::parse(content)?;
14//! let fast = FastDbc::new(dbc);
15//!
16//! // Pre-allocate buffer based on max signals
17//! let mut values = vec![0.0f64; fast.max_signals()];
18//!
19//! // Hot path - O(1) lookup + zero-alloc decode
20//! loop {
21//!     let (id, payload) = receive_frame();
22//!     if let Some(count) = fast.decode_into(id, &payload, &mut values) {
23//!         // values[0..count] contains physical values
24//!         // Use fast.get(id).unwrap().signals() to get signal metadata
25//!     }
26//! }
27//! ```
28
29use crate::{Dbc, Message, Result};
30use std::collections::HashMap;
31use std::path::Path;
32use std::sync::Arc;
33
34/// High-performance DBC wrapper with O(1) message lookup.
35///
36/// Wraps a [`Dbc`] and adds a `HashMap` index for fast message lookup by CAN ID.
37/// Use this when you need to decode many frames at high speed.
38///
39/// Cloning is O(1) due to internal `Arc` usage.
40#[derive(Debug, Clone)]
41pub struct FastDbc {
42    /// Shared inner data (cheap to clone)
43    inner: Arc<FastDbcInner>,
44}
45
46/// Inner data for FastDbc (shared via Arc).
47#[derive(Debug)]
48struct FastDbcInner {
49    /// The underlying DBC
50    dbc: Dbc,
51    /// O(1) message lookup by CAN ID (standard IDs stored directly, extended with flag)
52    index: HashMap<u32, usize>,
53    /// Maximum signals in any single message
54    max_signals: usize,
55    /// Total signal count across all messages
56    total_signals: usize,
57}
58
59impl FastDbc {
60    /// Load a DBC file from disk and wrap it for fast access.
61    ///
62    /// This is a convenience method that combines [`Dbc::from_file`] and [`FastDbc::new`].
63    ///
64    /// # Examples
65    ///
66    /// ```rust,no_run
67    /// use dbc_rs::FastDbc;
68    ///
69    /// let fast_dbc = FastDbc::from_file("path/to/file.dbc")?;
70    /// println!("Loaded {} messages", fast_dbc.message_count());
71    /// # Ok::<(), dbc_rs::Error>(())
72    /// ```
73    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
74        let dbc = Dbc::from_file(path)?;
75        Ok(Self::new(dbc))
76    }
77
78    /// Create a new FastDbc wrapper from a Dbc.
79    ///
80    /// This builds a HashMap index for O(1) message lookup.
81    pub fn new(dbc: Dbc) -> Self {
82        let mut index = HashMap::with_capacity(dbc.messages().len());
83        let mut max_signals = 0;
84        let mut total_signals = 0;
85
86        for (i, msg) in dbc.messages().iter().enumerate() {
87            // Use internal ID with extended flag for correct lookup
88            index.insert(msg.id_with_flag(), i);
89            let sig_count = msg.signals().len();
90            max_signals = max_signals.max(sig_count);
91            total_signals += sig_count;
92        }
93
94        Self {
95            inner: Arc::new(FastDbcInner {
96                dbc,
97                index,
98                max_signals,
99                total_signals,
100            }),
101        }
102    }
103
104    /// Get a message by standard (11-bit) CAN ID.
105    ///
106    /// Returns `None` if no message with this ID exists.
107    ///
108    /// # Performance
109    /// O(1) average case.
110    #[inline]
111    pub fn get(&self, id: u32) -> Option<&Message> {
112        self.inner.index.get(&id).and_then(|&idx| self.inner.dbc.messages().at(idx))
113    }
114
115    /// Get a message by extended (29-bit) CAN ID.
116    ///
117    /// Use this for extended CAN IDs.
118    #[inline]
119    pub fn get_extended(&self, id: u32) -> Option<&Message> {
120        let extended_id = id | Message::EXTENDED_ID_FLAG;
121        self.inner
122            .index
123            .get(&extended_id)
124            .and_then(|&idx| self.inner.dbc.messages().at(idx))
125    }
126
127    /// Get a message by CAN ID, trying with extended flag if standard not found.
128    ///
129    /// Single lookup optimization: checks if id exists, then tries with extended flag.
130    #[inline]
131    pub fn get_any(&self, id: u32) -> Option<&Message> {
132        // Try standard first, then extended - but use single index access pattern
133        self.inner
134            .index
135            .get(&id)
136            .or_else(|| self.inner.index.get(&(id | Message::EXTENDED_ID_FLAG)))
137            .and_then(|&idx| self.inner.dbc.messages().at(idx))
138    }
139
140    /// Decode a message by standard CAN ID into the output buffer.
141    ///
142    /// This is the primary high-speed decode path:
143    /// - O(1) message lookup
144    /// - Zero allocation
145    /// - Direct buffer write
146    ///
147    /// # Arguments
148    /// * `id` - Standard (11-bit) CAN ID
149    /// * `data` - Raw CAN payload bytes
150    /// * `out` - Output buffer for physical values
151    ///
152    /// # Returns
153    /// Number of signals decoded, or `None` if message not found or payload too short.
154    #[inline]
155    pub fn decode_into(&self, id: u32, data: &[u8], out: &mut [f64]) -> Option<usize> {
156        let msg = self.get(id)?;
157        let count = msg.decode_into(data, out);
158        if count > 0 { Some(count) } else { None }
159    }
160
161    /// Decode a message by extended CAN ID into the output buffer.
162    #[inline]
163    pub fn decode_extended_into(&self, id: u32, data: &[u8], out: &mut [f64]) -> Option<usize> {
164        let msg = self.get_extended(id)?;
165        let count = msg.decode_into(data, out);
166        if count > 0 { Some(count) } else { None }
167    }
168
169    /// Decode raw values by standard CAN ID.
170    #[inline]
171    pub fn decode_raw_into(&self, id: u32, data: &[u8], out: &mut [i64]) -> Option<usize> {
172        let msg = self.get(id)?;
173        let count = msg.decode_raw_into(data, out);
174        if count > 0 { Some(count) } else { None }
175    }
176
177    /// Get the maximum number of signals in any single message.
178    ///
179    /// Use this to pre-allocate decode buffers.
180    #[inline]
181    pub fn max_signals(&self) -> usize {
182        self.inner.max_signals
183    }
184
185    /// Get the total number of signals across all messages.
186    #[inline]
187    pub fn total_signals(&self) -> usize {
188        self.inner.total_signals
189    }
190
191    /// Get the number of messages.
192    #[inline]
193    pub fn message_count(&self) -> usize {
194        self.inner.dbc.messages().len()
195    }
196
197    /// Check if a message with this standard CAN ID exists.
198    #[inline]
199    pub fn contains(&self, id: u32) -> bool {
200        self.inner.index.contains_key(&id)
201    }
202
203    /// Check if a message with this extended CAN ID exists.
204    #[inline]
205    pub fn contains_extended(&self, id: u32) -> bool {
206        self.inner.index.contains_key(&(id | Message::EXTENDED_ID_FLAG))
207    }
208
209    /// Get the underlying Dbc.
210    #[inline]
211    pub fn dbc(&self) -> &Dbc {
212        &self.inner.dbc
213    }
214
215    /// Consume and return the underlying Dbc.
216    ///
217    /// Returns the Dbc if this is the only reference, otherwise clones it.
218    #[inline]
219    pub fn into_dbc(self) -> Dbc {
220        match Arc::try_unwrap(self.inner) {
221            Ok(inner) => inner.dbc,
222            Err(arc) => arc.dbc.clone(),
223        }
224    }
225
226    /// Iterator over all CAN IDs (with extended flag where applicable).
227    pub fn ids(&self) -> impl Iterator<Item = u32> + '_ {
228        self.inner.index.keys().copied()
229    }
230}
231
232impl From<Dbc> for FastDbc {
233    fn from(dbc: Dbc) -> Self {
234        Self::new(dbc)
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241
242    #[test]
243    fn test_fast_dbc_basic() {
244        let dbc = Dbc::parse(
245            r#"VERSION "1.0"
246
247BU_: ECM
248
249BO_ 256 Engine : 8 ECM
250 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm" *
251 SG_ Temp : 16|8@1- (1,-40) [-40|215] "C" *
252"#,
253        )
254        .unwrap();
255
256        let fast = FastDbc::new(dbc);
257
258        assert_eq!(fast.message_count(), 1);
259        assert_eq!(fast.max_signals(), 2);
260        assert_eq!(fast.total_signals(), 2);
261        assert!(fast.contains(256));
262        assert!(!fast.contains(512));
263
264        let msg = fast.get(256).unwrap();
265        assert_eq!(msg.name(), "Engine");
266    }
267
268    #[test]
269    fn test_fast_dbc_decode_into() {
270        let dbc = Dbc::parse(
271            r#"VERSION "1.0"
272
273BU_: ECM
274
275BO_ 256 Engine : 8 ECM
276 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm" *
277 SG_ Temp : 16|8@1- (1,-40) [-40|215] "C" *
278"#,
279        )
280        .unwrap();
281
282        let fast = FastDbc::new(dbc);
283
284        // RPM = 2000 (raw 8000), Temp = 50°C (raw 90)
285        let payload = [0x40, 0x1F, 0x5A, 0x00, 0x00, 0x00, 0x00, 0x00];
286        let mut values = vec![0.0f64; fast.max_signals()];
287
288        let count = fast.decode_into(256, &payload, &mut values).unwrap();
289
290        assert_eq!(count, 2);
291        assert_eq!(values[0], 2000.0);
292        assert_eq!(values[1], 50.0);
293    }
294
295    #[test]
296    fn test_fast_dbc_message_not_found() {
297        let dbc = Dbc::parse(
298            r#"VERSION "1.0"
299
300BU_: ECM
301
302BO_ 256 Engine : 8 ECM
303 SG_ RPM : 0|16@1+ (1,0) [0|8000] "rpm" *
304"#,
305        )
306        .unwrap();
307
308        let fast = FastDbc::new(dbc);
309        let payload = [0x00; 8];
310        let mut values = [0.0f64; 8];
311
312        assert!(fast.decode_into(512, &payload, &mut values).is_none());
313    }
314
315    #[test]
316    fn test_fast_dbc_extended_id() {
317        let dbc = Dbc::parse(
318            r#"VERSION "1.0"
319
320BU_: ECM
321
322BO_ 2147484672 ExtendedMsg : 8 ECM
323 SG_ Speed : 0|16@1+ (0.1,0) [0|6553.5] "km/h" *
324"#,
325        )
326        .unwrap();
327        // 2147484672 = 0x80000400 = extended ID 0x400
328
329        let fast = FastDbc::new(dbc);
330
331        // Should NOT find by standard ID
332        assert!(!fast.contains(0x400));
333        assert!(fast.get(0x400).is_none());
334
335        // Should find by extended ID
336        assert!(fast.contains_extended(0x400));
337        let msg = fast.get_extended(0x400).unwrap();
338        assert_eq!(msg.name(), "ExtendedMsg");
339
340        // Decode
341        let payload = [0xE8, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
342        let mut values = [0.0f64; 8];
343
344        let count = fast.decode_extended_into(0x400, &payload, &mut values).unwrap();
345        assert_eq!(count, 1);
346        assert_eq!(values[0], 100.0); // 1000 * 0.1
347    }
348
349    #[test]
350    fn test_fast_dbc_multiple_messages() {
351        let dbc = Dbc::parse(
352            r#"VERSION "1.0"
353
354BU_: ECM
355
356BO_ 256 Msg1 : 8 ECM
357 SG_ Sig1 : 0|8@1+ (1,0) [0|255] "" *
358 SG_ Sig2 : 8|8@1+ (1,0) [0|255] "" *
359
360BO_ 512 Msg2 : 8 ECM
361 SG_ SigA : 0|16@1+ (1,0) [0|65535] "" *
362
363BO_ 768 Msg3 : 8 ECM
364 SG_ SigX : 0|8@1+ (1,0) [0|255] "" *
365 SG_ SigY : 8|8@1+ (1,0) [0|255] "" *
366 SG_ SigZ : 16|8@1+ (1,0) [0|255] "" *
367"#,
368        )
369        .unwrap();
370
371        let fast = FastDbc::new(dbc);
372
373        assert_eq!(fast.message_count(), 3);
374        assert_eq!(fast.max_signals(), 3); // Msg3 has most
375        assert_eq!(fast.total_signals(), 6);
376
377        assert!(fast.contains(256));
378        assert!(fast.contains(512));
379        assert!(fast.contains(768));
380    }
381
382    #[test]
383    fn test_fast_dbc_from_trait() {
384        let dbc = Dbc::parse(
385            r#"VERSION "1.0"
386
387BU_: ECM
388
389BO_ 256 Engine : 8 ECM
390"#,
391        )
392        .unwrap();
393
394        let fast: FastDbc = dbc.into();
395        assert_eq!(fast.message_count(), 1);
396    }
397
398    #[test]
399    fn test_fast_dbc_into_dbc() {
400        let dbc = Dbc::parse(
401            r#"VERSION "1.0"
402
403BU_: ECM
404
405BO_ 256 Engine : 8 ECM
406"#,
407        )
408        .unwrap();
409
410        let fast = FastDbc::new(dbc);
411        let dbc_back = fast.into_dbc();
412
413        assert_eq!(dbc_back.messages().len(), 1);
414    }
415}