Skip to main content

agent_can/can/
dbc.rs

1use crate::frame::{CAN_FLAG_EXTENDED, CAN_FLAG_FD, CanFrame};
2use crate::protocol::{
3    DbcSpec, DecodedSignalValue, MessagePayload, SchemaMessage, SchemaSignal, Selector,
4};
5use can_dbc::{ByteOrder, Dbc, MessageId, MultiplexIndicator, ValueType};
6use std::collections::{BTreeMap, HashMap};
7use std::path::Path;
8
9#[derive(Debug, Clone)]
10pub struct DbcRegistry {
11    loaded: Vec<DbcSpec>,
12    messages_by_name: BTreeMap<String, DbcMessageDef>,
13    messages_by_frame: HashMap<FrameIdentity, Vec<String>>,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub struct FrameIdentity {
18    pub arb_id: u32,
19    pub extended: bool,
20}
21
22#[derive(Debug, Clone)]
23pub struct DbcMessageDef {
24    pub alias: String,
25    pub name: String,
26    pub qualified_name: String,
27    pub arb_id: u32,
28    pub extended: bool,
29    pub len: u8,
30    pub signals: Vec<DbcSignalDef>,
31}
32
33#[derive(Debug, Clone)]
34pub struct DbcSignalDef {
35    pub name: String,
36    pub start_bit: u64,
37    pub bit_len: u64,
38    pub byte_order: ByteOrder,
39    pub value_type: ValueType,
40    pub factor: f64,
41    pub offset: f64,
42    pub min: Option<f64>,
43    pub max: Option<f64>,
44    pub unit: Option<String>,
45}
46
47#[derive(Debug, Clone)]
48pub struct DecodedSemanticMessage {
49    pub qualified_name: String,
50    pub arb_id: u32,
51    pub extended: bool,
52    pub fd: bool,
53    pub len: u8,
54    pub signals: Vec<DecodedSignalValue>,
55}
56
57impl DbcRegistry {
58    pub fn empty() -> Self {
59        Self {
60            loaded: Vec::new(),
61            messages_by_name: BTreeMap::new(),
62            messages_by_frame: HashMap::new(),
63        }
64    }
65
66    pub fn load(specs: &[DbcSpec]) -> Result<Self, String> {
67        let mut registry = Self::empty();
68        let mut seen_aliases = BTreeMap::new();
69
70        for spec in specs {
71            let alias = spec.alias.trim();
72            if alias.is_empty() {
73                return Err("DBC alias must not be empty".to_string());
74            }
75            if seen_aliases
76                .insert(alias.to_string(), spec.path.clone())
77                .is_some()
78            {
79                return Err(format!("duplicate DBC alias '{alias}'"));
80            }
81
82            let normalized_path = canonicalize_dbc_path(&spec.path)?;
83            let content = std::fs::read_to_string(Path::new(&normalized_path))
84                .map_err(|err| format!("failed to read DBC '{}': {err}", normalized_path))?;
85            let dbc = Dbc::try_from(content.as_str())
86                .map_err(|err| format!("failed to parse DBC '{}': {err}", normalized_path))?;
87
88            for message in dbc.messages {
89                let (arb_id, extended) = match message.id {
90                    MessageId::Standard(id) => (u32::from(id), false),
91                    MessageId::Extended(id) => (id, true),
92                };
93                let len = u8::try_from(message.size).map_err(|_| {
94                    format!(
95                        "DBC message '{}.{}' has invalid size {}; max supported is 255 bytes",
96                        alias, message.name, message.size
97                    )
98                })?;
99                if len > 64 {
100                    return Err(format!(
101                        "DBC message '{}.{}' size {} exceeds CAN FD max payload (64)",
102                        alias, message.name, len
103                    ));
104                }
105
106                let mut signals = Vec::with_capacity(message.signals.len());
107                for signal in message.signals {
108                    if signal.multiplexer_indicator != MultiplexIndicator::Plain {
109                        return Err(format!(
110                            "DBC signal '{}.{}' uses multiplexing, which is unsupported in this version",
111                            message.name, signal.name
112                        ));
113                    }
114                    let (min, max) = normalize_range_bounds(signal.min, signal.max);
115                    signals.push(DbcSignalDef {
116                        name: signal.name,
117                        start_bit: signal.start_bit,
118                        bit_len: signal.size,
119                        byte_order: signal.byte_order,
120                        value_type: signal.value_type,
121                        factor: signal.factor,
122                        offset: signal.offset,
123                        min,
124                        max,
125                        unit: if signal.unit.is_empty() {
126                            None
127                        } else {
128                            Some(signal.unit)
129                        },
130                    });
131                }
132
133                let qualified_name = format!("{alias}.{}", message.name);
134                let frame = FrameIdentity { arb_id, extended };
135                registry
136                    .messages_by_frame
137                    .entry(frame)
138                    .or_default()
139                    .push(qualified_name.clone());
140                registry.messages_by_name.insert(
141                    qualified_name.clone(),
142                    DbcMessageDef {
143                        alias: alias.to_string(),
144                        name: message.name,
145                        qualified_name,
146                        arb_id,
147                        extended,
148                        len,
149                        signals,
150                    },
151                );
152            }
153
154            registry.loaded.push(DbcSpec {
155                alias: alias.to_string(),
156                path: normalized_path,
157            });
158        }
159
160        Ok(registry)
161    }
162
163    pub fn loaded(&self) -> &[DbcSpec] {
164        &self.loaded
165    }
166
167    pub fn is_empty(&self) -> bool {
168        self.messages_by_name.is_empty()
169    }
170
171    pub fn schema(&self, filter: Option<&Selector>) -> Vec<SchemaMessage> {
172        let mut out = self
173            .messages_by_name
174            .values()
175            .filter(|message| selector_matches_message(filter, message))
176            .map(|message| SchemaMessage {
177                qualified_name: message.qualified_name.clone(),
178                alias: message.alias.clone(),
179                message: message.name.clone(),
180                arb_id: message.arb_id,
181                extended: message.extended,
182                fd: message.len > 8,
183                len: message.len,
184                signals: message
185                    .signals
186                    .iter()
187                    .map(|signal| SchemaSignal {
188                        name: signal.name.clone(),
189                        value_type: match signal.value_type {
190                            ValueType::Signed => "signed".to_string(),
191                            ValueType::Unsigned => "unsigned".to_string(),
192                        },
193                        unit: signal.unit.clone(),
194                        min: signal.min,
195                        max: signal.max,
196                        factor: signal.factor,
197                        offset: signal.offset,
198                        start_bit: signal.start_bit,
199                        bit_len: signal.bit_len,
200                    })
201                    .collect(),
202            })
203            .collect::<Vec<_>>();
204        out.sort_by(|lhs, rhs| lhs.qualified_name.cmp(&rhs.qualified_name));
205        out
206    }
207
208    pub fn resolve_selector(&self, selector: &Selector) -> Result<&DbcMessageDef, String> {
209        match selector {
210            Selector::ArbId(_) => {
211                Err("raw selectors do not resolve to semantic messages".to_string())
212            }
213            Selector::SemanticPattern(pattern) => {
214                let matches = self
215                    .messages_by_name
216                    .values()
217                    .filter(|message| {
218                        Selector::SemanticPattern(pattern.clone())
219                            .matches_qualified_name(&message.qualified_name)
220                    })
221                    .collect::<Vec<_>>();
222                match matches.as_slice() {
223                    [] => Err(format!(
224                        "semantic selector '{pattern}' matched no DBC messages"
225                    )),
226                    [message] => Ok(message),
227                    _ => Err(format!(
228                        "semantic selector '{pattern}' matched multiple DBC messages"
229                    )),
230                }
231            }
232        }
233    }
234
235    pub fn matches_for_frame(&self, frame: &CanFrame) -> Vec<&DbcMessageDef> {
236        let frame_identity = FrameIdentity::from_frame(frame);
237        self.messages_by_frame
238            .get(&frame_identity)
239            .into_iter()
240            .flat_map(|names| names.iter())
241            .filter_map(|name| self.messages_by_name.get(name))
242            .collect()
243    }
244
245    pub fn decode_selected(
246        &self,
247        qualified_name: &str,
248        frame: &CanFrame,
249    ) -> Result<DecodedSemanticMessage, String> {
250        let message = self
251            .messages_by_name
252            .get(qualified_name)
253            .ok_or_else(|| format!("DBC message '{qualified_name}' not found"))?;
254        let frame_identity = FrameIdentity::from_frame(frame);
255        if frame_identity != FrameIdentity::from_message(message) {
256            return Err(format!(
257                "frame 0x{:X} does not match semantic definition '{}'",
258                frame.arb_id, qualified_name
259            ));
260        }
261        let signals = message
262            .signals
263            .iter()
264            .map(|signal| {
265                Ok(DecodedSignalValue {
266                    name: signal.name.clone(),
267                    value: decode_signal(frame, signal)?,
268                    unit: signal.unit.clone(),
269                })
270            })
271            .collect::<Result<Vec<_>, String>>()?;
272        Ok(DecodedSemanticMessage {
273            qualified_name: message.qualified_name.clone(),
274            arb_id: message.arb_id,
275            extended: message.extended,
276            fd: message.len > 8 || (frame.flags & CAN_FLAG_FD) != 0,
277            len: frame.len,
278            signals,
279        })
280    }
281
282    pub fn encode_payload(
283        &self,
284        selector: &Selector,
285        payload: &MessagePayload,
286    ) -> Result<(String, CanFrame), String> {
287        match (selector, payload) {
288            (Selector::SemanticPattern(_), MessagePayload::Signals(values)) => {
289                let message = self.resolve_selector(selector)?;
290                let frame = encode_message(message, values)?;
291                Ok((message.qualified_name.clone(), frame))
292            }
293            (Selector::SemanticPattern(pattern), MessagePayload::RawHex(_)) => Err(format!(
294                "semantic target '{pattern}' requires a signal map, not raw hex"
295            )),
296            (Selector::ArbId(arb_id), MessagePayload::RawHex(raw)) => {
297                let payload = crate::can::parse_data_hex(raw)?;
298                Ok((
299                    format!("0x{arb_id:X}"),
300                    crate::can::frame_from_payload(*arb_id, &payload),
301                ))
302            }
303            (Selector::ArbId(arb_id), MessagePayload::Signals(_)) => Err(format!(
304                "raw target '0x{arb_id:X}' requires raw payload bytes"
305            )),
306        }
307    }
308}
309
310impl FrameIdentity {
311    pub fn from_frame(frame: &CanFrame) -> Self {
312        Self {
313            arb_id: frame.arb_id,
314            extended: (frame.flags & CAN_FLAG_EXTENDED) != 0,
315        }
316    }
317
318    pub fn from_message(message: &DbcMessageDef) -> Self {
319        Self {
320            arb_id: message.arb_id,
321            extended: message.extended,
322        }
323    }
324}
325
326fn selector_matches_message(filter: Option<&Selector>, message: &DbcMessageDef) -> bool {
327    match filter {
328        None => true,
329        Some(Selector::ArbId(arb_id)) => message.arb_id == *arb_id,
330        Some(selector) => selector.matches_qualified_name(&message.qualified_name),
331    }
332}
333
334fn encode_message(
335    message: &DbcMessageDef,
336    signal_values: &BTreeMap<String, f64>,
337) -> Result<CanFrame, String> {
338    for signal in &message.signals {
339        if !signal_values.contains_key(&signal.name) {
340            return Err(format!(
341                "semantic send for '{}' is missing required signal '{}'",
342                message.qualified_name, signal.name
343            ));
344        }
345    }
346    for name in signal_values.keys() {
347        if !message.signals.iter().any(|signal| signal.name == *name) {
348            return Err(format!(
349                "signal '{name}' is not part of DBC message '{}'",
350                message.qualified_name
351            ));
352        }
353    }
354
355    let mut frame = CanFrame {
356        arb_id: message.arb_id,
357        len: message.len,
358        flags: 0,
359        data: [0; 64],
360    };
361    if message.extended {
362        frame.flags |= CAN_FLAG_EXTENDED;
363    }
364    if message.len > 8 {
365        frame.flags |= CAN_FLAG_FD;
366    }
367
368    for signal in &message.signals {
369        let value = *signal_values
370            .get(&signal.name)
371            .expect("checked required signals");
372        encode_signal(&mut frame, signal, value)?;
373    }
374    Ok(frame)
375}
376
377fn decode_signal(frame: &CanFrame, signal: &DbcSignalDef) -> Result<f64, String> {
378    let raw = extract_raw(frame, signal)?;
379    let numeric = match signal.value_type {
380        ValueType::Signed => signed_from_raw(raw, signal.bit_len) as f64,
381        ValueType::Unsigned => raw as f64,
382    };
383    Ok(numeric * signal.factor + signal.offset)
384}
385
386fn encode_signal(frame: &mut CanFrame, signal: &DbcSignalDef, value: f64) -> Result<(), String> {
387    if !value.is_finite() {
388        return Err(format!(
389            "invalid value for CAN signal '{}': {value}",
390            signal.name
391        ));
392    }
393    if let Some(min) = signal.min
394        && value < min
395    {
396        return Err(format!(
397            "value {value} is below DBC min {min} for CAN signal '{}'",
398            signal.name
399        ));
400    }
401    if let Some(max) = signal.max
402        && value > max
403    {
404        return Err(format!(
405            "value {value} is above DBC max {max} for CAN signal '{}'",
406            signal.name
407        ));
408    }
409    if signal.factor == 0.0 {
410        return Err(format!(
411            "DBC signal '{}' has zero factor, cannot encode",
412            signal.name
413        ));
414    }
415
416    let raw_float = (value - signal.offset) / signal.factor;
417    let raw_i64 = raw_float.round() as i64;
418    let raw_u64 = match signal.value_type {
419        ValueType::Signed => {
420            let min = -(1_i128 << (signal.bit_len - 1));
421            let max = (1_i128 << (signal.bit_len - 1)) - 1;
422            let val = i128::from(raw_i64);
423            if val < min || val > max {
424                return Err(format!(
425                    "encoded raw value {raw_i64} exceeds signed {}-bit range for signal '{}'",
426                    signal.bit_len, signal.name
427                ));
428            }
429            let mask = if signal.bit_len == 64 {
430                u64::MAX
431            } else {
432                (1_u64 << signal.bit_len) - 1
433            };
434            (raw_i64 as i128 as u64) & mask
435        }
436        ValueType::Unsigned => {
437            if raw_i64 < 0 {
438                return Err(format!(
439                    "encoded raw value {raw_i64} is negative for unsigned signal '{}'",
440                    signal.name
441                ));
442            }
443            let max = if signal.bit_len == 64 {
444                u64::MAX
445            } else {
446                (1_u64 << signal.bit_len) - 1
447            };
448            let raw = raw_i64 as u64;
449            if raw > max {
450                return Err(format!(
451                    "encoded raw value {raw} exceeds unsigned {}-bit range for signal '{}'",
452                    signal.bit_len, signal.name
453                ));
454            }
455            raw
456        }
457    };
458    insert_raw(frame, signal, raw_u64)
459}
460
461fn extract_raw(frame: &CanFrame, signal: &DbcSignalDef) -> Result<u64, String> {
462    if signal.bit_len > 64 {
463        return Err(format!(
464            "signal '{}' size {} exceeds 64 bits",
465            signal.name, signal.bit_len
466        ));
467    }
468    match signal.byte_order {
469        ByteOrder::LittleEndian => {
470            let mut raw = 0_u64;
471            for idx in 0..signal.bit_len {
472                let frame_bit = signal.start_bit + idx;
473                let bit = get_bit(&frame.data, frame_bit)?;
474                raw |= u64::from(bit) << idx;
475            }
476            Ok(raw)
477        }
478        ByteOrder::BigEndian => {
479            let mut raw = 0_u64;
480            let mut bit_pos = signal.start_bit as i64;
481            for _ in 0..signal.bit_len {
482                let bit = get_bit(&frame.data, bit_pos as u64)?;
483                raw = (raw << 1) | u64::from(bit);
484                bit_pos = next_motorola_bit(bit_pos);
485            }
486            Ok(raw)
487        }
488    }
489}
490
491fn insert_raw(frame: &mut CanFrame, signal: &DbcSignalDef, raw: u64) -> Result<(), String> {
492    match signal.byte_order {
493        ByteOrder::LittleEndian => {
494            for idx in 0..signal.bit_len {
495                let frame_bit = signal.start_bit + idx;
496                let bit = ((raw >> idx) & 1) as u8;
497                set_bit(&mut frame.data, frame_bit, bit)?;
498            }
499        }
500        ByteOrder::BigEndian => {
501            let mut bit_pos = signal.start_bit as i64;
502            for idx in 0..signal.bit_len {
503                let shift = signal.bit_len - 1 - idx;
504                let bit = ((raw >> shift) & 1) as u8;
505                set_bit(&mut frame.data, bit_pos as u64, bit)?;
506                bit_pos = next_motorola_bit(bit_pos);
507            }
508        }
509    }
510    Ok(())
511}
512
513fn signed_from_raw(raw: u64, size: u64) -> i64 {
514    if size == 64 {
515        return raw as i64;
516    }
517    let sign_bit = 1_u64 << (size - 1);
518    if raw & sign_bit == 0 {
519        raw as i64
520    } else {
521        let mask = (1_u64 << size) - 1;
522        let twos_complement = (!raw + 1) & mask;
523        -(twos_complement as i64)
524    }
525}
526
527fn get_bit(data: &[u8; 64], bit_index: u64) -> Result<u8, String> {
528    let byte_index =
529        usize::try_from(bit_index / 8).map_err(|_| "bit index overflow".to_string())?;
530    if byte_index >= data.len() {
531        return Err(format!("bit index {bit_index} exceeds CAN payload size"));
532    }
533    let bit_in_byte = (bit_index % 8) as u8;
534    Ok((data[byte_index] >> bit_in_byte) & 1)
535}
536
537fn set_bit(data: &mut [u8; 64], bit_index: u64, bit: u8) -> Result<(), String> {
538    let byte_index =
539        usize::try_from(bit_index / 8).map_err(|_| "bit index overflow".to_string())?;
540    if byte_index >= data.len() {
541        return Err(format!("bit index {bit_index} exceeds CAN payload size"));
542    }
543    let bit_in_byte = (bit_index % 8) as u8;
544    let mask = 1_u8 << bit_in_byte;
545    if bit == 0 {
546        data[byte_index] &= !mask;
547    } else {
548        data[byte_index] |= mask;
549    }
550    Ok(())
551}
552
553fn next_motorola_bit(bit_pos: i64) -> i64 {
554    let bit_in_byte = bit_pos % 8;
555    if bit_in_byte == 0 {
556        bit_pos + 15
557    } else {
558        bit_pos - 1
559    }
560}
561
562fn canonicalize_dbc_path(raw_path: &str) -> Result<String, String> {
563    let canonical = std::fs::canonicalize(raw_path).map_err(|err| {
564        format!("failed to resolve DBC path '{raw_path}' to an absolute path: {err}")
565    })?;
566    Ok(canonical.to_string_lossy().into_owned())
567}
568
569fn normalize_range_bounds(min: f64, max: f64) -> (Option<f64>, Option<f64>) {
570    if min == 0.0 && max == 0.0 {
571        (None, None)
572    } else {
573        (Some(min), Some(max))
574    }
575}
576
577#[cfg(test)]
578mod tests {
579    use super::DbcRegistry;
580    use crate::protocol::{DbcSpec, MessagePayload, Selector};
581    use std::collections::BTreeMap;
582    use std::io::Write;
583
584    fn write_temp_dbc(contents: &str) -> tempfile::NamedTempFile {
585        let mut file = tempfile::NamedTempFile::new().expect("tempfile");
586        write!(file, "{contents}").expect("write dbc");
587        file
588    }
589
590    #[test]
591    fn overlapping_ids_are_allowed() {
592        let dbc_a = write_temp_dbc(
593            "VERSION \"\"\nBO_ 291 Foo: 8 ECU\n SG_ enable : 0|8@1+ (1,0) [0|1] \"\" ECU\n",
594        );
595        let dbc_b = write_temp_dbc(
596            "VERSION \"\"\nBO_ 291 Bar: 8 ECU\n SG_ enable : 0|8@1+ (1,0) [0|1] \"\" ECU\n",
597        );
598        let registry = DbcRegistry::load(&[
599            DbcSpec {
600                alias: "a".to_string(),
601                path: dbc_a.path().display().to_string(),
602            },
603            DbcSpec {
604                alias: "b".to_string(),
605                path: dbc_b.path().display().to_string(),
606            },
607        ])
608        .expect("load");
609        assert_eq!(registry.schema(None).len(), 2);
610    }
611
612    #[test]
613    fn semantic_encode_requires_complete_message() {
614        let dbc = write_temp_dbc(
615            "VERSION \"\"\nBO_ 291 Foo: 8 ECU\n SG_ enable : 0|8@1+ (1,0) [0|1] \"\" ECU\n SG_ torque : 8|8@1+ (1,0) [0|255] \"\" ECU\n",
616        );
617        let registry = DbcRegistry::load(&[DbcSpec {
618            alias: "a".to_string(),
619            path: dbc.path().display().to_string(),
620        }])
621        .expect("load");
622        let mut signals = BTreeMap::new();
623        signals.insert("enable".to_string(), 1.0);
624        let err = registry
625            .encode_payload(
626                &Selector::SemanticPattern("a.Foo".to_string()),
627                &MessagePayload::Signals(signals),
628            )
629            .expect_err("missing signal must fail");
630        assert!(err.contains("missing required signal 'torque'"));
631    }
632
633    #[test]
634    fn zero_zero_placeholder_range_is_omitted_but_zero_edges_are_preserved() {
635        let dbc = write_temp_dbc(
636            "VERSION \"\"\nBO_ 291 Foo: 8 ECU\n SG_ lower_zero : 0|8@1+ (1,0) [0|10] \"\" ECU\n SG_ upper_zero : 8|8@1- (1,0) [-10|0] \"\" ECU\n SG_ placeholder : 16|8@1+ (1,0) [0|0] \"\" ECU\n",
637        );
638        let registry = DbcRegistry::load(&[DbcSpec {
639            alias: "a".to_string(),
640            path: dbc.path().display().to_string(),
641        }])
642        .expect("load");
643
644        let schema = registry.schema(Some(&Selector::SemanticPattern("a.Foo".to_string())));
645        let signals = &schema[0].signals;
646        assert_eq!(signals[0].name, "lower_zero");
647        assert_eq!(signals[0].min, Some(0.0));
648        assert_eq!(signals[0].max, Some(10.0));
649        assert_eq!(signals[1].name, "upper_zero");
650        assert_eq!(signals[1].min, Some(-10.0));
651        assert_eq!(signals[1].max, Some(0.0));
652        assert_eq!(signals[2].name, "placeholder");
653        assert_eq!(signals[2].min, None);
654        assert_eq!(signals[2].max, None);
655
656        let valid = BTreeMap::from([
657            ("lower_zero".to_string(), 5.0),
658            ("upper_zero".to_string(), -5.0),
659            ("placeholder".to_string(), 42.0),
660        ]);
661        registry
662            .encode_payload(
663                &Selector::SemanticPattern("a.Foo".to_string()),
664                &MessagePayload::Signals(valid),
665            )
666            .expect("placeholder range should not clamp to zero");
667
668        let lower_zero_err = registry
669            .encode_payload(
670                &Selector::SemanticPattern("a.Foo".to_string()),
671                &MessagePayload::Signals(BTreeMap::from([
672                    ("lower_zero".to_string(), -1.0),
673                    ("upper_zero".to_string(), -5.0),
674                    ("placeholder".to_string(), 42.0),
675                ])),
676            )
677            .expect_err("lower zero bound must be enforced");
678        assert!(lower_zero_err.contains("below DBC min 0"));
679
680        let upper_zero_err = registry
681            .encode_payload(
682                &Selector::SemanticPattern("a.Foo".to_string()),
683                &MessagePayload::Signals(BTreeMap::from([
684                    ("lower_zero".to_string(), 5.0),
685                    ("upper_zero".to_string(), 1.0),
686                    ("placeholder".to_string(), 42.0),
687                ])),
688            )
689            .expect_err("upper zero bound must be enforced");
690        assert!(upper_zero_err.contains("above DBC max 0"));
691    }
692
693    #[test]
694    fn semantic_encode_decode_roundtrip_preserves_signal_values() {
695        let dbc = write_temp_dbc(
696            "VERSION \"\"\nBO_ 291 Foo: 8 ECU\n SG_ little_u : 0|8@1+ (1,0) [0|255] \"\" ECU\n SG_ little_s : 8|8@1- (1,0) [-128|127] \"\" ECU\n SG_ big_u : 23|8@0+ (1,0) [0|255] \"\" ECU\n",
697        );
698        let registry = DbcRegistry::load(&[DbcSpec {
699            alias: "a".to_string(),
700            path: dbc.path().display().to_string(),
701        }])
702        .expect("load");
703
704        let payload = BTreeMap::from([
705            ("little_u".to_string(), 42.0),
706            ("little_s".to_string(), -7.0),
707            ("big_u".to_string(), 171.0),
708        ]);
709        let (_, frame) = registry
710            .encode_payload(
711                &Selector::SemanticPattern("a.Foo".to_string()),
712                &MessagePayload::Signals(payload),
713            )
714            .expect("encode");
715
716        assert_eq!(frame.data[0], 42);
717        assert_eq!(frame.data[1], 249);
718        assert_eq!(frame.data[2], 171);
719
720        let decoded = registry.decode_selected("a.Foo", &frame).expect("decode");
721        let values = decoded
722            .signals
723            .into_iter()
724            .map(|signal| (signal.name, signal.value))
725            .collect::<BTreeMap<_, _>>();
726        assert_eq!(values.get("little_u"), Some(&42.0));
727        assert_eq!(values.get("little_s"), Some(&-7.0));
728        assert_eq!(values.get("big_u"), Some(&171.0));
729    }
730
731    #[test]
732    fn load_fails_if_any_requested_dbc_is_invalid() {
733        let dbc = write_temp_dbc(
734            "VERSION \"\"\nBO_ 291 Foo: 8 ECU\n SG_ enable : 0|8@1+ (1,0) [0|1] \"\" ECU\n",
735        );
736        let err = DbcRegistry::load(&[
737            DbcSpec {
738                alias: "valid".to_string(),
739                path: dbc.path().display().to_string(),
740            },
741            DbcSpec {
742                alias: "missing".to_string(),
743                path: "/definitely/missing/file.dbc".to_string(),
744            },
745        ])
746        .expect_err("mixed valid and invalid DBC set must fail atomically");
747        assert!(err.contains("failed to resolve DBC path"));
748    }
749}