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}