Skip to content

Repository files navigation

CAN DBC code generator for Rust

GitHub repo crates.io version crate usage docs.rs status crates.io license CI build status Codecov

Generates Rust messages from a dbc file. DBC files are descriptions of CAN frames. See this post for an introduction.

Installation

Install published version using cargo:

cargo install dbc-codegen-cli

Install latest version from the git repository:

cargo install dbc-codegen-cli --git https://github.com/technocreatives/dbc-codegen --branch main

Using dbc-codegen

Generate messages.rs from example.dbc using the CLI:

dbc-codegen testing/dbc-examples/example.dbc dir/where/messages_rs/file/is/written

Or put something like this into your build.rs file. Create a Config and pass it to codegen along with the contents of a DBC-file. See Config docs for a complete list of options.

use std::env::var;
use std::path::PathBuf;
use std::fs::read_to_string;

use dbc_codegen::{Config, FeatureConfig};

fn main() {
    let dbc_path = "../dbc-examples/example.dbc";
    let dbc_file = read_to_string(dbc_path).unwrap();
    println!("cargo:rerun-if-changed={dbc_path}");

    let path = PathBuf::from(var("OUT_DIR").unwrap()).join("messages.rs");

    Config::builder()
        .dbc_name("example.dbc")
        .dbc_content(&dbc_file)
        //.impl_arbitrary(FeatureConfig::Gated("arbitrary")) // optional
        //.impl_debug(FeatureConfig::Always)                 // optional
        .build()
        .write_to_file(path)
        .unwrap();
}

Using generated Rust code

dbc-codegen generates a Rust file that is usually placed into the OUT_DIR directory. Here is an example testing/can-messages/Cargo.toml which defines dependencies and features that are used in generated message file.

Project setup

To use the code, add this code to your lib.rs (or main.rs):

// Import the generated code.
mod messages {
    include!(concat!(env!("OUT_DIR"), "/messages.rs"));
}

You will most likely want to interact with the generated Messages enum, and call Messages::from_can_message(id, &payload).

Note: The generated code contains a lot of documentation. Give it a try:

cargo doc --open

Optional impls

The generator config has the following flags that control what code gets generated:

  • impl_debug: enables #[derive(Debug)] for messages.
  • impl_arbitrary: enables implementation of Arbitrary trait. Also requires you to add arbitrary crate (version 1.x) as a dependency of the crate.
  • impl_error: Implements core::error::Error for CanError. This makes it easy to use crates like anyhow for error handling.
  • check_ranges: adds range checks in signal setters. (Enabled by default)

These implementations can be enabled, disabled, or placed behind feature guards, like so:

use dbc_codegen::{Config, FeatureConfig};

Config::builder()
    // this will generate Debug implementations
    .impl_debug(FeatureConfig::Always)

    // this will generate Error implementations behind a `#[cfg(feature = "std")]` guard
    .impl_error(FeatureConfig::Gated("std"))

    // this will disable range checks
    .check_ranges(FeatureConfig::Never);

Attribute-driven constants

Many DBCs contain additional metadata in BA_ attributes - for example, parameters describing an AUTOSAR E2E protection scheme for specific CAN signals. attribute_structs lets you expose this metadata as typed constants in the generated message types.

You can declare a struct in your own crate and map each field to a DBC attribute, derived signal, or a literal:

use dbc_codegen::{AttributeField, AttributeScope, AttributeStruct, Config, FieldSource};

// `data_protection::E2EDataIdInfo { data_id, start_byte, width_bit }` is a type defined in your crate.
let e2e = AttributeStruct {
    type_path: "data_protection::E2EDataIdInfo",
    const_name: "E2E",
    scope: AttributeScope::Signal,   // One const per matching signal
    require: "E2EDataId",            // Only signals carrying this attribute
    fields: &[
        AttributeField { name: "data_id",    source: FieldSource::Attr("E2EDataId") },
        AttributeField { name: "start_byte",  source: FieldSource::StartByte },
        AttributeField { name: "width_bit",   source: FieldSource::Attr("E2EDataLength") },
    ],
    for_node: None,
};

let dbc_file = "";
Config::builder()
    .dbc_name("example.dbc")
    .dbc_content(dbc_file)
    .attribute_structs(&[e2e])
    .build()
    .generate()
    .unwrap();

For every signal that carries an E2EDataId attribute, this generates:

impl SomeMessage {
    pub const SOME_SIGNAL_E2E: data_protection::E2EDataIdInfo =
        data_protection::E2EDataIdInfo { data_id: 373, start_byte: 0, width_bit: 48 };
}

DBCs can also attach attributes to relations between a node and a signal (BA_DEF_REL_ BU_SG_REL_ / BA_REL_) or a node and a message (BA_DEF_REL_ BU_BO_REL_ / BA_REL_), for example, a signal timeout. AttributeScope::NodeSignal emits one const per receiving node of a matching signal, and AttributeScope::NodeMessage emits one const per node related to a matching message. FieldSource::NodeName exposes the related node's name and is only valid with these two scopes:

use dbc_codegen::{AttributeField, AttributeScope, AttributeStruct, Config, FieldSource};

// `relation::SigTimeoutInfo { node, timeout_ms }` is a type defined in your crate.
let sig_timeout = AttributeStruct {
    type_path: "relation::SigTimeoutInfo",
    const_name: "SIG_TIMEOUT",
    scope: AttributeScope::NodeSignal,     // One const per receiving node of a matching signal
    require: "GenSigTimeoutTime",          // Only nodes carrying this relation attribute
    fields: &[
        AttributeField { name: "node",       source: FieldSource::NodeName },
        AttributeField { name: "timeout_ms", source: FieldSource::Attr("GenSigTimeoutTime") },
    ],
    for_node: None,
};

let dbc_file = "";
Config::builder()
    .dbc_name("example.dbc")
    .dbc_content(dbc_file)
    .attribute_structs(&[sig_timeout])
    .build()
    .generate()
    .unwrap();

For every signal and receiving node pair that carries a GenSigTimeoutTime relation attribute, this generates:

impl SomeMessage {
    pub const SOME_SIGNAL_ECU2_SIG_TIMEOUT: relation::SigTimeoutInfo =
        relation::SigTimeoutInfo { node: "ECU2", timeout_ms: 60 };
}

If you're generating code for a single node/ECU, you can set for_node to the node name to generates constants only for relation attributes that are relevant to the specific node. This also removes the node name from the constant names, making the generated code node/ECU name-agnostic.

use dbc_codegen::{AttributeField, AttributeScope, AttributeStruct, Config, FieldSource};

let sig_timeout_for_ecu2 = AttributeStruct {
    type_path: "relation::SigTimeoutInfo",
    const_name: "SIG_TIMEOUT",
    scope: AttributeScope::NodeSignal,
    require: "GenSigTimeoutTime",
    fields: &[
        AttributeField { name: "node",       source: FieldSource::NodeName },
        AttributeField { name: "timeout_ms", source: FieldSource::Attr("GenSigTimeoutTime") },
    ],
    for_node: Some("ECU2"),
};

let dbc_file = "";
Config::builder()
    .dbc_name("example.dbc")
    .dbc_content(dbc_file)
    .attribute_structs(&[sig_timeout_for_ecu2])
    .build()
    .generate()
    .unwrap();
impl SomeMessage {
    pub const SOME_SIGNAL_SIG_TIMEOUT: relation::SigTimeoutInfo =
        relation::SigTimeoutInfo { node: "ECU2", timeout_ms: 60 };
}

no_std

The generated code is no_std compatible.

Field/variant rename rules

If some field name starts with a non-alphabetic character or is a Rust keyword then it is prefixed with x.

For example:

VAL_ 512 Five 0 "0Off" 1 "1On" 2 "2Oner" 3 "3Onest";

…is generated as:

pub enum BarFive {
    X0off,
    X1on,
    X2oner,
    X3onest,
    _Other(bool),
}

Type here:

SG_ Type : 30|1@0+ (1,0) [0|1] "boolean" Dolor

…conflicts with the Rust keyword type. Therefore, we prefix it with x:

pub fn xtype(&self) -> BarType {
    match self.xtype_raw() {
        false => BarType::X0off,
        true => BarType::X1on,
        x => BarType::_Other(x),
    }
}

Development

  • This project is easier to develop with just, a modern alternative to make.
  • To get a list of available commands, run just.
  • To run tests, use just test.
  • This project uses insta for snapshot testing. To update the snapshots run just bless

lorri for Nix

If using Nix, dbc-codegen is integrated with lorri for easy project dependency management. To enable, create a symlink in the top-level working directory:

ln -s envrc.lorri .envrc

License

Licensed under either of

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual-licensed as above, without any additional terms or conditions.

About

Generate Rust structs for messages from a dbc (CAN bus definition) file.

Topics

Resources

Contributing

Stars

78 stars

Watchers

8 watching

Forks

Releases

Used by

Contributors

Languages