Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ members = [
"dbc-codegen-cli",
"testing/can-embedded",
"testing/can-messages",
"testing/can-messages-split",
"testing/cantools-messages",
"testing/rust-integration",
]
Expand Down
4 changes: 1 addition & 3 deletions dbc-codegen-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,12 @@ fn main() {
exit(exitcode::CANTCREAT);
}

let messages_path = args.out_path.join("messages.rs");

if let Err(e) = Config::builder()
.dbc_name(&dbc_file_name)
.dbc_content(&dbc_file)
.debug_prints(args.debug)
.build()
.write_to_file(messages_path)
.write_split_by_sender(&args.out_path)
{
eprintln!("could not convert `{}`: {e}", args.dbc_path.display());
if args.debug {
Expand Down
109 changes: 102 additions & 7 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,8 @@ impl Config<'_> {
})?;

writeln!(w)?;
self.render_dbc(&mut w, &dbc)?;
let messages: Vec<&Message> = get_relevant_messages(&dbc).collect();
self.render_dbc(&mut w, &dbc, &messages)?;
writeln!(w)?;
self.render_error(&mut w)?;
self.render_arbitrary_helpers(&mut w)?;
Expand All @@ -153,10 +154,10 @@ impl Config<'_> {
Ok(())
}

fn render_dbc(&self, w: &mut impl Write, dbc: &Dbc) -> Result<()> {
self.render_root_enum(w, dbc)?;
fn render_dbc(&self, w: &mut impl Write, dbc: &Dbc, messages: &[&Message]) -> Result<()> {
self.render_root_enum(w, messages)?;

for msg in get_relevant_messages(dbc) {
for msg in messages {
self.render_message(w, msg, dbc)
.with_context(|| format!("write message `{}`", msg.name))?;
writeln!(w)?;
Expand All @@ -165,7 +166,7 @@ impl Config<'_> {
Ok(())
}

fn render_root_enum(&self, w: &mut impl Write, dbc: &Dbc) -> Result<()> {
fn render_root_enum(&self, w: &mut impl Write, messages: &[&Message]) -> Result<()> {
writeln!(w, "/// All messages")?;
writeln!(w, "{ALLOW_LINTS}")?;
self.write_allow_dead_code(w)?;
Expand All @@ -177,7 +178,7 @@ impl Config<'_> {
writeln!(w, "pub enum Messages {{")?;
{
let mut w = PadAdapter::wrap(w);
for msg in get_relevant_messages(dbc) {
for msg in messages {
writeln!(w, "/// {}", msg.name)?;
writeln!(w, "{0}({0}),", msg.type_name())?;
}
Expand All @@ -199,7 +200,6 @@ impl Config<'_> {

{
let mut w = PadAdapter::wrap(&mut w);
let messages: Vec<_> = get_relevant_messages(dbc).collect();
if messages.is_empty() {
writeln!(w, "Err(CanError::UnknownMessageId(id))")?;
} else {
Expand Down Expand Up @@ -229,6 +229,53 @@ impl Config<'_> {
Ok(())
}

fn codegen_for_sender(&self, out: &mut impl Write, dbc: &Dbc, sender: &str) -> Result<()> {
let msgs: Vec<&Message> = get_relevant_messages(dbc)
.filter(|m| matches!(&m.transmitter, Transmitter::NodeName(n) if n == sender))
.collect();

let mut w = BufWriter::new(out);

writeln!(
w,
"/// The name of the DBC file this code was generated from"
)?;
writeln!(w, "#[allow(dead_code)]")?;
let dbc_name = self.dbc_name.to_token_stream();
writeln!(w, "pub const DBC_FILE_NAME: &str = {dbc_name};")?;
writeln!(
w,
"/// The version of the DBC file this code was generated from"
)?;
writeln!(w, "#[allow(dead_code)]")?;
let dbc_version = dbc.version.0.to_token_stream();
writeln!(w, "pub const DBC_FILE_VERSION: &str = {dbc_version};")?;

writeln!(w, "#[allow(unused_imports)]")?;
writeln!(w, "use core::ops::BitOr;")?;
writeln!(w, "#[allow(unused_imports)]")?;
writeln!(w, "use bitvec::prelude::*;")?;
writeln!(w, "#[allow(unused_imports)]")?;
writeln!(w, "use embedded_can::{{Id, StandardId, ExtendedId}};")?;

self.impl_arbitrary.fmt_cfg(&mut w, |w| {
writeln!(w, "use arbitrary::{{Arbitrary, Unstructured}};")
})?;

self.impl_serde.fmt_cfg(&mut w, |w| {
writeln!(w, "use serde::{{Serialize, Deserialize}};")
})?;

writeln!(w)?;
self.render_dbc(&mut w, dbc, &msgs)?;
writeln!(w)?;
self.render_error(&mut w)?;
self.render_arbitrary_helpers(&mut w)?;
writeln!(w)?;

Ok(())
}

fn render_message(&self, w: &mut impl Write, msg: &Message, dbc: &Dbc) -> Result<()> {
writeln!(w, "/// {}", msg.name)?;
writeln!(w, "///")?;
Expand Down Expand Up @@ -1322,6 +1369,16 @@ fn message_ignored(message: &Message) -> bool {
message.name == "VECTOR__INDEPENDENT_SIG_MSG"
}

fn unique_senders(dbc: &Dbc) -> Vec<String> {
let mut seen = BTreeSet::new();
for msg in get_relevant_messages(dbc) {
if let Transmitter::NodeName(name) = &msg.transmitter {
seen.insert(name.clone());
}
}
seen.into_iter().collect()
}

impl Config<'_> {
/// Generate Rust structs matching DBC input description and return as String
pub fn generate(self) -> Result<String> {
Expand Down Expand Up @@ -1353,6 +1410,44 @@ impl Config<'_> {
self.write(file)
}

/// Generate one `.rs` file per sender and a `mod.rs` in `out_dir`
pub fn write_split_by_sender<P: AsRef<Path>>(self, out_dir: P) -> Result<()> {
let dbc = Dbc::try_from(self.dbc_content).map_err(|e| {
let msg = "Could not parse dbc file";
if self.debug_prints {
anyhow!("{msg}: {e:#?}")
} else {
anyhow!("{msg}")
}
})?;

let senders = unique_senders(&dbc);
let mut mod_names: Vec<String> = Vec::new();

for sender in &senders {
let mod_name = sender.to_snake_case();
let path = out_dir.as_ref().join(format!("{mod_name}.rs"));

let mut buf = Vec::new();
self.codegen_for_sender(&mut buf, &dbc, sender)?;
let code = std::str::from_utf8(&buf).context("Failed to convert output to str")?;
let file = syn::parse_file(code).context("Failed to parse generated Rust code")?;
std::fs::write(&path, prettyplease::unparse(&file))?;

mod_names.push(mod_name);
}

let mut mod_rs = String::new();
for m in &mod_names {
mod_rs.push_str(&format!(
"pub mod {m} {{ include!(concat!(env!(\"OUT_DIR\"), \"/{m}.rs\")); }}\n"
));
}
std::fs::write(out_dir.as_ref().join("mod.rs"), mod_rs)?;

Ok(())
}

fn write_allow_dead_code(&self, w: &mut impl Write) -> Result<()> {
if self.allow_dead_code {
writeln!(w, "{ALLOW_DEADCODE}")?;
Expand Down
13 changes: 13 additions & 0 deletions testing/can-messages-split/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "can-messages-split"
version = "0.1.0"
edition = "2021"
publish = false

[dependencies]
bitvec = { version = "1.0", default-features = false }
embedded-can = "0.4.1"

[build-dependencies]
anyhow = "1.0"
dbc-codegen = { path = "../.." }
167 changes: 167 additions & 0 deletions testing/can-messages-split/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# can-messages-split

This crate demonstrates per-sender code generation from a DBC file using `dbc-codegen`.

## How it works

### Input: `multiple_devices.dbc`

The DBC file defines five nodes (devices) on the CAN bus:

```
BU_: MCU BMS DISPLAY VCU CHARGER
```

Each message has a transmitter (sender):

| Message | Sender |
|--------------------------------|---------|
| `Gear_Selection` | VCU |
| `Bike_Information_New` | VCU |
| `MCU_Diag_Status` | MCU |
| `Cell_voltage_information_BMS` | BMS |
| `Total_Information_0_BMS` | BMS |
| `Charger_to_BMS` | CHARGER |

### Build step: `build.rs`

At compile time, `build.rs` reads the DBC file and calls `write_split_by_sender`:

```rust
Config::builder()
.dbc_name("multiple_devices.dbc")
.dbc_content(&dbc_file)
.build()
.write_split_by_sender(&out_dir)
```

This generates one `.rs` file per sender in `OUT_DIR`:

```
OUT_DIR/
├── bms.rs # Cell_voltage_information_BMS, Total_Information_0_BMS
├── charger.rs # Charger_to_BMS
├── mcu.rs # MCU_Diag_Status
├── vcu.rs # Gear_Selection, Bike_Information_New
└── mod.rs # glue that includes each file above
```

Each sender file contains:
- A `Messages` enum listing that sender's messages
- A struct per message with typed signal getters and setters
- A `CanError` type for decode errors

The generated `mod.rs` uses inline `include!` macros so Rust can find the files in `OUT_DIR`:

```rust
pub mod vcu { include!(concat!(env!("OUT_DIR"), "/vcu.rs")); }
pub mod mcu { include!(concat!(env!("OUT_DIR"), "/mcu.rs")); }
pub mod bms { include!(concat!(env!("OUT_DIR"), "/bms.rs")); }
pub mod charger { include!(concat!(env!("OUT_DIR"), "/charger.rs")); }
```

### Entry point: `src/lib.rs`

The library simply pulls in the generated `mod.rs`:

```rust
include!(concat!(env!("OUT_DIR"), "/mod.rs"));
```

### Using the generated types

After building, each sender's messages are accessible under their module:

```rust
use can_messages_split::vcu::GearSelection;
use can_messages_split::mcu::McuDiagStatus;
use can_messages_split::bms::CellVoltageInformationBms;
use can_messages_split::charger::ChargerToBms;
```

---

## Logic in `dbc-codegen` (`src/lib.rs`)

The split is implemented in three functions in the library.

### 1. `unique_senders`

```rust
fn unique_senders(dbc: &Dbc) -> Vec<String> {
let mut seen = BTreeSet::new();
for msg in get_relevant_messages(dbc) {
if let Transmitter::NodeName(name) = &msg.transmitter {
seen.insert(name.clone());
}
}
seen.into_iter().collect()
}
```

Walks every message in the DBC and collects the transmitter name when it is a
named node (`NodeName`). Messages with no named transmitter (`VectorXxx`) are
skipped. A `BTreeSet` is used so the result is deduplicated and sorted
alphabetically, giving a stable file order across runs.

### 2. `codegen_for_sender`

```rust
fn codegen_for_sender(&self, out: &mut impl Write, dbc: &Dbc, sender: &str) -> Result<()>
```

Works like the existing `codegen` function but only renders messages belonging
to the given sender:

1. Filters `get_relevant_messages(dbc)` to those whose `transmitter` matches
`sender`.
2. Writes the standard file header — `DBC_FILE_NAME`, `DBC_FILE_VERSION`, and
the necessary `use` imports.
3. Calls `render_dbc` with the filtered message slice, which generates the
`Messages` enum and one struct per message.
4. Appends `CanError` and arbitrary helpers (same as the single-file path).

The raw output is a valid Rust source string, not yet formatted.

### 3. `write_split_by_sender`

```rust
pub fn write_split_by_sender<P: AsRef<Path>>(self, out_dir: P) -> Result<()>
```

Orchestrates the whole split:

1. **Parses** the DBC content once into a `Dbc` value.
2. **Calls `unique_senders`** to get the list of node names (e.g. `["BMS",
"CHARGER", "MCU", "VCU"]`).
3. **Loops** over each sender:
- Converts the sender name to `snake_case` using `heck` (e.g. `VCU` →
`vcu`) to get a valid Rust module name.
- Calls `codegen_for_sender` into an in-memory buffer.
- Parses the buffer with `syn` and reformats it with `prettyplease` so the
output is consistently formatted regardless of how the strings were built.
- Writes the result to `{out_dir}/{mod_name}.rs`.
4. **Generates `mod.rs`** with one entry per sender using an inline `include!`
macro instead of a plain `pub mod` declaration. A plain `pub mod vcu;`
would tell the compiler to look for `vcu.rs` next to `src/lib.rs`, but the
files are in `OUT_DIR`. The `include!` approach points directly at the right
path:

```rust
pub mod vcu { include!(concat!(env!("OUT_DIR"), "/vcu.rs")); }
```

### Why `render_dbc` takes a message slice

The original `render_dbc` called `get_relevant_messages` internally, so it
always rendered every message. To support the split it was refactored to accept
a `&[&Message]` slice from the caller:

```rust
fn render_dbc(&self, w: &mut impl Write, dbc: &Dbc, messages: &[&Message]) -> Result<()>
```

The single-file path collects all messages and passes the full slice. The
per-sender path passes only the filtered subset. Everything downstream
(`render_root_enum`, `render_message`, etc.) is unaware of the filtering — it
just renders whatever it receives.
Loading