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