Skip to main content

datui_lib/formats/
dbc.rs

1//! DBC files: what the frames of a CAN bus mean.
2//!
3//! A DBC file names each message by its id (`BO_`) and the signals packed in its data
4//! (`SG_`): start bit, length, byte order, sign, factor and offset, unit, and for a
5//! multiplexed message which value of the multiplexer selects each signal. `VAL_`
6//! names a signal's values, `SIG_VALTYPE_` marks a float signal, and `CM_` comments
7//! on messages and signals. Parsed by hand; every count and length is bounded.
8//!
9//! DBC files are found on the format search path, as FIX dictionaries are: a `.dbc`
10//! file applies to every interface, and a TOML file of `kind = "dbc"` names a DBC file
11//! (`file`) and the interface it applies to (`[match] interface = "can1"`). `--dict FILE`
12//! is read over them. A later file's message of an id takes the place of an earlier's.
13
14use std::collections::HashMap;
15use std::path::{Path, PathBuf};
16use std::sync::Arc;
17
18use crate::formats::SpecError;
19
20/// The largest DBC file read.
21pub const MAX_FILE: u64 = 64 << 20;
22/// Messages in one file, signals in one message, and values one signal names.
23const MAX_MESSAGES: usize = 65_536;
24const MAX_SIGNALS: usize = 4096;
25const MAX_VALUES: usize = 65_536;
26/// The longest statement (`VAL_`, `CM_`) read.
27const MAX_STATEMENT: usize = 1 << 20;
28
29/// A signal's place in its message and what its value means.
30#[derive(Debug, Clone, PartialEq)]
31pub struct Signal {
32    pub name: String,
33    pub start: u32,
34    pub length: u32,
35    /// Motorola (big-endian) bit order; Intel otherwise.
36    pub big_endian: bool,
37    pub signed: bool,
38    pub factor: f64,
39    pub offset: f64,
40    pub unit: String,
41    pub mux: Mux,
42    /// `SIG_VALTYPE_` 1 (a 32-bit float) or 2 (a 64-bit float); 0 for an integer.
43    pub float: u8,
44    /// `VAL_`: names of its values.
45    pub values: Arc<std::collections::BTreeMap<i64, String>>,
46    pub comment: Option<String>,
47}
48
49/// How a signal takes part in multiplexing.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum Mux {
52    Plain,
53    /// The multiplexer: its value selects the multiplexed signals.
54    Multiplexer,
55    /// Present when the multiplexer holds this value.
56    When(u64),
57}
58
59/// One message.
60#[derive(Debug, Clone, PartialEq)]
61pub struct Message {
62    pub id: u32,
63    pub extended: bool,
64    pub name: String,
65    pub dlc: u32,
66    pub signals: Vec<Signal>,
67    pub comment: Option<String>,
68}
69
70/// One DBC file and what it applies to.
71#[derive(Debug, Clone, PartialEq)]
72pub struct Dbc {
73    pub name: String,
74    pub path: Option<PathBuf>,
75    /// The interface it applies to; every one when `None`.
76    pub interface: Option<String>,
77    pub messages: Vec<Message>,
78    /// What was passed over: extended multiplexing, statements that do not parse.
79    pub notes: Vec<String>,
80}
81
82impl Dbc {
83    /// Whether it applies to frames on `interface`.
84    pub fn applies(&self, interface: &str) -> bool {
85        self.interface.as_deref().is_none_or(|i| i == interface)
86    }
87}
88
89fn error(path: Option<&Path>, line: usize, message: impl Into<String>) -> SpecError {
90    SpecError {
91        path: path.map(Path::to_path_buf),
92        line,
93        column: if line > 0 { 1 } else { 0 },
94        message: message.into(),
95    }
96}
97
98/// Whether a TOML file is a DBC reference (`kind = "dbc"`) rather than a format spec.
99pub fn is_dbc_toml(text: &str) -> bool {
100    text.parse::<toml::Table>()
101        .is_ok_and(|t| t.get("kind").and_then(|k| k.as_str()) == Some("dbc"))
102}
103
104/// Read the DBC file `path`, or the DBC a `kind = "dbc"` TOML file names. `Ok(None)` for
105/// a TOML file of another kind.
106pub fn load(path: &Path) -> Result<Option<Dbc>, SpecError> {
107    let read = |path: &Path| -> Result<String, SpecError> {
108        let size = std::fs::metadata(path)
109            .map_err(|e| {
110                error(
111                    Some(path),
112                    0,
113                    format!(
114                        "could not read it. {}",
115                        crate::error_display::user_message_from_io(&e, None)
116                    ),
117                )
118            })?
119            .len();
120        if size > MAX_FILE {
121            return Err(error(
122                Some(path),
123                0,
124                format!(
125                    "{} MiB is past the {} MiB a DBC dictionary may be",
126                    size >> 20,
127                    MAX_FILE >> 20
128                ),
129            ));
130        }
131        let bytes = std::fs::read(path).map_err(|e| {
132            error(
133                Some(path),
134                0,
135                format!(
136                    "could not read it. {}",
137                    crate::error_display::user_message_from_io(&e, None)
138                ),
139            )
140        })?;
141        // DBC files are often Windows-1252: read what is not UTF-8 as Latin-1.
142        Ok(match String::from_utf8(bytes) {
143            Ok(text) => text,
144            Err(e) => e.into_bytes().iter().map(|&b| char::from(b)).collect(),
145        })
146    };
147    let toml = path
148        .extension()
149        .is_some_and(|e| e.eq_ignore_ascii_case("toml"));
150    if !toml {
151        let text = read(path)?;
152        let name = path
153            .file_stem()
154            .map(|s| s.to_string_lossy().into_owned())
155            .unwrap_or_default();
156        return parse(&text, &name, Some(path)).map(Some);
157    }
158    let text = read(path)?;
159    if !is_dbc_toml(&text) {
160        return Ok(None);
161    }
162    let table: toml::Table = text
163        .parse()
164        .map_err(|e| error(Some(path), 0, format!("not TOML: {e}")))?;
165    let file = table.get("file").and_then(|f| f.as_str()).ok_or_else(|| {
166        error(
167            Some(path),
168            0,
169            "kind = \"dbc\" names its .dbc file with file = \"...\"",
170        )
171    })?;
172    let interface = match table.get("match") {
173        None => None,
174        Some(toml::Value::Table(m)) => match m.get("interface").or_else(|| m.get("bus")) {
175            Some(toml::Value::String(s)) => Some(s.clone()),
176            _ => {
177                return Err(error(Some(path), 0, "[match] takes interface = \"can0\""));
178            }
179        },
180        Some(_) => return Err(error(Some(path), 0, "match is a table: [match]")),
181    };
182    let dbc_path = crate::config::expand_path(file);
183    let dbc_path = if dbc_path.is_relative() {
184        path.parent().unwrap_or(Path::new("")).join(dbc_path)
185    } else {
186        dbc_path
187    };
188    let name = table
189        .get("name")
190        .and_then(|n| n.as_str())
191        .map(str::to_string)
192        .unwrap_or_else(|| {
193            path.file_stem()
194                .map(|s| s.to_string_lossy().into_owned())
195                .unwrap_or_default()
196        });
197    let text = read(&dbc_path)?;
198    let mut dbc = parse(&text, &name, Some(&dbc_path))?;
199    dbc.interface = interface;
200    Ok(Some(dbc))
201}
202
203/// A token of a DBC statement.
204#[derive(Debug, Clone, PartialEq)]
205enum Token {
206    Word(String),
207    Str(String),
208    Punct(char),
209}
210
211fn tokens(text: &str) -> Vec<Token> {
212    let mut out = Vec::new();
213    let mut chars = text.char_indices().peekable();
214    while let Some((i, c)) = chars.next() {
215        if c.is_whitespace() {
216            continue;
217        }
218        if c == '"' {
219            let mut s = String::new();
220            let mut escaped = false;
221            for (_, c) in chars.by_ref() {
222                if escaped {
223                    s.push(c);
224                    escaped = false;
225                } else if c == '\\' {
226                    escaped = true;
227                } else if c == '"' {
228                    break;
229                } else if s.len() < 1 << 16 {
230                    s.push(c);
231                }
232            }
233            out.push(Token::Str(s));
234        } else if ":|@(),[];".contains(c) {
235            out.push(Token::Punct(c));
236        } else {
237            let mut end = i + c.len_utf8();
238            while let Some(&(j, d)) = chars.peek() {
239                if d.is_whitespace() || d == '"' || ":|@(),[];".contains(d) {
240                    break;
241                }
242                end = j + d.len_utf8();
243                chars.next();
244            }
245            out.push(Token::Word(text[i..end].to_string()));
246        }
247    }
248    out
249}
250
251/// Keywords that start a statement ending in `;`.
252const STATEMENTS: [&str; 7] = [
253    "VAL_",
254    "CM_",
255    "SIG_VALTYPE_",
256    "SG_MUL_VAL_",
257    "BA_",
258    "BA_DEF_",
259    "BA_DEF_DEF_",
260];
261
262/// Parse the text of a DBC file named `name`.
263pub fn parse(text: &str, name: &str, path: Option<&Path>) -> Result<Dbc, SpecError> {
264    let mut dbc = Dbc {
265        name: name.to_string(),
266        path: path.map(Path::to_path_buf),
267        interface: None,
268        messages: Vec::new(),
269        notes: Vec::new(),
270    };
271    let mut by_id: HashMap<(u32, bool), usize> = HashMap::new();
272    let mut values: Vec<(u32, String, std::collections::BTreeMap<i64, String>)> = Vec::new();
273    let mut floats: Vec<(u32, String, u8)> = Vec::new();
274    let mut comments: Vec<(u32, Option<String>, String)> = Vec::new();
275    let mut extended_mux = false;
276    let mut lines = text.lines().enumerate();
277    while let Some((n, line)) = lines.next() {
278        let trimmed = line.trim_start();
279        let keyword = trimmed.split_whitespace().next().unwrap_or("");
280        match keyword {
281            "BO_" => {
282                let t = tokens(trimmed);
283                // BO_ id name : dlc transmitter
284                let (Some(Token::Word(raw)), Some(Token::Word(msg)), Some(Token::Punct(':'))) =
285                    (t.get(1), t.get(2), t.get(3))
286                else {
287                    return Err(error(path, n + 1, "BO_ is BO_ id name: dlc sender"));
288                };
289                let raw: u32 = raw
290                    .parse()
291                    .map_err(|_| error(path, n + 1, format!("BO_ id {raw} is not a number")))?;
292                let dlc = match t.get(4) {
293                    Some(Token::Word(d)) => d.parse().unwrap_or(8),
294                    _ => 8,
295                };
296                // The pseudo-message that holds signals of no message.
297                if raw == 0xC000_0000 {
298                    continue;
299                }
300                if dbc.messages.len() >= MAX_MESSAGES {
301                    return Err(error(
302                        path,
303                        n + 1,
304                        format!("more than {MAX_MESSAGES} messages"),
305                    ));
306                }
307                let extended = raw & 0x8000_0000 != 0;
308                let id = raw & 0x1FFF_FFFF;
309                by_id.insert((raw, false), dbc.messages.len());
310                dbc.messages.push(Message {
311                    id,
312                    extended,
313                    name: msg.clone(),
314                    dlc,
315                    signals: Vec::new(),
316                    comment: None,
317                });
318            }
319            "SG_" => {
320                let signal = parse_signal(&tokens(trimmed))
321                    .ok_or_else(|| error(path, n + 1, "SG_ is SG_ name [M|mN] : start|length@order+/- (factor,offset) [min|max] \"unit\" receivers"))?;
322                let Some(message) = dbc.messages.last_mut() else {
323                    return Err(error(path, n + 1, "SG_ before any BO_"));
324                };
325                if message.signals.len() >= MAX_SIGNALS {
326                    return Err(error(
327                        path,
328                        n + 1,
329                        format!("more than {MAX_SIGNALS} signals in {}", message.name),
330                    ));
331                }
332                message.signals.push(signal);
333            }
334            k if STATEMENTS.contains(&k) => {
335                // A keyword alone on its line is the list of new symbols (NS_).
336                if trimmed.split_whitespace().nth(1).is_none() {
337                    continue;
338                }
339                let mut statement = trimmed.to_string();
340                while !ends_statement(&statement) {
341                    let Some((_, next)) = lines.next() else {
342                        break;
343                    };
344                    if statement.len() > MAX_STATEMENT {
345                        return Err(error(path, n + 1, format!("a {k} statement does not end")));
346                    }
347                    statement.push('\n');
348                    statement.push_str(next);
349                }
350                let t = tokens(&statement);
351                match k {
352                    "VAL_" => {
353                        if let (Some(Token::Word(id)), Some(Token::Word(sig))) =
354                            (t.get(1), t.get(2))
355                            && let Ok(id) = id.parse::<u32>()
356                        {
357                            let mut map = std::collections::BTreeMap::new();
358                            let mut i = 3;
359                            while let (Some(Token::Word(v)), Some(Token::Str(label))) =
360                                (t.get(i), t.get(i + 1))
361                            {
362                                if let Ok(v) = v
363                                    .parse::<i64>()
364                                    .or_else(|_| v.parse::<f64>().map(|f| f as i64))
365                                {
366                                    map.insert(v, label.clone());
367                                }
368                                if map.len() >= MAX_VALUES {
369                                    break;
370                                }
371                                i += 2;
372                            }
373                            values.push((id, sig.clone(), map));
374                        }
375                    }
376                    "SIG_VALTYPE_" => {
377                        if let (Some(Token::Word(id)), Some(Token::Word(sig))) =
378                            (t.get(1), t.get(2))
379                            && let Ok(id) = id.parse::<u32>()
380                            && let Some(Token::Word(kind)) =
381                                t.iter().skip(3).find(|t| matches!(t, Token::Word(_)))
382                            && let Ok(kind) = kind.parse::<u8>()
383                        {
384                            floats.push((id, sig.clone(), kind));
385                        }
386                    }
387                    "CM_" => match (t.get(1), t.get(2), t.get(3), t.get(4)) {
388                        (
389                            Some(Token::Word(w)),
390                            Some(Token::Word(id)),
391                            Some(Token::Word(sig)),
392                            Some(Token::Str(c)),
393                        ) if w == "SG_" => {
394                            if let Ok(id) = id.parse() {
395                                comments.push((id, Some(sig.clone()), c.clone()));
396                            }
397                        }
398                        (Some(Token::Word(w)), Some(Token::Word(id)), Some(Token::Str(c)), _)
399                            if w == "BO_" =>
400                        {
401                            if let Ok(id) = id.parse() {
402                                comments.push((id, None, c.clone()));
403                            }
404                        }
405                        _ => {}
406                    },
407                    "SG_MUL_VAL_" => extended_mux = true,
408                    _ => {}
409                }
410            }
411            _ => {}
412        }
413    }
414    for (raw, sig, map) in values {
415        if let Some(&m) = by_id.get(&(raw, false))
416            && let Some(s) = dbc.messages[m].signals.iter_mut().find(|s| s.name == sig)
417        {
418            s.values = Arc::new(map);
419        }
420    }
421    for (raw, sig, kind) in floats {
422        if let Some(&m) = by_id.get(&(raw, false))
423            && let Some(s) = dbc.messages[m].signals.iter_mut().find(|s| s.name == sig)
424            && (kind == 1 && s.length == 32 || kind == 2 && s.length == 64)
425        {
426            s.float = kind;
427        }
428    }
429    for (raw, sig, comment) in comments {
430        if let Some(&m) = by_id.get(&(raw, false)) {
431            match sig {
432                Some(sig) => {
433                    if let Some(s) = dbc.messages[m].signals.iter_mut().find(|s| s.name == sig) {
434                        s.comment = Some(comment);
435                    }
436                }
437                None => dbc.messages[m].comment = Some(comment),
438            }
439        }
440    }
441    if extended_mux {
442        dbc.notes.push(format!(
443            "{}: extended multiplexing (SG_MUL_VAL_) not read {} signals follow the message's one multiplexer",
444            dbc.name,
445            crate::glyphs::get().middot
446        ));
447    }
448    Ok(dbc)
449}
450
451/// Whether `statement` holds a `;` outside a string.
452fn ends_statement(statement: &str) -> bool {
453    let mut quoted = false;
454    let mut escaped = false;
455    for c in statement.chars() {
456        match c {
457            _ if escaped => escaped = false,
458            '\\' if quoted => escaped = true,
459            '"' => quoted = !quoted,
460            ';' if !quoted => return true,
461            _ => {}
462        }
463    }
464    false
465}
466
467fn parse_signal(t: &[Token]) -> Option<Signal> {
468    let word = |i: usize| match t.get(i) {
469        Some(Token::Word(w)) => Some(w.as_str()),
470        _ => None,
471    };
472    let punct = |i: usize, c: char| t.get(i) == Some(&Token::Punct(c));
473    let name = word(1)?.to_string();
474    let (mux, at) = if punct(2, ':') {
475        (Mux::Plain, 3)
476    } else {
477        let m = word(2)?;
478        if !punct(3, ':') {
479            return None;
480        }
481        let mux = if m == "M" {
482            Mux::Multiplexer
483        } else {
484            // `m3M`: a multiplexer that is itself multiplexed; read as selected by 3.
485            let v = m.strip_prefix('m')?;
486            Mux::When(v.trim_end_matches('M').parse().ok()?)
487        };
488        (mux, 4)
489    };
490    let start: u32 = word(at)?.parse().ok()?;
491    if !punct(at + 1, '|') {
492        return None;
493    }
494    let length: u32 = word(at + 2)?.parse().ok()?;
495    if !punct(at + 3, '@') {
496        return None;
497    }
498    let order = word(at + 4)?;
499    let big_endian = order.starts_with('0');
500    let signed = order.ends_with('-');
501    if !punct(at + 5, '(') || !punct(at + 7, ',') || !punct(at + 9, ')') {
502        return None;
503    }
504    let factor: f64 = word(at + 6)?.parse().ok()?;
505    let offset: f64 = word(at + 8)?.parse().ok()?;
506    // [min|max], then the unit.
507    let unit = t[at + 10..]
508        .iter()
509        .find_map(|t| match t {
510            Token::Str(s) => Some(s.clone()),
511            _ => None,
512        })
513        .unwrap_or_default();
514    if length == 0 || length > 64 || start >= 512 * 8 || !factor.is_finite() || !offset.is_finite()
515    {
516        return None;
517    }
518    Some(Signal {
519        name,
520        start,
521        length,
522        big_endian,
523        signed,
524        factor,
525        offset,
526        unit,
527        mux,
528        float: 0,
529        values: Arc::default(),
530        comment: None,
531    })
532}
533
534/// The raw bits of `signal` in `data`, or `None` when the data is too short to hold them.
535pub fn raw(signal: &Signal, data: &[u8]) -> Option<u64> {
536    let len = signal.length as usize;
537    let mut value: u64 = 0;
538    if signal.big_endian {
539        // Motorola: the start bit is the most significant, numbered within its byte
540        // from the least significant; the next is one lower, wrapping to the next
541        // byte's bit 7.
542        let mut pos = signal.start as usize;
543        for _ in 0..len {
544            let byte = *data.get(pos / 8)?;
545            let bit = (byte >> (pos % 8)) & 1;
546            value = (value << 1) | u64::from(bit);
547            if pos.is_multiple_of(8) {
548                pos += 15;
549            } else {
550                pos -= 1;
551            }
552        }
553    } else {
554        // Intel: the start bit is the least significant, counting up.
555        for i in 0..len {
556            let pos = signal.start as usize + i;
557            let byte = *data.get(pos / 8)?;
558            value |= u64::from((byte >> (pos % 8)) & 1) << i;
559        }
560    }
561    Some(value)
562}
563
564/// `signal`'s value in `data` as an integer of its sign, before factor and offset.
565pub fn integer(signal: &Signal, data: &[u8]) -> Option<i128> {
566    let bits = raw(signal, data)?;
567    let len = signal.length;
568    Some(if signal.signed && len < 64 && bits >> (len - 1) & 1 == 1 {
569        i128::from(bits) - (1i128 << len)
570    } else if signal.signed && len == 64 {
571        i128::from(bits as i64)
572    } else {
573        i128::from(bits)
574    })
575}
576
577/// `signal`'s physical value in `data`: factor and offset applied, a float signal read
578/// as one.
579pub fn physical(signal: &Signal, data: &[u8]) -> Option<f64> {
580    let bits = raw(signal, data)?;
581    let value = match signal.float {
582        1 => f64::from(f32::from_bits(bits as u32)),
583        2 => f64::from_bits(bits),
584        _ => integer(signal, data)? as f64,
585    };
586    Some(value * signal.factor + signal.offset)
587}
588
589/// Whether `signal` is present in a frame whose multiplexer reads `mux`.
590pub fn present(signal: &Signal, mux: Option<u64>) -> bool {
591    match signal.mux {
592        Mux::When(v) => mux == Some(v),
593        _ => true,
594    }
595}
596
597#[cfg(test)]
598mod tests {
599    use super::*;
600
601    #[test]
602    fn messages_signals_values_and_comments() {
603        let dbc = parse(crate::tests::fixtures::DBC, "car", None).unwrap();
604        assert_eq!(dbc.messages.len(), 3);
605        let body = &dbc.messages[1];
606        assert!(body.extended);
607        assert_eq!(body.id, 0x18FEF1FE);
608        let engine = &dbc.messages[0];
609        assert_eq!(engine.signals[1].offset, -40.0);
610        assert!(engine.signals[1].signed);
611        assert_eq!(
612            engine.signals[2].values.get(&1).map(String::as_str),
613            Some("First")
614        );
615        assert_eq!(
616            engine.signals[0].comment.as_deref(),
617            Some("Engine speed;\nover two lines")
618        );
619        let muxed = &dbc.messages[2];
620        assert_eq!(muxed.signals[0].mux, Mux::Multiplexer);
621        assert_eq!(muxed.signals[2].mux, Mux::When(2));
622        assert_eq!(muxed.signals[3].float, 1);
623    }
624
625    #[test]
626    fn intel_motorola_signed_and_float() {
627        let dbc = parse(crate::tests::fixtures::DBC, "car", None).unwrap();
628        let engine = &dbc.messages[0];
629        // Speed 0x1F40 = 8000 * 0.125 = 1000 rpm; Temp 0xF6 = -10 - 40 = -50.
630        let data = [0x40, 0x1F, 0xF6, 0x02, 0, 0, 0, 0];
631        assert_eq!(physical(&engine.signals[0], &data), Some(1000.0));
632        assert_eq!(physical(&engine.signals[1], &data), Some(-50.0));
633        assert_eq!(integer(&engine.signals[2], &data), Some(2));
634        let body = &dbc.messages[1];
635        // Motorola: Pressure starts at bit 7 of byte 0: 0x1234 = 4660 * 0.1.
636        // Offset starts at bit 23 (byte 2's top) for 12 bits: 0xFFF = -1.
637        let data = [0x12, 0x34, 0xFF, 0xF0, 0, 0, 0, 0];
638        let p = physical(&body.signals[0], &data).unwrap();
639        assert!((p - 466.0).abs() < 1e-9);
640        assert_eq!(integer(&body.signals[1], &data), Some(-1));
641        let muxed = &dbc.messages[2];
642        let mut data = [1u8, 0x10, 0x27, 0, 0, 0, 0, 0];
643        data[3..7].copy_from_slice(&1.5f32.to_le_bytes());
644        assert_eq!(physical(&muxed.signals[3], &data), Some(1.5));
645        assert!(present(&muxed.signals[1], Some(1)));
646        assert!(!present(&muxed.signals[2], Some(1)));
647        // Too short a frame holds no value.
648        assert_eq!(raw(&engine.signals[0], &[0x40]), None);
649    }
650
651    #[test]
652    fn bad_statements_are_errors_with_their_line() {
653        let e = parse("BO_ x ENGINE: 8 ECU\n", "car", None).unwrap_err();
654        assert_eq!(e.line, 1);
655        let e = parse("BO_ 1 A: 8 X\n SG_ broken\n", "car", None).unwrap_err();
656        assert_eq!(e.line, 2);
657        assert!(parse(" SG_ S : 0|8@1+ (1,0) [0|0] \"\" X\n", "car", None).is_err());
658        // An unterminated statement runs to the end of the file, and is bounded.
659        assert!(parse("CM_ \"never ends", "car", None).is_ok());
660    }
661}