Skip to content
Merged
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
34 changes: 34 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---

name: Build and test
on:
pull_request:
push:
branches:
- main

jobs:
build_and_test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
ros: [humble, jazzy, lyrical]
name: ROS 2 ${{ matrix.ros }}
container:
image: polymathrobotics/ros:${{ matrix.ros }}-builder-ubuntu
# rosdep installs pip deps (e.g. python3-cantools-pip) globally as root.
# On Python >= 3.11 (Ubuntu 24.04 images: jazzy, lyrical), PEP 668 requires
# opting in to installing alongside externally-managed packages.
env:
PIP_BREAK_SYSTEM_PACKAGES: 1
steps:
- uses: actions/checkout@v4
- uses: ros-tooling/action-ros-ci@v0.4
with:
target-ros2-distro: ${{ matrix.ros }}
coverage-result: false
- uses: actions/upload-artifact@v4
with:
name: colcon-logs-${{ matrix.ros }}
path: ros_ws/log
15 changes: 15 additions & 0 deletions .github/workflows/pre-commit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
name: pre-commit

on:
pull_request:
push:
branches: [main]

jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v3
- uses: pre-commit/action@v3.0.1
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/.ruff.toml
/*.egg-info/
__pycache__/
/.pytest_tmp*/
21 changes: 21 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
repos:
- repo: https://github.com/polymathrobotics/polymath_code_standard
rev: v2.2.0
hooks:
# Basic checks and fixes that apply to any text file and the git repository itself
- id: polymath-general
- id: polymath-copyright
args: [--license, Apache-2.0, --copyright-org, 'Polymath Robotics, Inc.', --reuse-style]
# Specific languages
- id: polymath-python
- id: polymath-cpp
- id: polymath-ros
- id: polymath-shell
- id: polymath-cmake
- id: polymath-docker
- id: polymath-markdown
- id: polymath-xml
- id: polymath-yaml
- id: polymath-toml
- id: polymath-json
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.

Copyright [yyyy] [name of copyright owner]
Copyright 2026 Polymath Robotics, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
95 changes: 93 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,93 @@
# dbc_gen_cpp
DBC CAN Message Type Generator for cpp
# DBC CAN Message Type Generator for C++

Extends the code generation capability of `cantools`, which outputs C sources from DBC frame definitions, to create idiomatic C++ interfaces for using CAN message types.

## Usage

In `package.xml`, you'll need this as a `build_depend`.

In your `CMakeLists.txt`:

```cmake
find_package(dbc_gen_cpp REQUIRED)
...

generate_dbc_cpp(my_can_library_name
DBC ${CMAKE_CURRENT_SOURCE_DIR}/database.dbc
)

...

target_link_libraries(my_library PUBLIC my_can_library_name)
```

### CAN Messages

Message structs are defined in the library name's namespace.

They can be implicitly converted to `can_frame` from `<linux/can.h>`

```c++
#include "my_can_library_name/my_can_library_name.hpp"

...

my_can_library_name::MessageName message{value};
my_can_socket->send(message);
can_frame frame = message;
my_can_socket->send(frame);

```

### CAN Handler - Receive/Subscribe to CAN Messages

A helper class `dbc_gen_cpp::CANHandler` is provided.

Simply register a handler function for a type via `set_handler`, then forward all `can_frame`s received to the `handle()` method to trigger the registered handler functions with the typed structs.

```c++
dbc_gen_cpp::CANHandler handler;
handler.set_handler<my_can_library_name::MessageName>(
[](const my_can_library_name::MessageName & message) {
printf('Received MessageName (value %f)\n', message.value);
});

my_can_socket.on_receive(
[&](can_frame frame) {
handler.handle(frame);
});
```

### J1939-Specific Handling

#### **How is J1939 Defined in DBCs**
DBC files indicate whether a message uses **Standard CAN** or **J1939** via the two lines below.
The first line defines the attribute itself. The second line is applied **per message** and
should appear once for each message that is intended to be treated as J1939, using that
message’s specific CAN ID.

```
BA_DEF_ BO_ "VFrameFormat" ENUM "StandardCAN","ExtendedCAN","reserved","J1939PG";
BA_ "VFrameFormat" BO_ 2364539904 3;
```

#### **J1939 Specific Constants**
The following will only be defined if a message is labeled as J1939 in the DBC:
- static constexpr uint32_t Pgn
- static constexpr uint8_t DefaultPriority = 3;
- static constexpr uint8_t SourceAddress = 37;
- static constexpr bool IsPduBroadcast = true;

The following will only be defined if the message is J1939 and it's of type PDU1 (destination specific):
- static constexpr uint8_t DefaultDestinationAddress

> Note: If you want to safely test if a message is of the J1939 Standard, use the variable `IsJ1939`.

#### **J1939 Logic Changes**
1. When passing a can frame into the explicit constructor, it will only check if the PGN of the incoming frame matches, instead of the whole CAN ID.

# Tests for dbc_gen_cpp

Since [`dbc_gen_cpp`](dbc_gen_cpp/) provides mostly functionality via the `install/` space with CMake functions and a Python package with importlib-registered Jinja templates, it's not possible to test the full usage of that package internally.

`test_dbc_gen_cpp` is fully dedicated to providing tests, it is not meant to be used as a dependency by any package.
55 changes: 55 additions & 0 deletions dbc_gen_cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# SPDX-FileCopyrightText: 2026 Polymath Robotics, Inc.
# SPDX-License-Identifier: Apache-2.0
cmake_minimum_required(VERSION 3.22)
project(dbc_gen_cpp)

if(NOT CMAKE_CXX_STANDARD)
set(CMAKE_CXX_STANDARD 17)
endif()
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
add_link_options(-Wl,-no-undefined)
endif()

find_package(ament_cmake REQUIRED)
find_package(ament_cmake_python REQUIRED)

ament_python_install_package(${PROJECT_NAME}
SCRIPTS_DESTINATION bin
)

add_library(${PROJECT_NAME} INTERFACE)
target_include_directories(${PROJECT_NAME} INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include/${PROJECT_NAME}>
)

install(
TARGETS ${PROJECT_NAME}
EXPORT ${PROJECT_NAME}Targets
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin
INCLUDES DESTINATION include
)
install(
EXPORT ${PROJECT_NAME}Targets
NAMESPACE ${PROJECT_NAME}::
DESTINATION share/${PROJECT_NAME}/cmake
)
install(
DIRECTORY include/
DESTINATION include
)
install(
DIRECTORY cmake
DESTINATION share/${PROJECT_NAME}
)

if(BUILD_TESTING)
endif()

ament_export_targets(${PROJECT_NAME}Targets HAS_LIBRARY_TARGET)
ament_package(
CONFIG_EXTRAS "dbc_gen_cpp-extras.cmake"
)
57 changes: 57 additions & 0 deletions dbc_gen_cpp/cmake/generate_dbc_cpp.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# SPDX-FileCopyrightText: 2026 Polymath Robotics, Inc.
# SPDX-License-Identifier: Apache-2.0
function(generate_dbc_cpp library_name)
find_package(Python3 REQUIRED COMPONENTS Interpreter)

set(one_value_args DBC)
cmake_parse_arguments(ARG "" "${one_value_args}" "" ${ARGN})
if(NOT ARG_DBC)
message(FATAL_ERROR "generate_dbc_cpp: Missing required keyword argument DBC")
endif()

set(gen_basedir ${CMAKE_CURRENT_BINARY_DIR}/dbc_gen_cpp)
set(gen_dir ${gen_basedir}/${library_name})

set(generated_c ${gen_dir}/${library_name}.c)
set(generated_h ${gen_dir}/${library_name}.h)
set(generated_hpp ${gen_dir}/${library_name}.hpp)
set(generated_files ${generated_c} ${generated_h} ${generated_hpp})

# Kind of awkward, but makes the sources get regenerated if the generator tool changes, by depending on the generator sources.
# Finds the python module directory via Python3 import
execute_process(
COMMAND ${Python3_EXECUTABLE} -c "import dbc_gen_cpp, os; print(os.path.dirname(dbc_gen_cpp.__file__))"
OUTPUT_VARIABLE GENERATOR_DIR
OUTPUT_STRIP_TRAILING_WHITESPACE
)
file(GLOB_RECURSE GENERATOR_SOURCES CONFIGURE_DEPENDS
"${GENERATOR_DIR}/*.py"
"${GENERATOR_DIR}/templates/*.j2"
)

# Generate the source files
add_custom_command(
OUTPUT ${generated_files}
COMMAND ${Python3_EXECUTABLE} -m dbc_gen_cpp ${ARG_DBC} -o ${gen_dir} -n ${library_name}
DEPENDS ${ARG_DBC} ${GENERATOR_SOURCES}
COMMENT "Generating C source from DBC ${ARG_DBC} with cantools"
VERBATIM
)
add_custom_target(${library_name}_c_sources
DEPENDS ${generated_c} ${generated_h}
)

# Create the library target from generated sources
add_library(${library_name} STATIC ${generated_c})
add_dependencies(${library_name} ${library_name}_c_sources)
target_link_libraries(${library_name} PUBLIC dbc_gen_cpp::dbc_gen_cpp)
target_include_directories(${library_name}
PUBLIC ${gen_basedir}
)

# Install the generated files to the install space, just in case they're included in public headers
install(
FILES ${generated_h} ${generated_hpp}
DESTINATION include/${library_name}
)
endfunction()
3 changes: 3 additions & 0 deletions dbc_gen_cpp/dbc_gen_cpp-extras.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2026 Polymath Robotics, Inc.
# SPDX-License-Identifier: Apache-2.0
include("${dbc_gen_cpp_DIR}/generate_dbc_cpp.cmake")
2 changes: 2 additions & 0 deletions dbc_gen_cpp/dbc_gen_cpp/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# SPDX-FileCopyrightText: 2026 Polymath Robotics, Inc.
# SPDX-License-Identifier: Apache-2.0
6 changes: 6 additions & 0 deletions dbc_gen_cpp/dbc_gen_cpp/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# SPDX-FileCopyrightText: 2026 Polymath Robotics, Inc.
# SPDX-License-Identifier: Apache-2.0
from .generate_cpp import main

if __name__ == '__main__':
main()
Loading
Loading