diff --git a/Cargo.lock b/Cargo.lock index 2bc9d1f2..45f87689 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -189,6 +189,16 @@ dependencies = [ "embedded-can", ] +[[package]] +name = "can-messages-split" +version = "0.1.0" +dependencies = [ + "anyhow", + "bitvec", + "dbc-codegen", + "embedded-can", +] + [[package]] name = "cantools-messages" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 221d4dc9..fcc3a56a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,6 +59,7 @@ members = [ "dbc-codegen-cli", "testing/can-embedded", "testing/can-messages", + "testing/can-messages-split", "testing/cantools-messages", "testing/rust-integration", ] diff --git a/dbc-codegen-cli/src/main.rs b/dbc-codegen-cli/src/main.rs index 70feec92..689708ba 100644 --- a/dbc-codegen-cli/src/main.rs +++ b/dbc-codegen-cli/src/main.rs @@ -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 { diff --git a/src/lib.rs b/src/lib.rs index 23440f4b..6f94b557 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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)?; @@ -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)?; @@ -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)?; @@ -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())?; } @@ -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 { @@ -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, "///")?; @@ -1322,6 +1369,16 @@ fn message_ignored(message: &Message) -> bool { message.name == "VECTOR__INDEPENDENT_SIG_MSG" } +fn unique_senders(dbc: &Dbc) -> Vec { + 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 { @@ -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>(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 = 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}")?; diff --git a/testing/can-messages-split/Cargo.toml b/testing/can-messages-split/Cargo.toml new file mode 100644 index 00000000..fa4eb98a --- /dev/null +++ b/testing/can-messages-split/Cargo.toml @@ -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 = "../.." } diff --git a/testing/can-messages-split/README.md b/testing/can-messages-split/README.md new file mode 100644 index 00000000..230ac504 --- /dev/null +++ b/testing/can-messages-split/README.md @@ -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 { + 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>(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. diff --git a/testing/can-messages-split/build.rs b/testing/can-messages-split/build.rs new file mode 100644 index 00000000..df021479 --- /dev/null +++ b/testing/can-messages-split/build.rs @@ -0,0 +1,21 @@ +use std::env::var; +use std::fs::read_to_string; +use std::path::PathBuf; + +use anyhow::Result; +use dbc_codegen::Config; + +fn main() -> Result<()> { + let dbc_file = read_to_string("../dbc-examples/multiple_devices.dbc")?; + println!("cargo:rerun-if-changed=../dbc-examples/multiple_devices.dbc"); + println!("cargo:rerun-if-changed=../../src"); + + let out_dir = PathBuf::from(var("OUT_DIR")?); + + Config::builder() + .dbc_name("multiple_devices.dbc") + .dbc_content(&dbc_file) + .allow_dead_code(true) + .build() + .write_split_by_sender(&out_dir) +} diff --git a/testing/can-messages-split/src/lib.rs b/testing/can-messages-split/src/lib.rs new file mode 100644 index 00000000..cde779ad --- /dev/null +++ b/testing/can-messages-split/src/lib.rs @@ -0,0 +1 @@ +include!(concat!(env!("OUT_DIR"), "/mod.rs")); diff --git a/testing/dbc-examples/multiple_devices.dbc b/testing/dbc-examples/multiple_devices.dbc new file mode 100644 index 00000000..55ab5303 --- /dev/null +++ b/testing/dbc-examples/multiple_devices.dbc @@ -0,0 +1,104 @@ +VERSION "1" + + +NS_ : + NS_DESC_ + CM_ + BA_DEF_ + BA_ + VAL_ + CAT_DEF_ + CAT_ + FILTER + BA_DEF_DEF_ + EV_DATA_ + ENVVAR_DATA_ + SGTYPE_ + SGTYPE_VAL_ + BA_DEF_SGTYPE_ + BA_SGTYPE_ + SIG_TYPE_REF_ + VAL_TABLE_ + SIG_GROUP_ + SIG_VALTYPE_ + SIGTYPE_VALTYPE_ + BO_TX_BU_ + BA_DEF_REL_ + BA_REL_ + BA_DEF_DEF_REL_ + BU_SG_REL_ + BU_EV_REL_ + BU_BO_REL_ + SG_MUL_VAL_ + +BS_: + +BU_: MCU BMS DISPLAY VCU CHARGER + +BO_ 2203160576 Gear_Selection: 8 VCU + SG_ Gear : 0|2@1+ (1,0) [0|1] "" MCU + SG_ Drive_Profile : 8|4@1+ (1,0) [0|15] "" MCU + SG_ Allow_Discharge : 16|16@1+ (0.1,0) [0|6553.5] "A" MCU + SG_ Allow_Charge : 32|16@1+ (0.1,0) [0|6553.5] "A" MCU + +BO_ 2203131904 MCU_Diag_Status: 8 MCU + SG_ Throttle : 50|10@1+ (0.1,0) [0|100] "%" VCU + SG_ Derating : 40|10@1+ (0.1,0) [0|100] "%" VCU + SG_ Driving_Profile : 36|4@1+ (1,0) [0|15] "" VCU + SG_ FNR : 32|4@1+ (1,0) [0|15] "" VCU + SG_ Pre_charge_temp : 24|8@1+ (1,-50) [-50|205] "C" VCU + SG_ MCU_temp : 16|8@1+ (1,-50) [-50|205] "C" VCU + SG_ Motor_temp : 8|8@1+ (1,-50) [-50|205] "degC" VCU + SG_ Brake : 2|1@1+ (1,0) [0|1] "" VCU + SG_ Limp_home : 1|1@1+ (1,0) [0|1] "" VCU + SG_ hill_hold : 0|1@1+ (1,0) [0|1] "" VCU + SG_ Derating_Reason : 3|3@1+ (1,0) [0|7] "" VCU + +BO_ 2214625281 Cell_voltage_information_BMS: 8 BMS + SG_ Multiplexor M : 0|8@1+ (1,0) [0|255] "" VCU + SG_ Cell_1_Voltage m1 : 8|16@1+ (0.001,0) [0|65.535] "V" VCU + SG_ Cell_2_Voltage m1 : 24|16@1+ (0.001,0) [0|65.535] "V" VCU + SG_ Cell_3_Voltage m1 : 40|16@1+ (0.001,0) [0|65.535] "V" VCU + SG_ Cell_4_Voltage m2 : 8|16@1+ (0.001,0) [0|65.535] "V" VCU + SG_ Cell_5_Voltage m2 : 24|16@1+ (0.001,0) [0|65.535] "V" VCU + SG_ Cell_6_Voltage m2 : 40|16@1+ (0.001,0) [0|65.535] "V" VCU + SG_ Cell_7_Voltage m3 : 8|16@1+ (0.001,0) [0|65.535] "V" VCU + SG_ Cell_8_Voltage m3 : 24|16@1+ (0.001,0) [0|65.535] "V" VCU + SG_ Cell_9_Voltage m3 : 40|16@1+ (0.001,0) [0|65.535] "V" VCU + SG_ Cell_10_Voltage m4 : 8|16@1+ (0.001,0) [0|65.535] "V" VCU + SG_ Cell_11_Voltage m4 : 24|16@1+ (0.001,0) [0|65.535] "V" VCU + SG_ Cell_12_Voltage m4 : 40|16@1+ (0.001,0) [0|65.535] "V" VCU + SG_ Cell_13_Voltage m5 : 8|16@1+ (0.001,0) [0|65.535] "V" VCU + SG_ Cell_14_Voltage m5 : 24|16@1+ (0.001,0) [0|65.535] "V" VCU + SG_ Cell_15_Voltage m5 : 40|16@1+ (0.001,0) [0|65.535] "V" VCU + + BO_ 2214756353 Total_Information_0_BMS: 8 BMS + SG_ Sum_Voltage : 7|16@0+ (0.1,0) [0|6553.5] "V" VCU + SG_ Current : 23|16@0- (0.1,-3000) [-3000|276.7] "A" VCU + SG_ SOC : 39|16@0+ (0.1,0) [0|100] "%" VCU + SG_ Life : 48|8@0+ (1,0) [0|255] "" VCU + +BO_ 2566869221 Charger_to_BMS: 8 CHARGER + SG_ Charger_Output_Voltage : 7|16@0+ (0.1,0) [0|6553.5] "V" BMS + SG_ Charger_Output_Current : 23|16@0+ (0.1,0) [0|6553.5] "A" BMS + SG_ Charger_Status_Flag : 32|8@1+ (1,0) [0|255] "" BMS + + +BO_ 2432176272 Bike_Information_New: 8 VCU + SG_ Low_Beam : 0|1@1+ (1,0) [0|1] "" DISPLAY + SG_ High_Beam : 1|1@1+ (1,0) [0|1] "" DISPLAY + SG_ DRL : 2|1@1+ (1,0) [0|1] "" DISPLAY + SG_ Right_Blinker : 3|1@1+ (1,0) [0|1] "" DISPLAY + SG_ Left_Blinker : 4|1@1+ (1,0) [0|1] "" DISPLAY + SG_ Kickstand : 5|1@1+ (1,0) [0|1] "" DISPLAY + SG_ Ready : 6|1@1+ (1,0) [0|1] "" DISPLAY + SG_ Warning : 8|1@1+ (1,0) [0|1] "" DISPLAY + SG_ Battery_Error : 9|1@1+ (1,0) [0|1] "" DISPLAY + SG_ Motor_Error : 10|1@1+ (1,0) [0|1] "" DISPLAY + SG_ Limp_Home : 11|1@1+ (1,0) [0|1] "" DISPLAY + SG_ Modes : 16|2@1+ (1,0) [0|2] "" DISPLAY + SG_ Gear : 18|2@1+ (1,0) [0|2] "" DISPLAY + SG_ Units : 20|1@1+ (1,0) [0|1] "" DISPLAY + SG_ Trip_Selection : 21|1@1+ (1,0) [0|1] "" DISPLAY + SG_ TripA_Distance : 24|16@1+ (1,0) [0|999] "" DISPLAY + SG_ Speed : 48|8@1+ (1,0) [0|255] "" DISPLAY