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 a message by CAN ID, yielding `(name, physical value)` pairs.
222    ///
223    /// This is the ergonomic counterpart of [`decode_into`](Self::decode_into)
224    /// for callers that want signal names alongside values: one message
225    /// lookup, no output buffer to manage, and no allocation — signals are
226    /// decoded lazily as the iterator is advanced, in DBC declaration order
227    /// (the same order `decode_into` fills its buffer).
228    ///
229    /// Takes `is_extended` like [`Dbc::decode`], so a receive loop can pass
230    /// the frame's IDE flag straight through.
231    ///
232    /// # Returns
233    /// `None` if the message is not found or the payload is too short.
234    #[inline]
235    pub fn decode<'a>(
236        &'a self,
237        id: u32,
238        data: &'a [u8],
239        is_extended: bool,
240    ) -> Option<impl Iterator<Item = (&'a str, f64)> + 'a> {
241        let plan_idx = if is_extended {
242            self.get_plan_index_extended(id)
243        } else {
244            self.get_plan_index(id)
245        };
246        self.decode_named_with_plan(plan_idx?, data)
247    }
248
249    /// Decode raw values by standard CAN ID.
250    #[inline]
251    pub fn decode_raw_into(&self, id: u32, data: &[u8], out: &mut [i64]) -> Option<usize> {
252        let plan_idx = self.get_plan_index(id)?;
253        let plan = &self.inner.decode_plans[plan_idx];
254
255        if data.len() < plan.min_bytes as usize {
256            return None;
257        }
258
259        Some(self.decode_raw_with_plan(plan, data, out))
260    }
261
262    // ========================================================================
263    // Internal Decode Implementation
264    // ========================================================================
265
266    /// Decode `(name, value)` pairs using a pre-computed plan.
267    #[inline]
268    fn decode_named_with_plan<'a>(
269        &'a self,
270        plan_idx: usize,
271        data: &'a [u8],
272    ) -> Option<impl Iterator<Item = (&'a str, f64)> + 'a> {
273        let plan = &self.inner.decode_plans[plan_idx];
274
275        if data.len() < plan.min_bytes as usize {
276            return None;
277        }
278
279        let message = self.inner.dbc.messages().at(plan.message_index)?;
280        Some(
281            message
282                .signals()
283                .iter()
284                .zip(plan.signals.iter())
285                .map(move |(signal, sig)| (signal.name(), self.decode_physical(*sig, data))),
286        )
287    }
288
289    /// Decode using pre-computed plan.
290    #[inline(always)]
291    fn decode_with_plan(&self, plan: &DecodePlan, data: &[u8], out: &mut [f64]) -> usize {
292        let mut count = 0;
293        for (out_val, sig) in out.iter_mut().zip(plan.signals.iter()) {
294            *out_val = self.decode_physical(*sig, data);
295            count += 1;
296        }
297        count
298    }
299
300    /// Decode a signal's physical value from its pre-computed plan.
301    #[inline(always)]
302    fn decode_physical(&self, sig: SignalDecode, data: &[u8]) -> f64 {
303        // Compute the physical value from the correct integer domain: a
304        // 64-bit unsigned value with the MSB set must not be treated as a
305        // negative i64 (mirrors `Signal::decode_raw`).
306        let raw_bits = self.extract_bits(sig, data);
307        let raw = if sig.is_unsigned() {
308            raw_bits as f64
309        } else {
310            Self::sign_extend(raw_bits, sig.length as usize) as f64
311        };
312        self.apply_scaling(sig, raw)
313    }
314
315    /// Decode raw values using pre-computed plan.
316    #[inline(always)]
317    fn decode_raw_with_plan(&self, plan: &DecodePlan, data: &[u8], out: &mut [i64]) -> usize {
318        let mut count = 0;
319        for (out_val, sig) in out.iter_mut().zip(plan.signals.iter()) {
320            *out_val = self.extract_raw(*sig, data);
321            count += 1;
322        }
323        count
324    }
325
326    /// Extract the raw bit pattern for a signal.
327    #[inline(always)]
328    fn extract_bits(&self, sig: SignalDecode, data: &[u8]) -> u64 {
329        let byte_order = if sig.is_little_endian() {
330            ByteOrder::LittleEndian
331        } else {
332            ByteOrder::BigEndian
333        };
334
335        let start_bit = sig.byte_start as usize * 8 + sig.bit_offset as usize;
336        byte_order.extract_bits(data, start_bit, sig.length as usize)
337    }
338
339    /// Extract raw signed value from data.
340    #[inline(always)]
341    fn extract_raw(&self, sig: SignalDecode, data: &[u8]) -> i64 {
342        let raw_bits = self.extract_bits(sig, data);
343
344        if sig.is_unsigned() {
345            raw_bits as i64
346        } else {
347            Self::sign_extend(raw_bits, sig.length as usize)
348        }
349    }
350
351    /// Apply factor and offset scaling.
352    #[inline(always)]
353    fn apply_scaling(&self, sig: SignalDecode, raw: f64) -> f64 {
354        if sig.is_identity() {
355            raw
356        } else {
357            raw * sig.factor + sig.offset
358        }
359    }
360
361    /// Sign-extend a value.
362    #[inline(always)]
363    fn sign_extend(value: u64, bits: usize) -> i64 {
364        // A full-width 64-bit value is already its own two's-complement
365        // representation; `1u64 << 64` would be undefined behavior.
366        if bits >= 64 {
367            return value as i64;
368        }
369        let sign_bit = 1u64 << (bits - 1);
370        if (value & sign_bit) != 0 {
371            let mask = !((1u64 << bits) - 1);
372            (value | mask) as i64
373        } else {
374            value as i64
375        }
376    }
377
378    // ========================================================================
379    // Accessors
380    // ========================================================================
381
382    /// Get the maximum number of signals in any single message.
383    ///
384    /// Use this to pre-allocate decode buffers.
385    #[inline]
386    pub fn max_signals(&self) -> usize {
387        self.inner.max_signals
388    }
389
390    /// Get the total number of signals across all messages.
391    #[inline]
392    pub fn total_signals(&self) -> usize {
393        self.inner.total_signals
394    }
395
396    /// Get the number of messages.
397    #[inline]
398    pub fn message_count(&self) -> usize {
399        self.inner.decode_plans.len()
400    }
401
402    /// Check if a message with this standard CAN ID exists.
403    #[inline]
404    pub fn contains(&self, id: u32) -> bool {
405        self.get_plan_index(id).is_some()
406    }
407
408    /// Check if a message with this extended CAN ID exists.
409    #[inline]
410    pub fn contains_extended(&self, id: u32) -> bool {
411        self.get_plan_index_extended(id).is_some()
412    }
413
414    /// Get the underlying Dbc.
415    #[inline]
416    pub fn dbc(&self) -> &Dbc {
417        &self.inner.dbc
418    }
419
420    /// Consume and return the underlying Dbc.
421    ///
422    /// Returns the Dbc if this is the only reference, otherwise clones it.
423    #[inline]
424    pub fn into_dbc(self) -> Dbc {
425        match Arc::try_unwrap(self.inner) {
426            Ok(inner) => inner.dbc,
427            Err(arc) => arc.dbc.clone(),
428        }
429    }
430
431    /// Iterator over all CAN IDs.
432    pub fn ids(&self) -> impl Iterator<Item = u32> + '_ {
433        // Standard IDs from direct lookup table
434        let standard = self
435            .inner
436            .standard_ids
437            .iter()
438            .enumerate()
439            .filter(|(_, idx)| **idx != usize::MAX)
440            .map(|(id, _)| id as u32);
441
442        // Extended IDs from hash map
443        let extended = self.inner.extended_ids.keys().copied();
444
445        standard.chain(extended)
446    }
447}
448
449impl From<Dbc> for FastDbc {
450    fn from(dbc: Dbc) -> Self {
451        Self::new(dbc)
452    }
453}
454
455// ============================================================================
456// Tests
457// ============================================================================
458
459#[cfg(test)]
460mod tests {
461    use super::*;
462
463    #[test]
464    fn test_fast_dbc_basic() {
465        let dbc = Dbc::parse(
466            r#"VERSION "1.0"
467
468BU_: ECM
469
470BO_ 256 Engine : 8 ECM
471 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm" *
472 SG_ Temp : 16|8@1- (1,-40) [-40|215] "C" *
473"#,
474        )
475        .unwrap();
476
477        let fast = FastDbc::new(dbc);
478
479        assert_eq!(fast.message_count(), 1);
480        assert_eq!(fast.max_signals(), 2);
481        assert_eq!(fast.total_signals(), 2);
482        assert!(fast.contains(256));
483        assert!(!fast.contains(512));
484
485        let msg = fast.get(256).unwrap();
486        assert_eq!(msg.name(), "Engine");
487    }
488
489    #[test]
490    fn test_fast_dbc_decode_named() {
491        let dbc = Dbc::parse(
492            r#"VERSION "1.0"
493
494BU_: ECM
495
496BO_ 256 Engine : 8 ECM
497 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm" *
498 SG_ Temp : 16|8@1- (1,-40) [-40|215] "C" *
499"#,
500        )
501        .unwrap();
502
503        let fast = FastDbc::new(dbc);
504
505        // RPM = 2000 (raw 8000), Temp = 50°C (raw 90)
506        let payload = [0x40, 0x1F, 0x5A, 0x00, 0x00, 0x00, 0x00, 0x00];
507        let pairs: Vec<(&str, f64)> = fast.decode(256, &payload, false).unwrap().collect();
508        assert_eq!(pairs, vec![("RPM", 2000.0), ("Temp", 50.0)]);
509
510        // Unknown ID and too-short payload both yield None.
511        assert!(fast.decode(512, &payload, false).is_none());
512        assert!(fast.decode(256, &[0u8; 1], false).is_none());
513    }
514
515    #[test]
516    fn test_fast_dbc_decode_named_extended() {
517        // 0x80000100 = extended flag | ID 256
518        let dbc = Dbc::parse(
519            r#"VERSION "1.0"
520
521BU_: ECM
522
523BO_ 2147483904 Engine : 8 ECM
524 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm" *
525"#,
526        )
527        .unwrap();
528
529        let fast = FastDbc::new(dbc);
530
531        let payload = [0x40, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
532        let pairs: Vec<(&str, f64)> = fast.decode(256, &payload, true).unwrap().collect();
533        assert_eq!(pairs, vec![("RPM", 2000.0)]);
534
535        // The bare 29-bit ID is not a standard ID.
536        assert!(fast.decode(256, &payload, false).is_none());
537    }
538
539    #[test]
540    fn test_fast_dbc_unsigned_64bit_msb_not_negative() {
541        // A 64-bit unsigned value with the MSB set must not be treated as a
542        // negative i64 when computing the physical value. Mirrors
543        // `Signal::decode_raw` (test_decode_unsigned_64bit_physical_value);
544        // regression test for the fast path funnelling raw values through i64.
545        let dbc = Dbc::parse(
546            r#"VERSION "1.0"
547
548BU_: ECM
549
550BO_ 256 Wide : 8 ECM
551 SG_ Serial : 0|64@1+ (1,0) [0|1] "" *
552
553BO_ 257 WideSigned : 8 ECM
554 SG_ Signed : 0|64@1- (1,0) [-1|1] "" *
555"#,
556        )
557        .unwrap();
558
559        let fast = FastDbc::new(dbc);
560        let mut values = vec![0.0f64; fast.max_signals()];
561
562        // MSB set: unsigned must stay in the u64 domain, signed must wrap.
563        let payload = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80];
564        assert_eq!(fast.decode_into(256, &payload, &mut values), Some(1));
565        assert_eq!(values[0], 9223372036854775808.0);
566        assert_eq!(fast.decode_into(257, &payload, &mut values), Some(1));
567        assert_eq!(values[0], -9223372036854775808.0);
568
569        // All bits set: unsigned is u64::MAX, signed is -1.
570        let payload = [0xFF; 8];
571        assert_eq!(fast.decode_into(256, &payload, &mut values), Some(1));
572        assert_eq!(values[0], u64::MAX as f64);
573        assert_eq!(fast.decode_into(257, &payload, &mut values), Some(1));
574        assert_eq!(values[0], -1.0);
575    }
576
577    #[test]
578    fn test_fast_dbc_decode_into() {
579        let dbc = Dbc::parse(
580            r#"VERSION "1.0"
581
582BU_: ECM
583
584BO_ 256 Engine : 8 ECM
585 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm" *
586 SG_ Temp : 16|8@1- (1,-40) [-40|215] "C" *
587"#,
588        )
589        .unwrap();
590
591        let fast = FastDbc::new(dbc);
592
593        // RPM = 2000 (raw 8000), Temp = 50°C (raw 90)
594        let payload = [0x40, 0x1F, 0x5A, 0x00, 0x00, 0x00, 0x00, 0x00];
595        let mut values = vec![0.0f64; fast.max_signals()];
596
597        let count = fast.decode_into(256, &payload, &mut values).unwrap();
598
599        assert_eq!(count, 2);
600        assert_eq!(values[0], 2000.0);
601        assert_eq!(values[1], 50.0);
602    }
603
604    #[test]
605    fn test_fast_dbc_identity_transform() {
606        let dbc = Dbc::parse(
607            r#"VERSION "1.0"
608
609BU_: ECM
610
611BO_ 256 Engine : 8 ECM
612 SG_ RawValue : 0|16@1+ (1,0) [0|65535] "" *
613"#,
614        )
615        .unwrap();
616
617        let fast = FastDbc::new(dbc);
618
619        // Raw value 12345
620        let payload = [0x39, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
621        let mut values = [0.0f64; 1];
622
623        fast.decode_into(256, &payload, &mut values);
624
625        assert_eq!(values[0], 12345.0);
626    }
627
628    #[test]
629    fn test_fast_dbc_message_not_found() {
630        let dbc = Dbc::parse(
631            r#"VERSION "1.0"
632
633BU_: ECM
634
635BO_ 256 Engine : 8 ECM
636 SG_ RPM : 0|16@1+ (1,0) [0|8000] "rpm" *
637"#,
638        )
639        .unwrap();
640
641        let fast = FastDbc::new(dbc);
642        let payload = [0x00; 8];
643        let mut values = [0.0f64; 8];
644
645        assert!(fast.decode_into(512, &payload, &mut values).is_none());
646    }
647
648    #[test]
649    fn test_fast_dbc_extended_id() {
650        let dbc = Dbc::parse(
651            r#"VERSION "1.0"
652
653BU_: ECM
654
655BO_ 2147484672 ExtendedMsg : 8 ECM
656 SG_ Speed : 0|16@1+ (0.1,0) [0|6553.5] "km/h" *
657"#,
658        )
659        .unwrap();
660        // 2147484672 = 0x80000400 = extended ID 0x400
661
662        let fast = FastDbc::new(dbc);
663
664        // Should NOT find by standard ID
665        assert!(!fast.contains(0x400));
666        assert!(fast.get(0x400).is_none());
667
668        // Should find by extended ID
669        assert!(fast.contains_extended(0x400));
670        let msg = fast.get_extended(0x400).unwrap();
671        assert_eq!(msg.name(), "ExtendedMsg");
672
673        // Decode
674        let payload = [0xE8, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
675        let mut values = [0.0f64; 8];
676
677        let count = fast.decode_extended_into(0x400, &payload, &mut values).unwrap();
678        assert_eq!(count, 1);
679        assert_eq!(values[0], 100.0); // 1000 * 0.1
680    }
681
682    #[test]
683    fn test_fast_dbc_multiple_messages() {
684        let dbc = Dbc::parse(
685            r#"VERSION "1.0"
686
687BU_: ECM
688
689BO_ 256 Msg1 : 8 ECM
690 SG_ Sig1 : 0|8@1+ (1,0) [0|255] "" *
691 SG_ Sig2 : 8|8@1+ (1,0) [0|255] "" *
692
693BO_ 512 Msg2 : 8 ECM
694 SG_ SigA : 0|16@1+ (1,0) [0|65535] "" *
695
696BO_ 768 Msg3 : 8 ECM
697 SG_ SigX : 0|8@1+ (1,0) [0|255] "" *
698 SG_ SigY : 8|8@1+ (1,0) [0|255] "" *
699 SG_ SigZ : 16|8@1+ (1,0) [0|255] "" *
700"#,
701        )
702        .unwrap();
703
704        let fast = FastDbc::new(dbc);
705
706        assert_eq!(fast.message_count(), 3);
707        assert_eq!(fast.max_signals(), 3); // Msg3 has most
708        assert_eq!(fast.total_signals(), 6);
709
710        assert!(fast.contains(256));
711        assert!(fast.contains(512));
712        assert!(fast.contains(768));
713    }
714
715    #[test]
716    fn test_fast_dbc_large_id() {
717        // Test ID >= 2048 (uses hash map)
718        let dbc = Dbc::parse(
719            r#"VERSION "1.0"
720
721BU_: ECM
722
723BO_ 3000 LargeId : 8 ECM
724 SG_ Value : 0|16@1+ (1,0) [0|65535] "" *
725"#,
726        )
727        .unwrap();
728
729        let fast = FastDbc::new(dbc);
730
731        assert!(fast.contains(3000));
732        assert!(!fast.contains(256));
733
734        let payload = [0x39, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
735        let mut values = [0.0f64; 1];
736
737        let count = fast.decode_into(3000, &payload, &mut values).unwrap();
738        assert_eq!(count, 1);
739        assert_eq!(values[0], 12345.0);
740    }
741
742    #[test]
743    fn test_fast_dbc_from_trait() {
744        let dbc = Dbc::parse(
745            r#"VERSION "1.0"
746
747BU_: ECM
748
749BO_ 256 Engine : 8 ECM
750"#,
751        )
752        .unwrap();
753
754        let fast: FastDbc = dbc.into();
755        assert_eq!(fast.message_count(), 1);
756    }
757
758    #[test]
759    fn test_fast_dbc_into_dbc() {
760        let dbc = Dbc::parse(
761            r#"VERSION "1.0"
762
763BU_: ECM
764
765BO_ 256 Engine : 8 ECM
766"#,
767        )
768        .unwrap();
769
770        let fast = FastDbc::new(dbc);
771        let dbc_back = fast.into_dbc();
772
773        assert_eq!(dbc_back.messages().len(), 1);
774    }
775
776    #[test]
777    fn test_fast_dbc_ids_iterator() {
778        let dbc = Dbc::parse(
779            r#"VERSION "1.0"
780
781BU_: ECM
782
783BO_ 100 Msg1 : 8 ECM
784BO_ 200 Msg2 : 8 ECM
785BO_ 3000 LargeId : 8 ECM
786"#,
787        )
788        .unwrap();
789
790        let fast = FastDbc::new(dbc);
791        let ids: Vec<u32> = fast.ids().collect();
792
793        assert_eq!(ids.len(), 3);
794        assert!(ids.contains(&100));
795        assert!(ids.contains(&200));
796        assert!(ids.contains(&3000));
797    }
798
799    #[test]
800    fn test_fast_dbc_decode_raw_into() {
801        let dbc = Dbc::parse(
802            r#"VERSION "1.0"
803
804BU_: ECM
805
806BO_ 256 Engine : 8 ECM
807 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm" *
808"#,
809        )
810        .unwrap();
811
812        let fast = FastDbc::new(dbc);
813
814        let payload = [0x40, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
815        let mut raw_values = [0i64; 8];
816
817        let count = fast.decode_raw_into(256, &payload, &mut raw_values).unwrap();
818
819        assert_eq!(count, 1);
820        assert_eq!(raw_values[0], 8000); // Raw before factor
821    }
822}