Skip to content

feat(rtps_embedded): Phase 4 — limit profiles, dynamic host memory, DATA_FRAG large payloads - #710

Open
finger563 wants to merge 9 commits into
mainfrom
feat/rtps-phase4-limits
Open

feat(rtps_embedded): Phase 4 — limit profiles, dynamic host memory, DATA_FRAG large payloads#710
finger563 wants to merge 9 commits into
mainfrom
feat/rtps-phase4-limits

Conversation

@finger563

Copy link
Copy Markdown
Contributor

Phase 4 of the components/rtps_embedded refactor — see REFACTOR_PLAN.md. Makes the RTPS component scale from tiny MCU nodes to compute hosts (Jetson/laptop) that interoperate on the same DDS network: per-platform limits, dynamic memory on host, and large-payload (>64KB) fragmentation.

What's in it (4 commits, each interop-gated)

  1. Per-platform limit profilesembedded / host (relaxed default) / host_large, selected by a RTPS_LIMITS_PROFILE CMake option + ESP Kconfig on the existing RTPS_CONFIG_HEADER switch. config_esp32.hpp is byte-identical (MCU determinism preserved).
  2. DataSize_t → uint32 — internal sample-size type widened so a sample can exceed 64KB (fixes a silent PayloadBuffer::spaceUsed() truncation). Wire-neutral: on-wire length fields stay 16-bit; golden byte-identical.
  3. Dynamic value-pool storage — a compile-time StorageArray<T,N> policy (RTPS_STORAGE_DYNAMIC): host builds allocate on the heap and grow past the profile cap (std::deque, chosen because CacheChange isn't move-constructible); esp32 stays fully static (std::array, zero heap, RTPS_STORAGE_DYNAMIC absent from its compile DB). Entity pools stay static (Participant is non-movable).
  4. Best-effort DATA_FRAG — samples >64KB fragment on send and reassemble on receive, interoperating with FastDDS / ROS 2 both directions. Config: WriterConfig::fragment_size (default 63000, configurable down to <MTU), Config::max_sample_size (host 8MB / esp32 256KB). RTPS_ENABLE_FRAGMENTATION is always-on for host, opt-in (default off) on esp32 so the MCU pays nothing for the non-frag path it uses today. Reliable fragment recovery (HEARTBEAT_FRAG/NACK_FRAG) is a tracked v2.

Verification

  • Golden wire tests: existing sections byte-identical (never regenerated) under both host and embedded profiles; new data_frag section pins the fragment encoding.
  • Docker interop matrix: 12/12 — incl. a 200KB byte-exact espp↔ROS 2 leg both directions (FastDDS reassembles espp's fragments; espp reassembles FastDDS's), plus a >64KB espp↔espp loopback.
  • esp32: builds both frag-off (default — no frag code compiled) and frag-on (xtensa).
  • Notable interop detail: FastDDS pads the last fragment's serializedData to 4-byte alignment, so reassembly clamps the copy to sampleSize rather than rejecting the final fragment.

API impact

Additive: existing RtpsParticipant/typed API unchanged; new optional WriterConfig::fragment_size and Config::max_sample_size with sensible defaults. max_payload_size rises to max_sample_size when fragmentation is enabled.

🤖 Generated with Claude Code

finger563 and others added 4 commits August 12, 2026 21:47
Restructure the compile-time limits into named, build-time-selectable profiles
so one codebase serves both tiny MCU nodes and compute hosts, keeping the fully
static allocation model:

- embedded (config_esp32.hpp, unchanged/byte-identical): tight MCU caps.
- host (config_desktop.hpp, relaxed; the default non-ESP profile): sensible
  capacity for a compute host out of the box (e.g. MAX_NUM_PARTICIPANTS 2->8,
  stateful writers/readers ->32, unmatched remotes ->256/128). MAX_NUM_UNMATCHED
  remote writer/reader counts widened from uint8_t to uint16_t (values exceed
  255).
- host_large (config_host_large.hpp, new): larger caps for big DDS graphs.
- Selected via a CMake RTPS_LIMITS_PROFILE option (embedded|host|host_large,
  default host on non-ESP) that drives the existing RTPS_CONFIG_HEADER switch,
  plus an ESP Kconfig choice (default embedded).

Wire-neutral: limits cap capacity, not encoding. Gates: golden byte-identical
under host and host_large; engine + facade + typed loopbacks PASS; interop 8/8;
esp32 builds with the default embedded profile (config_esp32.hpp untouched).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…FRAG slice A)

Sample-size type DataSize_t was uint16_t, capping a whole sample at 64 KB and
silently truncating PayloadBuffer::spaceUsed() above that. Widen it to uint32_t
so a sample can exceed 64 KB internally (prerequisite for DATA_FRAG). Wire-format
neutral - only the internal type widens; on-the-wire length fields stay 16-bit:

- common/types.hpp: DataSize_t uint16_t -> uint32_t (the ~38 references are
  typed params/returns and widen automatically; spaceUsed() now holds >64 KB).
- MessageFactory.hpp: the one place a size feeds a 16-bit wire field
  (octetsToNextHeader for a non-fragmented DATA) is explicitly narrowed with
  static_cast<uint16_t> + comment (a single unfragmented DATA is <64 KB, so the
  narrowing is correct).
- rtps_participant: max_payload_size stays 65535 (one unfragmented DATA
  submessage is still bounded by the 16-bit octetsToNextHeader); the static_assert
  now checks against uint16_t max (raised when DATA_FRAG lands).

Gates: golden byte-identical (goldens never regenerated - proves wire neutrality
of the widening); engine + facade + typed loopbacks PASS; docker interop 8/8 PASS
vs ROS 2; esp32 example builds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ep 3)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 13, 2026 14:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown

✅Static analysis result - no issues found! ✅

@finger563
finger563 requested review from guo-max and a balanced review from Copilot August 13, 2026 14:54
@finger563 finger563 self-assigned this Aug 13, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

@finger563 finger563 added enhancement New feature or request rtps real time publish subscribe labels Aug 13, 2026
A `git add -A` fallback in an earlier commit swept up two local-only,
generated artifacts that are not part of the repo:
- docs/ : the Doxygen/Sphinx documentation OUTPUT (built from doc/); ~785 files.
- build_examples.sh : a local example-build helper.

Remove both from tracking (files kept on disk) and add them to .gitignore so
they cannot be committed again. Neither exists on main; with squash-merge the
net result is that they never land.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 35 out of 36 changed files in this pull request and generated 6 comments.

Suppressed comments (5)

components/rtps_embedded/src/entities/StatefulWriter.cpp:350

  • This clamps a DATA_FRAG payload using the DATA limit. DATA_FRAG has a 36-byte raw header rather than DATA's 24-byte header, so its maximum fragment payload is 65,439 bytes, not MAX_UNFRAGMENTED_PAYLOAD (65,451). A configured size in that 12-byte gap creates a UDP payload larger than 65,507 bytes and sendPacket cannot transmit it. Introduce a DATA_FRAG-specific maximum and clamp against it.
  const uint16_t fragmentSize = m_fragmentSize > MAX_UNFRAGMENTED_PAYLOAD
                                    ? static_cast<uint16_t>(MAX_UNFRAGMENTED_PAYLOAD)
                                    : m_fragmentSize;

components/rtps_embedded/src/entities/StatelessWriter.cpp:225

  • This clamps a DATA_FRAG payload using the DATA limit. DATA_FRAG has a 36-byte raw header rather than DATA's 24-byte header, so its maximum fragment payload is 65,439 bytes, not MAX_UNFRAGMENTED_PAYLOAD (65,451). A configured size in that 12-byte gap creates a UDP payload larger than 65,507 bytes and sendPacket cannot transmit it. Introduce a DATA_FRAG-specific maximum and clamp against it.
  const uint16_t fragmentSize = m_fragmentSize > MAX_UNFRAGMENTED_PAYLOAD
                                    ? static_cast<uint16_t>(MAX_UNFRAGMENTED_PAYLOAD)
                                    : m_fragmentSize;

components/rtps_embedded/src/entities/Reader.cpp:52

  • A delayed fragment from an older sequence number is treated as a new sample and evicts a newer in-progress reassembly. UDP reordering across consecutive fragmented samples can therefore make both samples fail repeatedly, contrary to the stated “newer sample evicts older” policy. Ignore older sequence numbers from the same writer while a newer sample is active.
    const bool sameSample =
        m_reassembly.active && m_reassembly.writerGuid == writerGuid && m_reassembly.sn == sn;
    if (!sameSample) {
      m_reassembly.active = true;

components/rtps_embedded/CMakeLists.txt:35

  • These ESP Kconfig selections include headers that unconditionally define RTPS_STORAGE_DYNAMIC (config_desktop.hpp:40 and config_host_large.hpp:40). Selecting either “host” profile on ESP therefore switches value pools to heap-backed std::deque, despite the Kconfig promise that all profiles remain fully static and the PR's MCU determinism guarantee. Separate limits selection from storage policy, or prevent these dynamic headers from enabling heap storage on ESP.
if(CONFIG_RTPS_LIMITS_PROFILE_HOST)
  target_compile_definitions(${COMPONENT_LIB} PUBLIC RTPS_CONFIG_HEADER="rtps/config_desktop.hpp")
elseif(CONFIG_RTPS_LIMITS_PROFILE_HOST_LARGE)
  target_compile_definitions(${COMPONENT_LIB} PUBLIC RTPS_CONFIG_HEADER="rtps/config_host_large.hpp")

components/rtps_embedded/interop/run_interop.sh:134

  • This gate does not verify the claimed byte-exact 200 KB payload: any successful YAML output over 150 KB passes, including a truncated or corrupted sample. Replace the size heuristic with a subscriber/check that compares the received string against the deterministic 200,000-byte pattern, as the reverse-direction gate already does.
# Require the echo to succeed AND to have carried a large (fragment-reassembled)
# payload (>150 KB of YAML), proving the 200 KB sample crossed intact.
big1_rc=1
if [ "$echobig_rc" -eq 0 ] && [ "$(wc -c < /tmp/echobig.log)" -gt 150000 ]; then big1_rc=0; fi

Comment thread components/rtps_embedded/include/rtps_participant.hpp Outdated
Comment thread components/rtps_embedded/src/entities/Reader.cpp Outdated
Comment thread components/rtps_embedded/src/rtps_participant.cpp
finger563 and others added 2 commits August 13, 2026 10:14
- MemoryPool::getSize() only calls the const capacity(), so make it const
  (cppcheck functionConst).
- rtps_facade_frag test: in the else branch `match` is false by construction, so
  `match ? 1 : 0` is a known-false condition; print byte-exact=0 directly
  (cppcheck knownConditionTrueFalse).

Gates: cppcheck clean on both files; frag loopback byte-exact (no regression);
golden byte-identical.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Six correctness/hardening fixes (Copilot):

- max_payload_size (no-frag): was 65535, which overflows the UDP datagram and
  wraps the 16-bit submessage length. Use 65451 = the engine's
  MAX_UNFRAGMENTED_PAYLOAD (65507 UDP - 20 RTPS hdr - 12 INFO_TS - 24 DATA hdr);
  static_assert keeps the facade constant in sync with the engine.
- Reader::newFragment: reject fragmentsInSubmessage == 0 and require the
  serialized data to cover all advertised fragments (fragmentSize each, last
  fragment ends at sampleSize; trailing 4-byte alignment padding allowed) before
  copying/marking - a short DATA_FRAG must not mark missing ranges complete and
  deliver zero-filled bytes.
- SimpleHistoryCache / HistoryCacheWithDeletion / ThreadSafeCircularBuffer:
  the dynamic ring indices are 16-bit; refuse to grow past uint16 max (the
  m_head/m_tail casts would truncate and corrupt the ring). The buffer then
  behaves as full (history: drop-oldest; queue: grow() returns false, insert
  fails).
- add_writer: reject fragment_size == 0 up front (the fragmented send paths treat
  it as failure, so publish() would report success while silently dropping every
  large sample).

Gates: golden byte-identical; loopbacks PASS; docker interop 12/12 (incl. 200 KB
ROS 2 both directions); esp32 builds frag-off and frag-on.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@finger563

Copy link
Copy Markdown
Contributor Author

Addressed all review comments in cbc8ed4:

  • max_payload_size too high (rtps_participant.hpp): the no-frag cap was 65535, which overflows the UDP datagram and wraps the 16-bit octetsToNextHeader. Now 65451 = the engine's MAX_UNFRAGMENTED_PAYLOAD (65507 UDP − 20 RTPS − 12 INFO_TS − 24 DATA), with a static_assert keeping the two in sync.
  • Reader::newFragment — unverified packed fragments: now rejects fragmentsInSubmessage == 0 and requires the serialized data to actually cover all advertised fragments (fragmentSize each, last ends at sampleSize; trailing 4-byte alignment padding allowed) before copying/marking — a short DATA_FRAG can no longer mark missing ranges complete and deliver zero-filled bytes.
  • 16-bit ring indices vs dynamic growth (SimpleHistoryCache / HistoryCacheWithDeletion / ThreadSafeCircularBuffer): grow() now refuses to grow past uint16 max (the m_head/m_tail casts would truncate and corrupt the ring). The buffer then behaves as full — history drops-oldest, the queue's grow() returns false and the insert fails cleanly.
  • fragment_size == 0 accepted (add_writer): now rejected up front, since the fragmented send paths treat 0 as failure and publish() would otherwise report success while silently dropping every large sample.

Verified: golden byte-identical; engine + facade + typed + >64KB frag loopbacks PASS; docker interop 12/12 (incl. both 200 KB ROS 2 directions — the reader validation preserves FastDDS's alignment-padded final fragment); esp32 builds frag-off (the static_assert) and frag-on; cppcheck clean on the changed files (no new findings).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 35 out of 36 changed files in this pull request and generated 2 comments.

Suppressed comments (7)

components/rtps_embedded/include/rtps/storages/MemoryPool.hpp:117

  • The new growth path is bypassed for the unmatched-endpoint pools: SEDPAgent.cpp:109-124 returns as soon as isFull() is true, so add() is never called at the profile cap. Host discovery therefore still drops unmatched writers/readers instead of growing as promised. Remove those caller-side prechecks (and handle add()'s result), or otherwise make the full check trigger growth.
#ifdef RTPS_STORAGE_DYNAMIC
      // Host: grow past the profile cap instead of hard-failing.
      grow();

components/rtps_embedded/src/entities/StatelessWriter.cpp:225

  • MAX_UNFRAGMENTED_PAYLOAD accounts for the 24-byte DATA header, but DATA_FRAG has a 36-byte fixed header. Clamping to this value permits a 65,519-byte UDP payload (20 + 12 + 36 + 65,451), exceeding UDP's 65,507-byte maximum, so large configured fragment sizes are silently unsendable. Clamp using the DATA_FRAG overhead instead.
  const uint16_t fragmentSize = m_fragmentSize > MAX_UNFRAGMENTED_PAYLOAD
                                    ? static_cast<uint16_t>(MAX_UNFRAGMENTED_PAYLOAD)
                                    : m_fragmentSize;

components/rtps_embedded/src/entities/StatefulWriter.cpp:350

  • This uses the unfragmented DATA limit even though DATA_FRAG has 12 more bytes of fixed overhead. A configured size above 65,439 is clamped to 65,451 and produces a 65,519-byte UDP payload, which exceeds the 65,507-byte protocol maximum and cannot be sent. Derive the clamp from the DATA_FRAG header size.
  const uint16_t fragmentSize = m_fragmentSize > MAX_UNFRAGMENTED_PAYLOAD
                                    ? static_cast<uint16_t>(MAX_UNFRAGMENTED_PAYLOAD)
                                    : m_fragmentSize;

components/rtps_embedded/interop/run_interop.sh:134

  • This gate verifies only that ROS produced more than 150 KB of YAML, not that the received 200 KB pattern is byte-exact. A truncated or corrupted payload above that threshold still passes, contradicting the PR's stated byte-exact espp→ROS verification. Use a ROS subscriber that compares msg.data against the deterministic pattern, as the reverse-direction espp subscriber does.
# Require the echo to succeed AND to have carried a large (fragment-reassembled)
# payload (>150 KB of YAML), proving the 200 KB sample crossed intact.
big1_rc=1
if [ "$echobig_rc" -eq 0 ] && [ "$(wc -c < /tmp/echobig.log)" -gt 150000 ]; then big1_rc=0; fi

components/rtps_embedded/Kconfig:10

  • This help text says every profile is fully static, but the host and host_large headers define RTPS_STORAGE_DYNAMIC, making value pools heap-backed and growable. That is especially misleading in ESP menuconfig, where selecting either profile introduces dynamic allocation. Describe the embedded/static versus host/dynamic distinction explicitly.
            Selects the compile-time capacity limits (profile header) used by the
            rtps_embedded engine. All profiles use the same fully-static,
            deterministic allocation model; only the capacity caps differ. These
            caps are pure capacity limits and do NOT change any bytes on the wire.

pc/tests/rtps_embedded_interop_pub.cpp:9

  • The usage text omits the newly parsed ninth fragment_size argument, so users cannot discover how to override the fragment size from this CLI's own documentation.
    components/rtps_embedded/include/rtps_participant.hpp:152
  • The PR describes an additive optional RtpsParticipant::Config::max_sample_size, but this implementation exposes only a compile-time RTPS_MAX_SAMPLE_SIZE/max_payload_size; the public Config has no such field and callers cannot configure the cap per participant. Either implement and enforce the advertised option on publish/reassembly or update the stated API impact to make the compile-time-only design explicit.
#if defined(RTPS_ENABLE_FRAGMENTATION)
  static constexpr std::size_t max_payload_size = RTPS_MAX_SAMPLE_SIZE;
#else

…s (PR #710)

Fix a coordination bug the dynamic value pools (Phase 4 Step 2) introduced, and
add regression tests for the review-flagged hazards.

Bug: StatelessWriter/StatefulWriter::newChange() advance m_nextSequenceNumberToSend
on m_history.isFull(), assuming the next addChange drops the oldest sample. That
holds for the static ring, but the dynamic (host) storage path GROWS and retains
the sample instead - so the writer skipped retained samples and never sent them
under backlog. Guard the drop/cursor-advance on the static path
(#ifndef RTPS_STORAGE_DYNAMIC); on the dynamic path nothing is dropped, so the
cursor must not advance. esp32 (static) behavior is unchanged.

Regression tests (docker-free, in pc/tests; the plain loopbacks never backlog):
- rtps_facade_backlog: a reliable writer publishes 300 samples (>> history depth)
  so the history backlogs + grows; the subscriber must receive ALL 300 with no
  gaps (a skipped/never-sent sample shows as a missing sequence number - the
  exact symptom of the bug). Also asserts add_writer rejects fragment_size == 0.
- rtps_facade_frag_sizes: DATA_FRAG reassembly with a NON-exact final fragment
  (70003 and 205003 B @ frag 8000 -> short last fragments), which the existing
  rtps_facade_frag (exact 25x8000) never exercises. Guards Reader::newFragment
  fragment-length handling.

interop harness: builds both new tests (compile guard) and runs backlog as a CI
leg (non-fragmented -> robust in the container). Gates: golden byte-identical;
docker interop 13/13 (backlog_no_skip included); esp32 builds frag-off/on.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@finger563

Copy link
Copy Markdown
Contributor Author

Addressed the latest review comments (commit c9a33a2).

Writer cursor-advance vs dynamic-growth bug (the substantive one).
StatelessWriter/StatefulWriter::newChange() advanced m_nextSequenceNumberToSend whenever m_history.isFull(), on the assumption that the next addChange overwrites (drops) the oldest sample. That's true for the static ring (esp32), but the Phase 4 dynamic (host) storage path grows and retains the sample instead — so the writer skipped retained samples and never sent them under backlog. Fixed by guarding the drop/cursor-advance on the static path only (#ifndef RTPS_STORAGE_DYNAMIC); on the dynamic path nothing is dropped, so the cursor must not advance. esp32 behavior is unchanged.

New regression tests (docker-free, in pc/tests — the plain loopback tests never backlog the writer history, so they couldn't catch either hazard):

  • rtps_facade_backlog — a reliable writer publishes 300 samples (≫ history depth), forcing the grow/backlog path; the subscriber must receive all 300 with no gaps (a skipped/never-sent sample shows up as a missing sequence number — the exact symptom of the bug). Also asserts add_writer rejects fragment_size == 0.
  • rtps_facade_frag_sizes — DATA_FRAG reassembly with a non-exact final fragment (70003 and 205003 B @ frag 8000 → short last fragments), which the existing rtps_facade_frag (exact 25×8000) never exercises. Guards Reader::newFragment fragment-length handling.

The interop harness builds both new tests and runs backlog_no_skip as a CI leg.

The re-listed older comments (16-bit index overflow guards, fragment_size == 0, max_payload_size = 65451, Reader.cpp last-fragment clamp) were already fixed in cbc8ed4.

Gates: golden wire test byte-identical; docker ROS 2 interop 13/13 (incl. backlog_no_skip, 200KB espp↔ROS 2 both directions); esp32 builds green with fragmentation off and on; cppcheck clean.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 37 out of 38 changed files in this pull request and generated 2 comments.

Suppressed comments (5)

components/rtps_embedded/Kconfig:23

  • This ESP-IDF option selects config_desktop.hpp, which unconditionally defines RTPS_STORAGE_DYNAMIC; the same applies to host_large. Thus choosing either relaxed profile on ESP compiles deque-backed, growable heap storage, contrary to this Kconfig menu's claim that all profiles remain fully static and the PR's MCU determinism guarantee. Either make these choices unavailable on ESP or separate the limits profile from the storage policy so ESP always keeps static storage.
        config RTPS_LIMITS_PROFILE_HOST
            bool "host (relaxed static caps)"
            help
                Relaxed static caps suitable for small-to-medium DDS graphs
                (rtps/config_desktop.hpp). Larger footprint than embedded.

components/rtps_embedded/include/rtps/entities/Reader.hpp:162

  • A reader may match multiple remote writers, but this single global slot evicts an incomplete sample whenever a fragment from another (writerGuid, sn) arrives. Interleaved DATA_FRAG streams from two publishers can therefore continually discard each other and never deliver either sample. Keep bounded reassembly state per writer/sample (with an explicit eviction limit) instead of one slot for the whole reader.
  // Single best-effort reassembly slot: accumulates the fragments of one sample
  // at a time (a newer sample or different writer evicts an older incomplete
  // one). Bounded by Config::MAX_SAMPLE_SIZE.

components/rtps_embedded/include/rtps_participant.hpp:81

  • The new public fragmentation option and profile behavior are not reflected in the component documentation. components/rtps_embedded/README.md:86-116 still documents only two fixed config headers/old limits, while doc/en/protocols/rtps.rst:22-60 still says ROS 2 data exchange and reliable endpoints are unsupported. Update those user-facing docs with the new profiles, payload limits, and DATA_FRAG scope.
    /// Nominal per-fragment payload size (bytes) used when a published sample is
    /// too large for a single DATA submessage and is split into DATA_FRAG
    /// submessages. Default 63000 (large: fewer fragments). Lower it toward the
    /// path MTU (e.g. ~1400) for lossy links. Only relevant when fragmentation is
    /// compiled in (always on host; opt-in on ESP32). Ignored for samples that
    /// fit a single DATA submessage.
    uint16_t fragment_size{63000};

pc/tests/rtps_embedded_interop_pub.cpp:9

  • The publisher now parses a ninth fragment_size argument, but the usage line omits it, so the interop harness interface is incomplete.
    components/rtps_embedded/interop/run_interop.sh:142
  • This gate only checks that the YAML output exceeds 150 KB; it does not verify the advertised 200 KB length or any byte content. A truncated or corrupted payload can therefore pass, so this does not support the PR's claim of byte-exact large-payload interop in both directions. Parse/compare the echoed data against the deterministic 200,000-byte pattern (or use a ROS subscriber that performs that comparison) before recording success.
# Require the echo to succeed AND to have carried a large (fragment-reassembled)
# payload (>150 KB of YAML), proving the 200 KB sample crossed intact.
big1_rc=1
if [ "$echobig_rc" -eq 0 ] && [ "$(wc -c < /tmp/echobig.log)" -gt 150000 ]; then big1_rc=0; fi

Comment thread components/rtps_embedded/src/entities/StatelessWriter.cpp Outdated
Comment thread components/rtps_embedded/src/entities/StatefulWriter.cpp Outdated
… (PR #710)

DATA_FRAG's fixed submessage header is 36 bytes vs DATA's 24, so a per-fragment
payload must be 12 bytes smaller than a single-DATA payload to keep the datagram
within UDP's 65507-byte limit. Both writers clamped a configured fragment_size to
MAX_UNFRAGMENTED_PAYLOAD (65451); a size in 65440..65451 then built a datagram of
up to 65519 bytes (20 RTPS hdr + 12 INFO_TS + 36 DATA_FRAG hdr + 65451), whose
send silently fails (sendPacket discards the error) - losing every large sample.

Add rtps::MAX_FRAGMENT_SIZE, derived symbolically from SubmessageDataFrag::
getRawSize() the same way MAX_UNFRAGMENTED_PAYLOAD is derived from SubmessageData
(= 65439), and clamp both StatelessWriter and StatefulWriter fragment paths to it.
Being constexpr-derived, it auto-recomputes if any header layout changes. A
maximal single-fragment datagram is now exactly 65507 bytes (fits).

Gates: golden byte-identical; docker ROS 2 interop 13/13 (incl. 200KB both
directions + backlog_no_skip); esp32 builds frag-on.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@finger563

Copy link
Copy Markdown
Contributor Author

Addressed the DATA_FRAG fragment-size clamp comments (commit 356fa19).

Both writers clamped a configured fragment_size to MAX_UNFRAGMENTED_PAYLOAD (65451), but DATA_FRAG's fixed submessage header is 36 bytes vs DATA's 24, so a per-fragment payload must be 12 bytes smaller. A configured size in 65440..65451 would therefore build a datagram of up to 65519 bytes (20 RTPS hdr + 12 INFO_TS + 36 DATA_FRAG hdr + 65451 payload), exceeding UDP's 65507-byte limit — and since sendPacket() discards the send error, every large sample would be silently lost.

Fix: added rtps::MAX_FRAGMENT_SIZE, derived symbolically from SubmessageDataFrag::getRawSize() exactly the way MAX_UNFRAGMENTED_PAYLOAD is derived from SubmessageData (= 65439), and clamp both the StatelessWriter and StatefulWriter fragment paths to it. Because it's constexpr-derived from the actual header layout, it auto-recomputes if any submessage size ever changes — a stronger guard than a hardcoded literal. A maximal single-fragment datagram is now exactly 65507 bytes (fits). Also updated the stale Writer::setFragmentSize doc comment.

Gates: golden wire test byte-identical; docker ROS 2 interop 13/13 (incl. 200KB espp↔ROS 2 both directions, cross_process_frag_200k, and backlog_no_skip); esp32 builds green with fragmentation on.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request rtps real time publish subscribe

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants