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
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,6 @@
[submodule "components/reflect_cpp/detail/reflect-cpp"]
path = components/reflect_cpp/detail/reflect-cpp
url = https://github.com/getml/reflect-cpp.git
[submodule "components/cdr/detail/cdr"]
path = components/cdr/detail/cdr
url = https://github.com/finger563/cdr.git
7 changes: 5 additions & 2 deletions components/cdr/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# The cdr library (detail/cdr, github.com/finger563/cdr) is header-only; its
# reflection backend comes from the reflect_cpp component. Interface-only
# registration, like the serialization component.
idf_component_register(
INCLUDE_DIRS "include"
SRC_DIRS "src")
INCLUDE_DIRS "detail/cdr/include" "include"
REQUIRES reflect_cpp)
79 changes: 48 additions & 31 deletions components/cdr/README.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,51 @@
# CDR (Common Data Representation) Component

[![Badge](https://components.espressif.com/components/espp/cdr/badge.svg)](https://components.espressif.com/components/espp/cdr)

The `cdr` component provides a small, standalone Common Data Representation
(CDR) reader/writer utility aimed at standards-oriented protocols such as
DDS/RTPS.

This initial slice is intentionally focused on the most immediately useful
pieces for building interoperable payloads:

- encapsulation identifiers for `CDR_BE`, `CDR_LE`, `PL_CDR_BE`, and `PL_CDR_LE`
- endian-aware primitive read/write helpers
- CDR alignment and padding handling
- string serialization helpers using the standard CDR length-prefix + null terminator format
- headerless/body helpers for CDR fields embedded inside larger protocol elements
- fixed-array helpers and zero-copy payload/span views
- sequence helpers for homogeneous primitive collections
- standalone usage without depending on RTPS or DDS layers

Current scope:

- good fit for building RTPS payloads and parameter lists incrementally
- designed to stay reusable outside DDS/RTPS
- **not** yet a full DDS XTypes / XCDR2 implementation
# CDR (Common Data Representation)

Reflection-driven CDR/XCDR serialization for plain C++ structs — no IDL
compiler, no hand-written read/write call sequences. The compiler generates
the serialization code from the struct definition itself (the same usability
pattern as the `serialization` component's alpaca, with a DDS/RTPS-compatible
wire format instead).

The library is [finger563/cdr](https://github.com/finger563/cdr), vendored as
a git submodule under `detail/`; this component wires it into ESP-IDF and
depends on the `reflect_cpp` component for its reflection backend. See the
library's `README.md` for the supported type mapping and
`docs/DESIGN.md` for the architecture and wire-format rules.

```cpp
struct ImuSample {
uint64_t stamp_us;
std::array<float, 3> accel;
std::array<float, 3> gyro;
float temperature;
};

auto bytes = cdr::serialize(sample); // XCDR2, appendable (default)
auto ros2 = cdr::serialize<cdr::xcdr1>(sample); // ROS 2 / classic-CDR peers
auto back = cdr::deserialize<ImuSample>(*bytes); // std::expected<ImuSample, cdr::error>
```

Highlights:

- XCDR1 (plain CDR — ROS 2, CycloneDDS defaults) and XCDR2 (plain +
delimited/appendable with DHEADER — FastDDS, OpenDDS defaults), both
endiannesses, with appendable schema evolution in both directions.
- Wire format byte-verified against pycdr2 (CycloneDDS's codec); the Python
side of a message is a plain `pycdr2` dataclass.
- `std::expected` error handling with error code, payload offset, and field
name; bounds-checked, fuzz-tested deserializers.
- `cdr::param_list_writer` / `param_list_reader` for the PL_CDR parameter
lists RTPS discovery (SPDP/SEDP) uses.
- Zero-allocation `cdr::serialize_into` and body-only variants for RTPS
submessage composition.

Requires C++23 (`std::expected`), the default on ESP-IDF 5.2+ toolchains.

This component replaces the previous manual `espp::CdrWriter`/`espp::CdrReader`
API (an imperative XCDR1-only reader/writer); the RTPS component and examples
have been migrated to the reflection-driven API.

## Example

The [example](./example) demonstrates a small round-trip using:

- a little-endian CDR encapsulation header
- primitive values
- a CDR string
- a `uint16_t` sequence
The [example](./example) shows struct round-trips in both XCDR versions, the
zero-allocation path, error handling, and PL_CDR parameter lists.
1 change: 1 addition & 0 deletions components/cdr/detail/cdr
Submodule cdr added at 15d5aa
4 changes: 2 additions & 2 deletions components/cdr/example/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ set(EXTRA_COMPONENT_DIRS

set(
COMPONENTS
"main esptool_py cdr logger"
"main esptool_py cdr reflect_cpp logger"
CACHE STRING
"List of components to include"
)

project(cdr_example)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD 23)
24 changes: 14 additions & 10 deletions components/cdr/example/README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
# CDR Example

This example demonstrates a small CDR round-trip using the `cdr` component.
This example demonstrates the reflection-driven `cdr` component: the compiler
generates CDR serialization code directly from a plain struct definition.

It exercises:

- a little-endian CDR encapsulation header
- primitive value serialization
- CDR string serialization
- `uint16_t` sequence serialization
- fixed-array serialization with `write_array` / `read_array`
- headerless/body CDR helpers for embedding fields inside a larger protocol value
- round-trip parsing with `CdrReader`
- struct serialization to XCDR2 (appendable, the FastDDS/OpenDDS default) and
XCDR1 (plain CDR, the ROS 2 / CycloneDDS default)
- deserialization driven by the received encapsulation header (version and
endianness)
- `std::expected` error handling with error code, payload offset, and field
name
- the zero-allocation `cdr::serialize_into` path and `cdr::serialized_size`
- PL_CDR parameter-list writing and reading (the encoding RTPS SPDP/SEDP
discovery uses)

## How to use example

Expand All @@ -26,5 +29,6 @@ Replace `PORT` with the name of the serial port to use.

## Expected Output

The example logs the encoded byte count and the decoded values. It finishes by
printing `CDR round-trip succeeded`.
The example logs the serialized sizes for both XCDR versions, the round-tripped
field values, the zero-allocation write size, and the parameters found in the
PL_CDR parameter list, finishing with `example complete`.
116 changes: 68 additions & 48 deletions components/cdr/example/main/cdr_example.cpp
Original file line number Diff line number Diff line change
@@ -1,68 +1,88 @@
#include <algorithm>
#include <array>
#include <string>
#include <vector>

#include "cdr.hpp"
#include "logger.hpp"

extern "C" void app_main(void) {
espp::Logger logger({.tag = "cdr_example", .level = espp::Logger::Verbosity::INFO});

std::array<uint8_t, 4> input_magic{'C', 'D', 'R', '!'};
std::array<uint16_t, 3> input_values{10, 20, 30};

//! [cdr example]
espp::CdrWriter writer({
.encapsulation = espp::CdrEncapsulation::CDR_LE,
.include_encapsulation = true,
});
writer.write<uint32_t>(42);
writer.write<float>(3.25f);
writer.write_string("hello cdr");
writer.write_sequence<uint16_t>(input_values);
namespace {

auto payload = writer.take_buffer();
logger.info("Serialized {} bytes of CDR data", payload.size());
// A plain aggregate is all it takes — the compiler generates the
// serialization code from the struct definition itself. Types default to
// @appendable extensibility (XTypes default); opt into @final with
// `static constexpr auto cdr_extensibility = cdr::extensibility::final;`.
struct ImuSample {
uint64_t stamp_us{};
std::array<float, 3> accel{};
std::array<float, 3> gyro{};
float temperature{};
std::string frame_id{};
};

auto inline_writer = espp::CdrWriter::make_body_writer(espp::CdrEncapsulation::CDR_LE);
inline_writer.write_array(input_magic);
inline_writer.write_string("embedded field");
auto inline_payload =
espp::CdrWriter::encapsulate(inline_writer.payload(), espp::CdrEncapsulation::PL_CDR_LE);
} // namespace

espp::CdrReader reader(payload);
espp::CdrReader inline_reader(inline_payload);
uint32_t decoded_count = 0;
float decoded_scale = 0.0f;
std::string decoded_text;
std::vector<uint16_t> decoded_values;
std::array<uint8_t, 4> decoded_magic{};
std::string decoded_inline_text;
extern "C" void app_main(void) {
espp::Logger logger({.tag = "cdr_example", .level = espp::Logger::Verbosity::INFO});

bool ok = reader.read<uint32_t>(decoded_count) && reader.read<float>(decoded_scale) &&
reader.read_string(decoded_text) && reader.read_sequence<uint16_t>(decoded_values);
bool inline_ok = inline_reader.encapsulation() == espp::CdrEncapsulation::PL_CDR_LE &&
inline_reader.read_array(decoded_magic) &&
inline_reader.read_string(decoded_inline_text);
//! [cdr example]
const ImuSample sample{
.stamp_us = 123456789,
.accel = {0.0f, 0.0f, 9.81f},
.gyro = {0.01f, -0.02f, 0.0f},
.temperature = 25.5f,
.frame_id = "imu_link",
};

if (!ok || !inline_ok) {
logger.error("Failed to decode CDR payload");
// XCDR2 (appendable): what FastDDS / OpenDDS speak by default.
auto bytes = cdr::serialize(sample);
// XCDR1 (plain CDR): what ROS 2 and CycloneDDS speak by default.
auto ros2_bytes = cdr::serialize<cdr::xcdr1>(sample);
if (!bytes) {
logger.error("XCDR2 serialize failed: {}", cdr::to_string(bytes.error().code));
return;
}
if (!ros2_bytes) {
logger.error("XCDR1 serialize failed: {}", cdr::to_string(ros2_bytes.error().code));
return;
}
logger.info("ImuSample: {} bytes as XCDR2, {} bytes as XCDR1 (ROS 2)", bytes->size(),
ros2_bytes->size());

logger.info("Decoded count={}, scale={:.2f}, text='{}', sequence size={}, embedded='{}'",
decoded_count, decoded_scale, decoded_text, decoded_values.size(),
decoded_inline_text);

if (decoded_count != 42 || decoded_scale != 3.25f || decoded_text != "hello cdr" ||
decoded_values.size() != input_values.size() ||
!std::equal(decoded_values.begin(), decoded_values.end(), input_values.begin()) ||
decoded_magic != input_magic || decoded_inline_text != "embedded field") {
logger.error("CDR round-trip mismatch");
// Deserialize picks version + endianness from the encapsulation header and
// returns std::expected — errors carry a code, payload offset, and the
// field that failed.
auto restored = cdr::deserialize<ImuSample>(*bytes);
if (!restored) {
logger.error("deserialize failed at {} offset {} in field '{}'",
cdr::to_string(restored.error().code), restored.error().offset,
restored.error().field);
return;
}
logger.info("round-trip ok: stamp={} frame='{}' accel.z={}", restored->stamp_us,
restored->frame_id, restored->accel[2]);

// Zero-allocation path for hot loops: serialize into a fixed buffer.
std::array<std::byte, 128> fixed{};
if (auto n = cdr::serialize_into(sample, fixed)) {
logger.info("serialize_into wrote {} bytes (exact size query: {})", *n,
cdr::serialized_size(sample));
}

// PL_CDR parameter lists — the encoding RTPS discovery (SPDP/SEDP) uses.
std::vector<std::byte> pl_buf;
cdr::param_list_writer pl(pl_buf);
pl.add(uint16_t{0x0050}, std::array<uint8_t, 16>{1, 2, 3, 4}); // PID_PARTICIPANT_GUID
pl.add(uint16_t{0x0062}, std::string("esp32_node")); // PID_ENTITY_NAME
if (pl.finish()) {
auto reader = cdr::param_list_reader::from_encapsulated(pl_buf);
while (reader) {
auto param = reader->next();
if (!param || !*param)
break;
logger.info(" parameter pid=0x{:04x} ({} bytes)", (*param)->pid, (*param)->value.size());
}
}
//! [cdr example]

logger.info("CDR round-trip succeeded");
logger.info("example complete");
}
8 changes: 6 additions & 2 deletions components/cdr/idf_component.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
## IDF Component Manager Manifest File
license: "MIT"
description: "Common Data Representation (CDR) read/write helpers for ESP-IDF and cross-platform use."
description: "Reflection-driven CDR/XCDR serialization for plain C++ structs (no IDL compiler) — wire-compatible with DDS/RTPS, byte-verified against CycloneDDS."
url: "https://github.com/esp-cpp/espp/tree/main/components/cdr"
repository: "git://github.com/esp-cpp/espp.git"
maintainers:
Expand All @@ -14,8 +14,12 @@ tags:
- CDR
- DDS
- RTPS
- ROS2
- Serialization
- XCDR
- Reflection
dependencies:
idf:
version: '>=5.0'
version: '>=5.2'
espp/reflect_cpp:
version: '>=1.0'
Loading