Skip to main content

dbc_rs/fast_dbc/
mod.rs

1//! High-performance DBC wrapper for blazing fast message lookup and decoding.
2//!
3//! This module provides [`FastDbc`], a wrapper around [`Dbc`] optimized for:
4//! - **O(1) message lookup** via direct array indexing for standard CAN IDs
5//! - **Pre-computed decode plans** eliminating runtime bit calculations
6//! - **Zero-allocation decoding** with optimized hot paths
7//! - **Identity transform detection** skipping factor/offset math when possible
8//!
9//! # Example
10//!
11//! ```rust,ignore
12//! use dbc_rs::{Dbc, FastDbc};
13//!
14//! let dbc = Dbc::parse(content)?;
15//! let fast = FastDbc::new(dbc);
16//!
17//! // Pre-allocate buffer based on max signals
18//! let mut values = vec![0.0f64; fast.max_signals()];
19//!
20//! // Hot path - direct array lookup + pre-computed decode
21//! loop {
22//!     let (id, payload) = receive_frame();
23//!     if let Some(count) = fast.decode_into(id, &payload, &mut values) {
24//!         // values[0..count] contains physical values
25//!     }
26//! }
27//! ```
28
29mod fast_dbc_inner;
30pub(crate) use fast_dbc_inner::FastDbcInner;
31
32mod decode;
33mod hasher;
34
35use crate::{ByteOrder, Dbc, Message, Result};
36use decode::{DecodePlan, SignalDecode};
37use hasher::FxHashMap;
38use std::collections::HashMap;
39use std::path::Path;
40use std::sync::Arc;
41
42/// Maximum standard CAN ID for direct array lookup (11-bit = 2048 values).
43const MAX_STANDARD_ID: usize = 2048;
44
45// ============================================================================
46// FastDbc
47// ============================================================================
48
49/// High-performance DBC wrapper with optimized message lookup and decoding.
50///
51/// # Performance Optimizations
52///
53/// - **Direct array lookup**: Standard CAN IDs (0-2047) use direct array indexing
54/// - **Pre-computed decode plans**: All bit positions, masks, and flags computed at build time
55/// - **Identity transform detection**: Skips factor/offset math when factor=1, offset=0
56/// - **Cache-friendly layout**: Decode parameters packed for optimal cache usage
57/// - **FxHash for extended IDs**: Fast hash function for non-standard IDs
58///
59/// Cloning is O(1) due to internal `Arc` usage.
60#[derive(Clone)]
61pub struct FastDbc {
62    inner: Arc<FastDbcInner>,
63}
64
65impl std::fmt::Debug for FastDbc {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        f.debug_struct("FastDbc")
68            .field("message_count", &self.message_count())
69            .field("max_signals", &self.max_signals())
70            .field("total_signals", &self.total_signals())
71            .finish()
72    }
73}
74
75impl FastDbc {
76    /// Load a DBC file from disk and wrap it for fast access.
77    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
78        let dbc = Dbc::from_file(path)?;
79        Ok(Self::new(dbc))
80    }
81
82    /// Create a new FastDbc wrapper from a Dbc.
83    ///
84    /// This pre-computes all decode parameters for maximum runtime performance.
85    pub fn new(dbc: Dbc) -> Self {
86        let mut standard_ids = Box::new([usize::MAX; MAX_STANDARD_ID]);
87        let mut extended_ids: FxHashMap<u32, usize> =
88            HashMap::with_capacity_and_hasher(16, Default::default());
89        let mut decode_plans = Vec::with_capacity(dbc.messages().len());
90        let mut max_signals = 0;
91        let mut total_signals = 0;
92
93        for (msg_idx, msg) in dbc.messages().iter().enumerate() {
94            let plan_idx = decode_plans.len();
95
96            // Build decode plan
97            let signals: Vec<SignalDecode> =
98                msg.signals().iter().map(SignalDecode::from_signal).collect();
99
100            let sig_count = signals.len();
101            max_signals = max_signals.max(sig_count);
102            total_signals += sig_count;
103
104            decode_plans.push(DecodePlan {
105                message_index: msg_idx,
106                min_bytes: msg.min_bytes_required(),
107                signals,
108            });
109
110            // Index by ID
111            let id = msg.id_with_flag();
112            if !msg.is_extended() && id < MAX_STANDARD_ID as u32 {
113                standard_ids[id as usize] = plan_idx;
114            } else {
115                extended_ids.insert(id, plan_idx);
116            }
117        }
118
119        Self {
120            inner: Arc::new(FastDbcInner {
121                dbc,
122                standard_ids,
123                extended_ids,
124                decode_plans,
125                max_signals,
126                total_signals,
127            }),
128        }
129    }
130
131    // ========================================================================
132    // Message Lookup
133    // ========================================================================
134
135    /// Get decode plan index for a standard CAN ID.
136    #[inline(always)]
137    fn get_plan_index(&self, id: u32) -> Option<usize> {
138        if id < MAX_STANDARD_ID as u32 {
139            // Direct array lookup - fastest path
140            let idx = self.inner.standard_ids[id as usize];
141            if idx != usize::MAX { Some(idx) } else { None }
142        } else {
143            // Fall back to hash map for large IDs
144            self.inner.extended_ids.get(&id).copied()
145        }
146    }
147
148    /// Get decode plan index for an extended CAN ID.
149    #[inline(always)]
150    fn get_plan_index_extended(&self, id: u32) -> Option<usize> {
151        let extended_id = id | Message::EXTENDED_ID_FLAG;
152        self.inner.extended_ids.get(&extended_id).copied()
153    }
154
155    /// Get a message by standard (11-bit) CAN ID.
156    #[inline]
157    pub fn get(&self, id: u32) -> Option<&Message> {
158        self.get_plan_index(id)
159            .map(|idx| &self.inner.decode_plans[idx])
160            .and_then(|plan| self.inner.dbc.messages().at(plan.message_index))
161    }
162
163    /// Get a message by extended (29-bit) CAN ID.
164    #[inline]
165    pub fn get_extended(&self, id: u32) -> Option<&Message> {
166        self.get_plan_index_extended(id)
167            .map(|idx| &self.inner.decode_plans[idx])
168            .and_then(|plan| self.inner.dbc.messages().at(plan.message_index))
169    }
170
171    /// Get a message by CAN ID, trying extended if standard not found.
172    #[inline]
173    pub fn get_any(&self, id: u32) -> Option<&Message> {
174        self.get(id).or_else(|| self.get_extended(id))
175    }
176
177    // ========================================================================
178    // High-Speed Decode
179    // ========================================================================
180
181    /// Decode a message by standard CAN ID into the output buffer.
182    ///
183    /// This is the primary high-speed decode path:
184    /// - O(1) message lookup via direct array indexing
185    /// - Pre-computed decode parameters
186    /// - Identity transform detection (skips math when factor=1, offset=0)
187    /// - Zero allocation
188    ///
189    /// # Arguments
190    /// * `id` - Standard (11-bit) CAN ID
191    /// * `data` - Raw CAN payload bytes
192    /// * `out` - Output buffer for physical values
193    ///
194    /// # Returns
195    /// Number of signals decoded, or `None` if message not found or payload too short.
196    #[inline]
197    pub fn decode_into(&self, id: u32, data: &[u8], out: &mut [f64]) -> Option<usize> {
198        let plan_idx = self.get_plan_index(id)?;
199        let plan = &self.inner.decode_plans[plan_idx];
200
201        if data.len() < plan.min_bytes as usize {
202            return None;
203        }
204
205        Some(self.decode_with_plan(plan, data, out))
206    }
207
208    /// Decode a message by extended CAN ID into the output buffer.
209    #[inline]
210    pub fn decode_extended_into(&self, id: u32, data: &[u8], out: &mut [f64]) -> Option<usize> {
211        let plan_idx = self.get_plan_index_extended(id)?;
212        let plan = &self.inner.decode_plans[plan_idx];
213
214        if data.len() < plan.min_bytes as usize {
215            return None;
216        }
217
218        Some(self.decode_with_plan(plan, data, out))
219    }
220
221    /// Decode raw values by standard CAN ID.
222    #[inline]
223    pub fn decode_raw_into(&self, id: u32, data: &[u8], out: &mut [i64]) -> Option<usize> {
224        let plan_idx = self.get_plan_index(id)?;
225        let plan = &self.inner.decode_plans[plan_idx];
226
227        if data.len() < plan.min_bytes as usize {
228            return None;
229        }
230
231        Some(self.decode_raw_with_plan(plan, data, out))
232    }
233
234    // ========================================================================
235    // Internal Decode Implementation
236    // ========================================================================
237
238    /// Decode using pre-computed plan.
239    #[inline(always)]
240    fn decode_with_plan(&self, plan: &DecodePlan, data: &[u8], out: &mut [f64]) -> usize {
241        let mut count = 0;
242        for (out_val, sig) in out.iter_mut().zip(plan.signals.iter()) {
243            let raw = self.extract_raw(*sig, data);
244            *out_val = self.apply_scaling(*sig, raw);
245            count += 1;
246        }
247        count
248    }
249
250    /// Decode raw values using pre-computed plan.
251    #[inline(always)]
252    fn decode_raw_with_plan(&self, plan: &DecodePlan, data: &[u8], out: &mut [i64]) -> usize {
253        let mut count = 0;
254        for (out_val, sig) in out.iter_mut().zip(plan.signals.iter()) {
255            *out_val = self.extract_raw(*sig, data);
256            count += 1;
257        }
258        count
259    }
260
261    /// Extract raw signed value from data.
262    #[inline(always)]
263    fn extract_raw(&self, sig: SignalDecode, data: &[u8]) -> i64 {
264        let byte_order = if sig.is_little_endian() {
265            ByteOrder::LittleEndian
266        } else {
267            ByteOrder::BigEndian
268        };
269
270        let start_bit = sig.byte_start as usize * 8 + sig.bit_offset as usize;
271        let raw_bits = byte_order.extract_bits(data, start_bit, sig.length as usize);
272
273        if sig.is_unsigned() {
274            raw_bits as i64
275        } else {
276            Self::sign_extend(raw_bits, sig.length as usize)
277        }
278    }
279
280    /// Apply factor and offset scaling.
281    #[inline(always)]
282    fn apply_scaling(&self, sig: SignalDecode, raw: i64) -> f64 {
283        if sig.is_identity() {
284            raw as f64
285        } else {
286            (raw as f64) * sig.factor + sig.offset
287        }
288    }
289
290    /// Sign-extend a value.
291    #[inline(always)]
292    fn sign_extend(value: u64, bits: usize) -> i64 {
293        // A full-width 64-bit value is already its own two's-complement
294        // representation; `1u64 << 64` would be undefined behavior.
295        if bits >= 64 {
296            return value as i64;
297        }
298        let sign_bit = 1u64 << (bits - 1);
299        if (value & sign_bit) != 0 {
300            let mask = !((1u64 << bits) - 1);
301            (value | mask) as i64
302        } else {
303            value as i64
304        }
305    }
306
307    // ========================================================================
308    // Accessors
309    // ========================================================================
310
311    /// Get the maximum number of signals in any single message.
312    ///
313    /// Use this to pre-allocate decode buffers.
314    #[inline]
315    pub fn max_signals(&self) -> usize {
316        self.inner.max_signals
317    }
318
319    /// Get the total number of signals across all messages.
320    #[inline]
321    pub fn total_signals(&self) -> usize {
322        self.inner.total_signals
323    }
324
325    /// Get the number of messages.
326    #[inline]
327    pub fn message_count(&self) -> usize {
328        self.inner.decode_plans.len()
329    }
330
331    /// Check if a message with this standard CAN ID exists.
332    #[inline]
333    pub fn contains(&self, id: u32) -> bool {
334        self.get_plan_index(id).is_some()
335    }
336
337    /// Check if a message with this extended CAN ID exists.
338    #[inline]
339    pub fn contains_extended(&self, id: u32) -> bool {
340        self.get_plan_index_extended(id).is_some()
341    }
342
343    /// Get the underlying Dbc.
344    #[inline]
345    pub fn dbc(&self) -> &Dbc {
346        &self.inner.dbc
347    }
348
349    /// Consume and return the underlying Dbc.
350    ///
351    /// Returns the Dbc if this is the only reference, otherwise clones it.
352    #[inline]
353    pub fn into_dbc(self) -> Dbc {
354        match Arc::try_unwrap(self.inner) {
355            Ok(inner) => inner.dbc,
356            Err(arc) => arc.dbc.clone(),
357        }
358    }
359
360    /// Iterator over all CAN IDs.
361    pub fn ids(&self) -> impl Iterator<Item = u32> + '_ {
362        // Standard IDs from direct lookup table
363        let standard = self
364            .inner
365            .standard_ids
366            .iter()
367            .enumerate()
368            .filter(|(_, idx)| **idx != usize::MAX)
369            .map(|(id, _)| id as u32);
370
371        // Extended IDs from hash map
372        let extended = self.inner.extended_ids.keys().copied();
373
374        standard.chain(extended)
375    }
376}
377
378impl From<Dbc> for FastDbc {
379    fn from(dbc: Dbc) -> Self {
380        Self::new(dbc)
381    }
382}
383
384// ============================================================================
385// Tests
386// ============================================================================
387
388#[cfg(test)]
389mod tests {
390    use super::*;
391
392    #[test]
393    fn test_fast_dbc_basic() {
394        let dbc = Dbc::parse(
395            r#"VERSION "1.0"
396
397BU_: ECM
398
399BO_ 256 Engine : 8 ECM
400 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm" *
401 SG_ Temp : 16|8@1- (1,-40) [-40|215] "C" *
402"#,
403        )
404        .unwrap();
405
406        let fast = FastDbc::new(dbc);
407
408        assert_eq!(fast.message_count(), 1);
409        assert_eq!(fast.max_signals(), 2);
410        assert_eq!(fast.total_signals(), 2);
411        assert!(fast.contains(256));
412        assert!(!fast.contains(512));
413
414        let msg = fast.get(256).unwrap();
415        assert_eq!(msg.name(), "Engine");
416    }
417
418    #[test]
419    fn test_fast_dbc_decode_into() {
420        let dbc = Dbc::parse(
421            r#"VERSION "1.0"
422
423BU_: ECM
424
425BO_ 256 Engine : 8 ECM
426 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm" *
427 SG_ Temp : 16|8@1- (1,-40) [-40|215] "C" *
428"#,
429        )
430        .unwrap();
431
432        let fast = FastDbc::new(dbc);
433
434        // RPM = 2000 (raw 8000), Temp = 50°C (raw 90)
435        let payload = [0x40, 0x1F, 0x5A, 0x00, 0x00, 0x00, 0x00, 0x00];
436        let mut values = vec![0.0f64; fast.max_signals()];
437
438        let count = fast.decode_into(256, &payload, &mut values).unwrap();
439
440        assert_eq!(count, 2);
441        assert_eq!(values[0], 2000.0);
442        assert_eq!(values[1], 50.0);
443    }
444
445    #[test]
446    fn test_fast_dbc_identity_transform() {
447        let dbc = Dbc::parse(
448            r#"VERSION "1.0"
449
450BU_: ECM
451
452BO_ 256 Engine : 8 ECM
453 SG_ RawValue : 0|16@1+ (1,0) [0|65535] "" *
454"#,
455        )
456        .unwrap();
457
458        let fast = FastDbc::new(dbc);
459
460        // Raw value 12345
461        let payload = [0x39, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
462        let mut values = [0.0f64; 1];
463
464        fast.decode_into(256, &payload, &mut values);
465
466        assert_eq!(values[0], 12345.0);
467    }
468
469    #[test]
470    fn test_fast_dbc_message_not_found() {
471        let dbc = Dbc::parse(
472            r#"VERSION "1.0"
473
474BU_: ECM
475
476BO_ 256 Engine : 8 ECM
477 SG_ RPM : 0|16@1+ (1,0) [0|8000] "rpm" *
478"#,
479        )
480        .unwrap();
481
482        let fast = FastDbc::new(dbc);
483        let payload = [0x00; 8];
484        let mut values = [0.0f64; 8];
485
486        assert!(fast.decode_into(512, &payload, &mut values).is_none());
487    }
488
489    #[test]
490    fn test_fast_dbc_extended_id() {
491        let dbc = Dbc::parse(
492            r#"VERSION "1.0"
493
494BU_: ECM
495
496BO_ 2147484672 ExtendedMsg : 8 ECM
497 SG_ Speed : 0|16@1+ (0.1,0) [0|6553.5] "km/h" *
498"#,
499        )
500        .unwrap();
501        // 2147484672 = 0x80000400 = extended ID 0x400
502
503        let fast = FastDbc::new(dbc);
504
505        // Should NOT find by standard ID
506        assert!(!fast.contains(0x400));
507        assert!(fast.get(0x400).is_none());
508
509        // Should find by extended ID
510        assert!(fast.contains_extended(0x400));
511        let msg = fast.get_extended(0x400).unwrap();
512        assert_eq!(msg.name(), "ExtendedMsg");
513
514        // Decode
515        let payload = [0xE8, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
516        let mut values = [0.0f64; 8];
517
518        let count = fast.decode_extended_into(0x400, &payload, &mut values).unwrap();
519        assert_eq!(count, 1);
520        assert_eq!(values[0], 100.0); // 1000 * 0.1
521    }
522
523    #[test]
524    fn test_fast_dbc_multiple_messages() {
525        let dbc = Dbc::parse(
526            r#"VERSION "1.0"
527
528BU_: ECM
529
530BO_ 256 Msg1 : 8 ECM
531 SG_ Sig1 : 0|8@1+ (1,0) [0|255] "" *
532 SG_ Sig2 : 8|8@1+ (1,0) [0|255] "" *
533
534BO_ 512 Msg2 : 8 ECM
535 SG_ SigA : 0|16@1+ (1,0) [0|65535] "" *
536
537BO_ 768 Msg3 : 8 ECM
538 SG_ SigX : 0|8@1+ (1,0) [0|255] "" *
539 SG_ SigY : 8|8@1+ (1,0) [0|255] "" *
540 SG_ SigZ : 16|8@1+ (1,0) [0|255] "" *
541"#,
542        )
543        .unwrap();
544
545        let fast = FastDbc::new(dbc);
546
547        assert_eq!(fast.message_count(), 3);
548        assert_eq!(fast.max_signals(), 3); // Msg3 has most
549        assert_eq!(fast.total_signals(), 6);
550
551        assert!(fast.contains(256));
552        assert!(fast.contains(512));
553        assert!(fast.contains(768));
554    }
555
556    #[test]
557    fn test_fast_dbc_large_id() {
558        // Test ID >= 2048 (uses hash map)
559        let dbc = Dbc::parse(
560            r#"VERSION "1.0"
561
562BU_: ECM
563
564BO_ 3000 LargeId : 8 ECM
565 SG_ Value : 0|16@1+ (1,0) [0|65535] "" *
566"#,
567        )
568        .unwrap();
569
570        let fast = FastDbc::new(dbc);
571
572        assert!(fast.contains(3000));
573        assert!(!fast.contains(256));
574
575        let payload = [0x39, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
576        let mut values = [0.0f64; 1];
577
578        let count = fast.decode_into(3000, &payload, &mut values).unwrap();
579        assert_eq!(count, 1);
580        assert_eq!(values[0], 12345.0);
581    }
582
583    #[test]
584    fn test_fast_dbc_from_trait() {
585        let dbc = Dbc::parse(
586            r#"VERSION "1.0"
587
588BU_: ECM
589
590BO_ 256 Engine : 8 ECM
591"#,
592        )
593        .unwrap();
594
595        let fast: FastDbc = dbc.into();
596        assert_eq!(fast.message_count(), 1);
597    }
598
599    #[test]
600    fn test_fast_dbc_into_dbc() {
601        let dbc = Dbc::parse(
602            r#"VERSION "1.0"
603
604BU_: ECM
605
606BO_ 256 Engine : 8 ECM
607"#,
608        )
609        .unwrap();
610
611        let fast = FastDbc::new(dbc);
612        let dbc_back = fast.into_dbc();
613
614        assert_eq!(dbc_back.messages().len(), 1);
615    }
616
617    #[test]
618    fn test_fast_dbc_ids_iterator() {
619        let dbc = Dbc::parse(
620            r#"VERSION "1.0"
621
622BU_: ECM
623
624BO_ 100 Msg1 : 8 ECM
625BO_ 200 Msg2 : 8 ECM
626BO_ 3000 LargeId : 8 ECM
627"#,
628        )
629        .unwrap();
630
631        let fast = FastDbc::new(dbc);
632        let ids: Vec<u32> = fast.ids().collect();
633
634        assert_eq!(ids.len(), 3);
635        assert!(ids.contains(&100));
636        assert!(ids.contains(&200));
637        assert!(ids.contains(&3000));
638    }
639
640    #[test]
641    fn test_fast_dbc_decode_raw_into() {
642        let dbc = Dbc::parse(
643            r#"VERSION "1.0"
644
645BU_: ECM
646
647BO_ 256 Engine : 8 ECM
648 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm" *
649"#,
650        )
651        .unwrap();
652
653        let fast = FastDbc::new(dbc);
654
655        let payload = [0x40, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
656        let mut raw_values = [0i64; 8];
657
658        let count = fast.decode_raw_into(256, &payload, &mut raw_values).unwrap();
659
660        assert_eq!(count, 1);
661        assert_eq!(raw_values[0], 8000); // Raw before factor
662    }
663}