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