Skip to content

feat(rtps_embedded): ROS 2-interoperable services (RMI) + actions (AMI) + native protocol - #711

Merged
finger563 merged 28 commits into
mainfrom
feat/rtps-services
Aug 14, 2026
Merged

feat(rtps_embedded): ROS 2-interoperable services (RMI) + actions (AMI) + native protocol#711
finger563 merged 28 commits into
mainfrom
feat/rtps-services

Conversation

@finger563

Copy link
Copy Markdown
Contributor

Adds services (RMI) and actions (AMI) to the rtps_embedded component, in
both a ROS 2-interoperable flavour and a lean native (espp↔espp) flavour, on top
of the existing pub/sub — validated end-to-end against live ROS 2 (rmw_fastrtps,
Jazzy) both directions.

What's new

  • Services (RMI) — request/reply with correlated responses. Byte-level
    (add_service_server/_client) and typed (espp::ServiceServer<Req,Resp> /
    ServiceClient<Req,Resp>). Three client call styles: blocking call,
    call_async, call_future. Deferred-reply support (ServiceResponder) for
    slow responses.
  • Actions (AMI) — long-running goals with feedback, result, and cancel
    (3 services + 2 topics; no new wire primitive). Byte-level and typed
    (ActionServer / ActionClient / ActionGoalHandle).
  • Native protocol — a deliberately lean espp↔espp variant (20-byte in-band
    correlation header, es_rq/es_rr topics, ~3 endpoints per action vs ROS's
    ~10), same typed wrappers via a RtpsProtocol config field.
  • Typed layerrtps_service.hpp / rtps_action.hpp, the RMI/AMI analogue
    of Publisher<T>/Subscriber<T>: reflectable structs (de)serialized by the
    cdr component, no manual bytes.
  • Python bindings for the whole surface + python/rtps_rpc_demo.py.
  • Docsdoc/en/protocols/rtps_rmi_ami.rst (function, use, rationale, wire
    format, when to choose native); refreshed component + example READMEs.
  • Kconfig RTPS_ENABLE_RPC (default on) compiles the whole services/actions
    layer out for pure-pub/sub devices.
  • Embedded example now hosts a typed service + action a ROS 2 client can
    drive, runs service/action clients, and has an opt-in second-participant
    self-test.

Wire format (confirmed by capture, not guessed)

Before writing code, each wire format was captured from a live rmw_fastrtps
exchange (tshark) and codified in golden byte tests — which corrected two initial
assumptions: ROS correlation is related_sample_identity inline QoS under both
PID 0x0083 (OMG) and 0x800f (eProsima), present on both request and reply
(not the 0x8002/request-only guess). Result: no SEDP type-hash is needed — an
espp service/action even appears in ros2 service list / ros2 action list.

Testing

  • Golden wire tests stay byte-identical; a new data_related_sample_identity
    section (all prior bytes unchanged).
  • Interop harness 27/27 (dockerised, gated in CI on rtps_embedded/**):
    in-process loopbacks (pub/sub, services, actions, native, typed), byte-level
    codec unit tests, live ros2 service call / ros2 action send_goal both
    directions, and a functional Python demo (5/5) built in-container.
  • esp32 builds green: RPC on and off, static/dynamic storage, frag on/off,
    and the example (now in the CI build matrix).
  • Wheel smoke test asserts the full RMI/AMI binding surface.

Notes for reviewers

  • The engine wire change is limited to emitting/parsing the reply inline-QoS
    correlation; plain pub/sub is byte-identical (proven by golden + interop).
  • A latent portability bug surfaced only when building the Python module on macOS
    libc++ (forward-declared context in a unique_ptr member) — fixed; a good
    argument for keeping the wheel build in the review loop.
  • The example was renamed main.cpprtps_embedded_example.cpp to prepare for
    retiring the old rtps component in a follow-up.
  • Deferred (intentional): the "shared L2" template-unification of the ROS and
    native paths — the two already share the pub/sub + service primitives; further
    merging is cosmetic and higher-risk.

Design + milestone detail: components/rtps_embedded/RMI_AMI_DESIGN.md.

🤖 Generated with Claude Code

finger563 and others added 24 commits August 13, 2026 20:12
… confirmed

Design spec for ROS 2-interoperable services (RMI) and actions (AMI) plus a
separate native minimal protocol, per the agreed direction (FastDDS-first
interop; separate lean protocol for espp<->espp; design-first).

Key: in ROS 2, services = 2 topics + correlation, actions = 3 services + 2 topics
(no new wire primitive), so the whole effort reduces to adding reliable
request/reply with correlation, then library code on top.

The request/reply correlation wire format is CONFIRMED against a live rmw_fastrtps
(ROS 2 Jazzy) AddTwoInts capture, dissected with tshark - correcting the initial
guesses:
  - related_sample_identity is carried as inline QoS under BOTH PID 0x0083
    (OMG DDS-RPC standard) and PID 0x800f (eProsima legacy), 24-byte SampleIdentity
    (GUID 16 + SequenceNumber 8, CDR_LE). NOT 0x8002.
  - BOTH request and reply carry it: client sends {its reply-reader GUID, seq=
    UNKNOWN}; server echoes {that GUID, request's writerSeqNumber}. The client
    matches replies on {own reply-reader GUID, pending writerSeqNumber}.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First slice of M1 (Track A request/reply). Header-only rtps::rpc helpers to
mangle a ROS 2 service name + base DDS type into the request/reply topic and type
names rmw_fastrtps uses on the wire:

  service_request_topic / service_reply_topic  -> rq/<svc>Request , rr/<svc>Reply
  service_request_type  / service_response_type -> <base>_Request_ , <base>_Response_

Expected strings are taken verbatim from the live rmw_fastrtps (ROS 2 Jazzy)
AddTwoInts capture. Pure string logic, no engine dependency; validated by a
host-only unit test (pc/tests/rtps_service_naming.cpp, 7/7). No wire/golden
impact.

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

Second slice of M1 (Track A request/reply): the engine wire support for ROS 2
service correlation, gated by the golden byte tests.

- rpc/sample_identity.hpp: SampleIdentity {Guid, SequenceNumber} + the two wire
  PIDs (0x0083 OMG-standard, 0x800f eProsima-legacy) + the 24-byte CDR_LE codec.
  Layout and dual-PID behavior confirmed against the live ROS 2 Jazzy capture
  (RMI_AMI_DESIGN.md 3.2).
- MessageFactory::addSubMessageDataWithRelatedSampleIdentity(): a DATA submessage
  that carries the related_sample_identity as inline QoS under BOTH PIDs, then
  PID_SENTINEL, then the payload. Kept separate from addSubMessageData so the
  plain pub/sub path stays byte-identical (proven: all existing golden sections
  unchanged).
- MessageReceiver: the existing inline-QoS TLV loop now captures a
  related_sample_identity (either PID) and surfaces it on ReaderCacheChange via
  two new optional fields (hasRelatedSampleIdentity + relatedSampleIdentity);
  zero-cost for plain pub/sub.

New golden section data_related_sample_identity (96 bytes), hand-added to the
.inc without touching any existing bytes; its expected bytes were generated by
the real emit function and cross-checked against the hand-derived layout and the
capture.

Gates: golden 8/8 incl. the new section, all prior sections byte-identical;
docker ROS 2 interop 13/13; esp32 build green. Value codec round-trip verified.

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

Third slice of M1 (Track A request/reply): the engine send path can now emit a
DATA carrying a related_sample_identity as inline QoS, for ROS 2 service
request/reply. Plumbing only; the facade service API + correlation is M1.4.

- CacheChange gains hasRelatedSampleIdentity + relatedSampleIdentity (default
  unset), copied in move-assign and cleared in reset().
- SimpleHistoryCache / HistoryCacheWithDeletion addChange() take the identity as
  two trailing optional params and stamp it on the stored change.
- Writer::newChange gains the two optional params (defaulted, so all existing
  call sites and the 3-arg forwarder are unchanged) + a public convenience
  newChangeWithRelatedSampleIdentity() the service reply/request writers use.
- StatelessWriter/StatefulWriter forward the identity to addChange and, in their
  send paths, branch to addSubMessageDataWithRelatedSampleIdentity when the
  change carries one; otherwise the byte-identical plain addSubMessageData path.

Because the branch is per-change and false for all plain pub/sub, the pub/sub
wire format is unchanged - proven: every existing golden section is byte-
identical (incl. plain `data`), docker ROS 2 interop 13/13, esp32 build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fourth slice of M1 (Track A request/reply): the public facade service API that
ties together mangling (M1.1), the inline-QoS wire codec (M1.2), and the writer
send path (M1.3) into working ROS 2-style request/reply.

RtpsParticipant additions:
- add_service_server(ServiceConfig, handler): creates a reliable request reader
  (rq/<svc>Request) + reply writer (rr/<svc>Reply). On each request, runs the
  handler and sends its return value as the reply, correlated by echoing
  {client reply-reader GUID (from the request's related_sample_identity), request
  writerSeqNumber} back as inline QoS.
- add_service_client(ServiceConfig) -> ServiceClient: creates a reliable request
  writer + reply reader; stamps outgoing requests with {own reply-reader GUID,
  seq=UNKNOWN}; correlates replies by {own GUID, the request's assigned
  writerSeqNumber} via a pending-request table (inserted under lock across
  newChange so a reply can never race ahead of registration).
- ServiceClient::call() (sync, condition-variable + timeout, drops the pending
  entry on timeout) and call_async() (callback on an engine worker thread).
  Engine types stay out of the public header (PIMPL for ServiceClient::Impl and
  a forward-declared ServiceServerContext).

New in-process test rtps_service_loopback: an add_two_ints server + client (two
participants, AddTwoInts CDR encoding matching example_interfaces) verifies both
a sync call (7+35=42) and an async call (1000+337) return the correlated reply.
Wired into the interop harness as the service_loopback leg.

Gates: golden byte-identical; docker ROS 2 interop 14/14 (incl. the new
service_loopback); esp32 build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Final slice of M1 (Track A request/reply): prove the espp service layer
interoperates with real ROS 2 (rmw_fastrtps, Jazzy) in both directions, closing
out ROS 2-interoperable services (RMI).

- rtps_service_interop_server: hosts an add_two_ints service; a live
  `ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts "{a,b}"`
  discovers it (it even appears in `ros2 service list`) and gets the correct
  Response(sum). Answers the open design question: no type-hash / TypeInformation
  exchange is needed - plain rq/rr topic + _Request_/_Response_ type-name matching
  suffices, exactly like pub/sub.
- rtps_service_interop_client: calls an rclpy add_two_ints server
  (ros2_add_two_ints_server.py) and verifies the correlated reply.
- interop Dockerfile installs ros-jazzy-example-interfaces; run_interop.sh builds
  both binaries and adds two legs: espp_service_server<-ros2_client and
  espp_service_client->ros2_server.

Gate: docker ROS 2 interop 16/16 (adds both service directions to the 14 from
M1.4), incl. `ros2 service call` -> espp -> Response(sum=42) and espp client ->
rclpy server -> 20+22=42. Golden unchanged; esp32 unaffected (host-only test
artifacts, no engine/facade change).

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

M1.1-M1.5 complete on feat/rtps-services: ROS 2-interoperable services (RMI)
working both directions (interop 16/16). Record that no SEDP type-hash /
TypeInformation exchange is needed - plain rq/rr topic + _Request_/_Response_
type-name matching suffices (the espp service appears in `ros2 service list`).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two additions toward the client API request (sync + callback + promise) and M2
(ROS 2 actions).

1. ServiceClient::call_future() - a std::future<optional<vector<uint8_t>>> API
   alongside the existing blocking call() (RMI) and callback call_async() (AMI).
   Built on call_async + a shared promise, so all three styles share one path.
   The loopback test now exercises all three (sync 7+35, async 1000+337,
   future 500+500).

2. M2.1 rpc/action_naming.hpp: ROS 2 action name/type mangling for the 3 services
   (send_goal/cancel_goal/get_result, reusing the service mangling on the
   "<action>/_action/<svc>" base names) + 2 topics (feedback/status). Verified by
   a host test (12/12) against a live rmw_fastrtps Fibonacci capture.

Also records the CONFIRMED action wire format in RMI_AMI_DESIGN.md 3.4 (SendGoal/
GetResult/Feedback/GoalStatusArray CDR layouts, UUID = 16 raw bytes, from the
Fibonacci capture) - actions add no new wire primitive; the 3 services correlate
via the same related_sample_identity as M1.

Harness: builds + runs the service_naming and action_naming host legs (the M1.1
service mangling test was never wired in; now it is). Gate: interop 18/18; golden
byte-identical; esp32 build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
rpc/action_types.hpp: the CDR envelope codec for ROS 2 actions - wraps/unwraps
the user's opaque goal/result/feedback CDR in the action message envelopes
(SendGoal_Request/Response, GetResult_Request/Response, FeedbackMessage,
GoalStatusArray) plus GoalUuid (16 raw bytes) and the GoalStatus enum.

The nested user payload is spliced field-wise (strip its encap, place its body
after the envelope prefix under the envelope's single encap): goal/feedback
follow UUID(16) at offset 16 (8-aligned, correct for any field); result follows
status(1)+pad(3) at offset 4 (correct for <=4-aligned first fields; an
8-aligned result first field needs the typed cdr path, out of scope for v1 -
documented in the header).

Validated byte-for-byte against the live Fibonacci capture (11/11): every
wrap/make matches the captured bytes exactly (goal UUID 93beb052..., order=5,
result [0,1,1,2,3,5] SUCCEEDED, feedback progression, GoalStatusArray ACCEPTED)
and every unwrap/parse round-trips. Pure header + host test, no engine impact;
wired into the interop harness as the action_types leg.

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

Deferred-reply service extension + the ROS 2 action goal server/client facade,
composed over the M1 services + pub/sub.

Deferred replies (foundation for get_result, which holds the request until the
goal finishes):
- ServiceResponder (copyable, thread-safe, reply-once) + service_deferred_handler_t
  + add_service_server_deferred(). The handler gets a responder it may fulfill
  later from any thread, so a slow response never blocks an engine worker.
- add_service_server() is now a thin wrapper: a sync handler is a deferred
  handler that replies immediately (one trampoline, one code path).

Actions (add_action_server / add_action_client, ActionGoalHandle, ActionClient):
- Server composes send_goal (accept -> spawn execute thread), get_result
  (deferred: reply now if done, else hold the responder until terminate()),
  cancel_goal (marks is_canceling()), + feedback/status publishers. The
  ActionGoalHandle offers publish_feedback / succeed / abort / canceled.
- Client composes the 3 service clients + a feedback subscriber, routing feedback
  and the terminal result to per-goal callbacks by goal UUID. send_goal() ->
  send_goal service; on accept -> get_result service; result delivered via
  on_result. Goal ids are random_device+counter (unique even if RNG is weak).
- stop() now also clears the service/action bookkeeping.

Tests: rtps_action_loopback (in-process Fibonacci: goal accepted, 4 feedback
msgs, deferred result [0,1,1,2,3,5] SUCCEEDED); rtps_service_loopback gains a
deferred-reply case (reply from a worker thread). AddTwoInts/Fibonacci CDR match
example_interfaces so the payloads are ROS 2-valid for the coming interop legs.

Gate: interop 20/20 (adds action_loopback + the deferred service case); golden
byte-identical; esp32 build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Final slice of M2: prove the espp action layer interoperates with real ROS 2
(rmw_fastrtps, Jazzy) both ways, completing ROS 2-interoperable actions (AMI).

- rtps_action_interop_server: hosts a Fibonacci action; a live
  `ros2 action send_goal -f /fibonacci example_interfaces/action/Fibonacci
  "{order: 5}"` discovers it (`ros2 action list` shows it), drives it through
  accept -> feedback -> deferred get_result, and finishes SUCCEEDED. Confirms no
  type-hash exchange is needed for actions either.
- rtps_action_interop_client: drives an rclpy Fibonacci server
  (ros2_fibonacci_server.py) and verifies the terminal result [0,1,1,2,3,5] with
  4 feedback messages.
- run_interop.sh adds espp_action_server<-ros2_client and
  espp_action_client->ros2_server.

Gate: docker ROS 2 interop 22/22 (adds both action directions to M2.4's 20).
Golden byte-identical; esp32 unaffected (host-only test artifacts).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
M2.1-M2.5 complete on feat/rtps-services: ROS 2-interoperable actions (AMI)
working both directions (interop 22/22), via a deferred-reply service extension
for get_result. Next: M3 native minimal protocol.

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

Track B: a deliberately non-ROS request/reply that trades interop for simplicity.
Correlation is a 20-byte in-band header prepended to the payload, so - unlike the
ROS path - it needs NO engine wire support (no inline QoS, no rq/rr mangling) and
rides plain reliable pub/sub. Pure facade composition.

- rpc/native_protocol.hpp: the header codec (client_prefix[12] + request_id:u32 +
  op:u8 + flags:u8 + reserved:u16) + es_rq/es_rr topic naming (a distinct prefix
  so it never aliases the ROS rq/rr topics). client_prefix is the correlation key
  the server echoes on the reply; clients filter replies by their own prefix +
  request_id, mirroring the ROS related_sample_identity scheme but in-band.
- RtpsParticipant::add_native_service_server / add_native_service_client +
  NativeServiceClient with the same three call styles as the ROS client
  (call / call_async / call_future). The client stamps requests with the
  participant's own GUID prefix; a single server per service (broadcast request).

Test rtps_native_service_loopback: two participants, add_two_ints, verifies sync
(7+35), async (1000+337), and future (500+500) calls all correlate. Gate: interop
23/23; esp32 build green; golden byte-identical (no engine change).

Follow-ups: native action (lean, collapsed endpoints) + M4 consolidation.

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

Native action (Track B lean AMI) + two portability fixes the growing RPC facade
needed for libc++ / the Python module build.

Native action (add_native_action_server / add_native_action_client,
NativeGoalHandle, NativeActionClient): collapses the ROS action's 3 services + 2
topics to ONE native send_goal service (-> {accepted, uint32 goal_handle}) + ONE
feedback topic that also carries the terminal result (status folded in). ~3
endpoints vs ROS's ~10; no UUIDs, no get_result/status services. Framing added to
rpc/native_protocol.hpp. Test rtps_native_action_loopback (countup: feedback +
result), interop leg native_action_loopback.

Portability fixes (surfaced building the pybind module on macOS libc++, latent
since M1.4; libstdc++ on linux/esp32 was lenient):
- service_servers_ now holds shared_ptr<ServiceServerContext> (was unique_ptr).
  A forward-declared context in a unique_ptr member forces the pointee complete
  at the participant's construct/destruct point; shared_ptr's destructor is
  type-erased, so an incomplete type is fine (all other RPC containers already
  used shared_ptr).
- stop() and ~RtpsParticipant moved to end of the .cpp, after every RPC context
  type is defined.

Gate: interop 24/24 (incl. native_action_loopback status=4 result=5 feedback=5);
esp32 build green; golden byte-identical.

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

Expose the full RMI/AMI surface to Python and document it, so every mechanism can
be showcased and tested from both C++ (the loopback + interop tests) and Python.

Python (lib/python_bindings/rtps_bindings.cpp): bind add_service_server/client
(+ ServiceClient.call/call_async), add_action_server/client (+ ActionClient.
send_goal, ActionGoalHandle.publish_feedback/succeed/abort), and the native
add_native_service_*/add_native_action_* equivalents (+ NativeServiceClient,
NativeActionClient, NativeGoalHandle). Callbacks run on engine threads and are
wrapped GIL-correctly (reusing the existing make_gil_safe_holder pattern); the
blocking call() releases the GIL. Payloads are CDR-encapsulated bytes.

python/rtps_rpc_demo.py: a runnable showcase + self-test that drives all four
mechanisms in one process (ROS service sync+async, ROS action Fibonacci with
feedback, native service, native action countup). Exits 0 iff all pass - verified
5/5 against the freshly built module.

doc/en/protocols/rtps_rmi_ami.rst: explains what services (RMI) and actions (AMI)
are and why, the three client call styles, the deferred-reply mechanism, the ROS 2
correlation wire format, the lean native protocol and when to choose it, the
Python API, and the full test matrix. Added to the protocols toctree.

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

Make the whole services/actions surface (ROS-interoperable and native) optional at
build time so a pure-pub/sub device can drop it - and its std::thread/std::future
use - to save flash.

- rtps_participant.hpp defines RTPS_WITH_RPC by default (unless RTPS_NO_RPC is
  set) and wraps the RPC public API + members in #ifdef RTPS_WITH_RPC.
- rtps_participant.cpp wraps the RPC implementation block and the RPC container
  clears in stop() in the same guard (~RtpsParticipant/stop() stay compiled).
- Kconfig option RTPS_ENABLE_RPC (default y); CMakeLists.txt defines RTPS_NO_RPC
  when it is turned off (ESP builds only; host always keeps RPC on).

Gate: esp32 builds both with RPC on (default, guarded) and off (RTPS_NO_RPC,
RPC excluded); host interop 24/24 unchanged; pub/sub unaffected either way.
#ifdef/#endif balance verified.

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

All milestones M1-M4 implemented and gated on feat/rtps-services: ROS 2 services
+ actions (both directions, live interop), the lean native protocol, Python
bindings + demo (5/5), docs, and the RPC Kconfig compile-out. The "shared L2"
template-unification is deliberately deferred as cosmetic/higher-risk.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the typed, espp-idiomatic service/action classes (the RMI/AMI analogue of the
typed Publisher<T>/Subscriber<T>) and exercise them on-device, addressing two gaps:
the byte-level API had no typed wrappers, and the esp32 example only did pub/sub.

Typed wrappers (header-only, over reflectable structs via the cdr component - no
manual bytes; one class covers both protocols via a RtpsProtocol config field):
- rtps_service.hpp: espp::ServiceServer<Req,Resp> + espp::ServiceClient<Req,Resp>
  (call / call_async / call_future).
- rtps_action.hpp: espp::ActionServer<Goal,Result,Feedback> +
  espp::ActionClient<...> + ActionGoalHandle<...> (typed publish_feedback/succeed/
  abort). GoalStatus enum for terminal state.
Both guarded by RTPS_WITH_RPC so they vanish with the RPC compile-out.

Embedded example (esp32): now also hosts a typed ServiceServer</add_two_ints> and
ActionServer</fibonacci> (guarded by RTPS_WITH_RPC) - a ROS 2 client can
`ros2 service call` / `ros2 action send_goal` the device directly.

Test rtps_typed_rpc_loopback: typed service + action, both ROS 2 and native
protocols, in one process. Docs updated with the two API levels.

Gate: interop 25/25 (typed_rpc_loopback: service ros/native, action ros/native);
esp32 builds with the typed example RPC-on and RPC-off (example guarded); golden
byte-identical.

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

Address review feedback on the typed RMI/AMI wrappers.

1. Extract the shared bits into rtps_message.hpp: the RtpsMessage concept (was in
   rtps_pubsub.hpp), the RtpsProtocol enum, and detail::rtps_serialize/deserialize
   (were duplicated in rtps_service.hpp). rtps_pubsub / rtps_service / rtps_action
   now all include it - one definition, clearer layering.

2. Embedded example now also exercises the CLIENT side: a typed ServiceClient +
   ActionClient calling a peer's "/peer_add_two_ints" service + "/peer_fib" action
   on a 5 s timer (blocking service call + action goal with typed feedback/result).
   Kept on the single participant (a second participant would double engine RAM on
   a plain ESP32); a participant can't call its own services, so these target a
   peer - run a ROS 2/rclpy server for those names for a full round-trip.

3. Fill in the missing/incomplete Doxygen on the service + action wrappers:
   @tparam on every class, \param on ctors/methods, doc'd Config fields
   (on_goal/execute/handler/callbacks), and the ActionGoalHandle terminators.

4. Fix ServiceClient::call_future doc: it works on BOTH protocols (it is built on
   call_async, which dispatches to ROS or native) - the "ROS 2 only" note was wrong.

Gate: interop 25/25 (typed_rpc_loopback service+action, both protocols); esp32
builds with the server+client example, RPC on and off; golden byte-identical.

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

Add CONFIG_RTPS_EXAMPLE_SECOND_PARTICIPANT (default n, depends on
RTPS_ENABLE_RPC): when enabled, the example additively brings up a SECOND
RtpsParticipant with its own typed ServiceClient + ActionClient that call THIS
device's own /add_two_ints service and /fibonacci action, for a full on-device
round-trip self-test (logging PASS/FAIL). A participant filters out its own
messages, so a distinct participant is the only way to loop back on one device.

Off by default because a second participant roughly doubles the RTPS engine RAM
(two discovery stacks, socket sets, pools) and may not fit a plain ESP32 without
PSRAM. Purely additive - the peer-facing client demo and the servers are
unchanged; when the option is off the block is #if'd out entirely.

Gate: esp32 builds with the option ON (self-test compiled) and OFF (default,
guard excludes it).

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

Close the test-coverage gaps for the services/actions/native/typed work.

- Interop harness (rtps_interop.yml, gated on rtps_embedded/**):
  * rtps_native_protocol: byte-level unit test for the native codec (20-byte
    header round-trip + fixed offsets, goal-reply, feedback framing) - the one
    codec that only had end-to-end coverage. New "native_protocol" leg.
  * python_rpc_demo: build the espp wheel in-container (a venv with the
    scikit-build-core backend; SETUPTOOLS_SCM_PRETEND_VERSION since the copied
    tree has no .git) and run python/rtps_rpc_demo.py - functionally tests the
    GIL-wrapped service/action/native bindings in the multicast-working shared
    netns, which no C++ test reaches. Now 27/27.
- build.yml: add components/rtps_embedded/example to the esp32 build matrix - it
  was never CI-built, and now exercises the typed service+action servers/clients
  and the second-participant self-test option.
- Wheel smoke test (pyproject cibuildwheel): replace the bare `import espp` with
  python/rtps_bindings_smoke.py, which asserts the full RMI/AMI binding surface
  (methods + client/handle classes) is exposed - catches a dropped binding.

Gate: interop 27/27 (native_protocol + python_rpc_demo 5/5); esp32 example builds
in CI matrix; smoke test passes on the built wheel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a concurrency group keyed by the workflow + PR head branch
(github.head_ref, falling back to github.ref for workflow_dispatch) with
cancel-in-progress, so pushing a new commit to a PR cancels the earlier,
now-stale interop run instead of queueing behind it. Runs on different PRs get
different groups and never cancel each other; this workflow never runs on push to
main, so cancel-in-progress is always safe here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The head branch name is not unique across forks (two PRs from different forks can
share a branch name and cross-cancel); the PR number is globally unique. Keep the
github.workflow prefix (so it never cancels other workflows) and the github.ref
fallback for workflow_dispatch (which has no PR number).

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

Prepare for the next phase (retiring the old rtps component + docs in favour of
this one).

- Rename example/main/main.cpp -> rtps_embedded_example.cpp so the file is
  uniquely named across the tree (avoids a clash when the components are
  consolidated). The example CMake uses SRC_DIRS ".", so no build change; added a
  file header comment. No docs referenced it by path.
- Rewrite components/rtps_embedded/README.md: it documented the low-level engine
  API (rtps::Domain, wrong .h extension, a removed per-participant ThreadPool, and
  an initiator/responder example that no longer exists). Now it leads with the
  espp::RtpsParticipant facade + the typed Publisher/Subscriber/Service/Action
  APIs, the current SocketReactor + shared ThreadPool architecture, the real
  build knobs (RTPS_LIMITS_PROFILE / _STORAGE_DYNAMIC / _ENABLE_FRAGMENTATION /
  _ENABLE_RPC), the correct dependencies, and pointers to the interop harness and
  RMI/AMI docs.
- Add example/README.md (matching the espp example-README convention): Ethernet
  bring-up, the typed pub/sub + service/action servers a ROS 2 client can drive,
  the service/action clients, and the RTPS_EXAMPLE_SECOND_PARTICIPANT self-test.

Gate: esp32 example builds after the rename; every path/value cited in the READMEs
verified against the tree.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@finger563
finger563 requested review from SwapnilPande and guo-max and a lite review from Copilot August 14, 2026 18:27
@finger563 finger563 self-assigned this Aug 14, 2026
@finger563 finger563 added enhancement New feature or request rtps real time publish subscribe labels Aug 14, 2026
@github-actions

github-actions Bot commented Aug 14, 2026

Copy link
Copy Markdown

✅Static analysis result - no issues found! ✅

…mo cleanup

Make the Python RMI/AMI examples use pycdr2 message objects instead of hand-packed
struct bytes - matching the existing espp.rtps.Publisher/Subscriber pattern, and
much easier to read.

- espp/rtps.py: add typed ServiceServer/ServiceClient + ActionServer/ActionClient
  (+ GoalHandle), the Python counterparts to the C++ espp::ServiceServer<> etc.
  Each pairs the byte-level RtpsParticipant RPC methods with a pycdr2 codec, so
  handlers/callbacks take and return message objects; native=True selects the lean
  protocol. Mirrors the existing typed Publisher/Subscriber wrappers.
- python/rtps_rpc_demo.py: rewritten around pycdr2 make_idl_struct schemas
  (AddTwoInts, Fibonacci, native Mul/CountUp) + the typed wrappers - no more
  struct.pack / manual offsets. pycdr2 emits ROS 2 / classic CDR, byte-compatible
  with the C++ side (verified: AddTwoInts req serializes to the same bytes).
- interop Dockerfile: add pycdr2 to the in-container venv so the python_rpc_demo
  harness leg can import it.

Gate: demo 5/5 locally and in the docker interop harness; interop 27/27.

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

Adds a full RTPS request/reply + goal-oriented messaging layer to rtps_embedded, including ROS 2 (rmw_fastrtps) interoperable services (RMI) and actions (AMI), plus a lean native espp↔espp protocol, with typed CDR-based wrappers, Python bindings, documentation, and an expanded interop/test harness.

Changes:

  • Add ROS 2-compatible service/action primitives (incl. related_sample_identity inline-QoS support) and native RPC/action protocol support.
  • Introduce typed wrappers (rtps_service.hpp, rtps_action.hpp) and extensive host + docker interop tests (goldens, naming, codec, loopbacks).
  • Add Python bindings + demos/smoke tests, docs for the protocol/API, and build/CI integration (Kconfig toggle + workflow updates).

Reviewed changes

Copilot reviewed 58 out of 58 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
python/rtps_rpc_demo.py Python end-to-end demo/self-test of ROS2 + native services/actions.
python/rtps_bindings_smoke.py Wheel smoke test to validate RTPS RPC/action bindings are present.
pyproject.toml Update cibuildwheel test-command to run the binding surface smoke test.
pc/tests/rtps_typed_rpc_loopback.cpp Host typed service/action loopback coverage (ROS2 + native).
pc/tests/rtps_service_naming.cpp Host unit test for ROS 2 service name/type mangling.
pc/tests/rtps_service_loopback.cpp In-process service loopback exercising correlation and call styles.
pc/tests/rtps_service_interop_server.cpp Host binary: espp service server for ROS 2 interop matrix.
pc/tests/rtps_service_interop_client.cpp Host binary: espp service client for ROS 2 interop matrix.
pc/tests/rtps_native_service_loopback.cpp In-process native service loopback (sync/async/future).
pc/tests/rtps_native_protocol.cpp Unit test for native protocol framing/header and action framing.
pc/tests/rtps_native_action_loopback.cpp In-process native action loopback (goal + feedback/result).
pc/tests/rtps_action_types.cpp Byte-accurate unit test for ROS 2 action envelope codec vs capture.
pc/tests/rtps_action_naming.cpp Host unit test for ROS 2 action naming/mangling.
pc/tests/rtps_action_loopback.cpp In-process ROS2-style action loopback (services/topics + UUID correlation).
pc/tests/rtps_action_interop_server.cpp Host binary: espp action server for ROS 2 interop matrix.
pc/tests/rtps_action_interop_client.cpp Host binary: espp action client for ROS 2 interop matrix.
pc/tests/rtps_embedded_golden.inc Add golden bytes for DATA with related_sample_identity inline QoS.
pc/tests/rtps_embedded_golden.cpp Build and run new golden test case for related_sample_identity.
lib/python_bindings/rtps_bindings.cpp Extend pybind11 RTPS bindings to include services/actions + native variants.
doc/en/protocols/rtps_rmi_ami.rst New documentation page describing RMI/AMI APIs and wire formats.
doc/en/protocols/index.rst Add rtps_rmi_ami to the protocols docs index.
components/rtps_embedded/src/messages/MessageReceiver.cpp Parse related_sample_identity inline QoS into ReaderCacheChange.
components/rtps_embedded/src/entities/StatelessWriter.cpp Emit DATA with related_sample_identity when requested by CacheChange.
components/rtps_embedded/src/entities/StatefulWriter.cpp Emit DATA with related_sample_identity in stateful send paths.
components/rtps_embedded/RMI_AMI_DESIGN.md Design doc capturing wire formats and milestone plan/validation.
components/rtps_embedded/README.md Update component README to cover pub/sub + services/actions + native protocol.
components/rtps_embedded/Kconfig Add RTPS_ENABLE_RPC option to compile out services/actions layer.
components/rtps_embedded/interop/run_interop.sh Expand interop harness to build/run new RPC/action tests and Python demo.
components/rtps_embedded/interop/ros2_fibonacci_server.py rclpy action server used by interop harness.
components/rtps_embedded/interop/ros2_add_two_ints_server.py rclpy service server used by interop harness.
components/rtps_embedded/interop/Dockerfile Add deps + venv to build/test Python wheel in-container.
components/rtps_embedded/include/rtps/storages/SimpleHistoryCache.hpp Extend stored CacheChange metadata to carry related_sample_identity.
components/rtps_embedded/include/rtps/storages/HistoryCacheWithDeletion.hpp Same: carry related_sample_identity through history cache path.
components/rtps_embedded/include/rtps/storages/CacheChange.hpp Add fields for related_sample_identity + copy/reset behavior.
components/rtps_embedded/include/rtps/rpc/service_naming.hpp New helper for ROS 2 service topic/type mangling.
components/rtps_embedded/include/rtps/rpc/sample_identity.hpp New constants/types for related_sample_identity serialization.
components/rtps_embedded/include/rtps/rpc/native_protocol.hpp New native protocol framing + native action helpers.
components/rtps_embedded/include/rtps/rpc/action_types.hpp New ROS 2 action envelope wrap/unwrap codec.
components/rtps_embedded/include/rtps/rpc/action_naming.hpp New helper for ROS 2 action endpoint/type naming.
components/rtps_embedded/include/rtps/messages/MessageFactory.hpp Add DATA submessage builder that emits related_sample_identity inline QoS.
components/rtps_embedded/include/rtps/entities/Writer.hpp Add newChangeWithRelatedSampleIdentity and propagate newChange params.
components/rtps_embedded/include/rtps/entities/StatelessWriter.hpp Update writer interface to accept related_sample_identity metadata.
components/rtps_embedded/include/rtps/entities/StatefulWriter.hpp Update writer interface to accept related_sample_identity metadata.
components/rtps_embedded/include/rtps/entities/Reader.hpp Extend ReaderCacheChange to carry optional related_sample_identity.
components/rtps_embedded/include/rtps_service.hpp New typed service wrapper API over byte-level service endpoints.
components/rtps_embedded/include/rtps_pubsub.hpp Refactor to share RtpsMessage concept from new rtps_message.hpp.
components/rtps_embedded/include/rtps_participant.hpp Add service/action/native RPC API surface + compile-out guard macros.
components/rtps_embedded/include/rtps_message.hpp New shared RtpsMessage concept + protocol selection enum + CDR helpers.
components/rtps_embedded/include/rtps_action.hpp New typed action wrapper API over byte-level action endpoints.
components/rtps_embedded/example/README.md New example README describing typed pub/sub + service/action demo on ESP32.
components/rtps_embedded/example/main/rtps_embedded_example.cpp New ESP32 example demonstrating typed pub/sub + service/action server/client.
components/rtps_embedded/example/main/main.cpp Remove old example entrypoint (renamed/replaced).
components/rtps_embedded/example/main/Kconfig.projbuild Add example option for a second self-test participant.
components/rtps_embedded/CMakeLists.txt Wire RTPS_ENABLE_RPC to RTPS_NO_RPC compile definition.
.github/workflows/rtps_interop.yml Add concurrency cancellation for interop workflow runs.
.github/workflows/build.yml Add components/rtps_embedded/example to CI build matrix.
Suppressed comments (1)

lib/python_bindings/rtps_bindings.cpp:328

  • The Python ActionClient binding exposes send_goal() but not cancel_goal(...), even though the underlying C++ API supports canceling goals. This makes the Python surface incomplete for ROS 2 actions (cancel is one of the core endpoints).

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread python/rtps_rpc_demo.py Outdated
Comment thread lib/python_bindings/rtps_bindings.cpp
Comment thread components/rtps_embedded/include/rtps/rpc/action_types.hpp
Comment thread lib/python_bindings/rtps_bindings.cpp
Comment thread lib/python_bindings/espp/rtps.py Fixed
Static analysis (cppcheck):
- rtps_action_types.cpp: replace the hex()+push_back loops with an encap_hex
  helper using insert() (useStlAlgorithm).
- rtps_service_loopback.cpp: rename the inner deferred-reply `reply` (shadowVariable).
- rtps_participant.cpp: generate_goal_id uses std::generate (useStlAlgorithm) +
  <algorithm>.
- rtps_service.hpp: ServiceClient::call_async takes on_response by const ref
  (passedByValue).

Review (Copilot):
- python/rtps_rpc_demo.py: replace `assert server.start()...` with an explicit
  check + stderr + non-zero return (asserts are skipped under `python -O`).
- Python bindings: expose ActionGoalHandle.canceled() (the C++ handle has it) and
  ServiceClient/NativeServiceClient.call_future() returning a
  concurrent.futures.Future - so the Python surface matches the docs' claim. Also
  added to the typed espp.rtps wrappers (ServiceClient.call_future,
  GoalHandle.canceled/is_canceling) and the demo now exercises call_future
  (ros_service_future); the smoke test asserts the new methods exist.
- espp/rtps.py: comment the previously-empty feedback except (must not kill the
  engine reader thread).
- Document the action get_result 8-byte-alignment limitation of the byte-splice
  envelope in the user-facing RMI/AMI docs (result first field must be <=4-aligned;
  reorder an int64/float64 first field). Goal/feedback/services unaffected.

Gate: interop 27/27 (golden, refactored action_types, service_loopback, python
demo 6/6 incl. call_future); esp32 build green; wheel smoke test green.

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

Copy link
Copy Markdown
Contributor Author

Addressed the review comments and static-analysis findings in 8d50039.

Static analysis (cppcheck):

  • rtps_action_types.cpp — replaced the hex()+push_back loops with an encap_hex helper using insert() (useStlAlgorithm ×7).
  • rtps_service_loopback.cpp — renamed the inner deferred-reply replydreply (shadowVariable).
  • rtps_participant.cppgenerate_goal_id now uses std::generate (useStlAlgorithm).
  • rtps_service.hppServiceClient::call_async takes on_response by const ref (passedByValue).

Review comments:

  • assert in the demo — replaced with an explicit check + stderr + non-zero return (asserts are skipped under python -O).
  • Missing canceled() binding — exposed ActionGoalHandle.canceled() in Python (+ the typed espp.rtps.GoalHandle). NativeGoalHandle correctly has none (the native protocol has no cancel).
  • Missing call_future in Python — added call_future() to the ServiceClient and NativeServiceClient bindings (returns a concurrent.futures.Future) and to the typed espp.rtps.ServiceClient; the demo now exercises it (ros_service_future) and the wheel smoke test asserts the new methods exist. So the Python surface now matches the docs' claim.
  • Empty except in espp/rtps.py — commented (a malformed feedback / raising callback must not kill the engine reader thread).
  • Action result 8-byte alignment — documented the byte-splice envelope limitation in the user-facing RMI/AMI docs (get_result's result first field must be ≤4-byte aligned; reorder an int64/float64 first field). Goal/feedback payloads and services are unaffected (they follow the 8-aligned UUID / have no such splice). Noted as a v1 limitation to be revisited with full-CDR envelope building.

Gate: interop 27/27 (golden byte-identical, refactored action_types, service_loopback, python demo 6/6 incl. call_future); esp32 build green; wheel smoke test green.

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 58 out of 58 changed files in this pull request and generated 6 comments.

Suppressed comments (6)

components/rtps_embedded/src/rtps_participant.cpp:763

  • A successfully accepted cancellation is returned as ERROR_NONE with an empty goals_canceling list. ROS clients therefore receive no indication that this goal entered cancellation, while unknown or rejected goals receive the same apparent success. Serialize the accepted GoalInfo in goals_canceling and return the appropriate CancelGoal error code for other cases.
    components/rtps_embedded/include/rtps_action.hpp:58
  • The typed server can observe is_canceling(), but its handle exposes only succeed() and abort(). It cannot terminate the goal as CANCELED, even though the byte-level handle supports canceled(), so cancellation cannot be completed through the recommended typed API. Add a typed canceled(Result) terminator and wire it to the underlying handle.
  /// Terminate the goal as ABORTED and deliver the result to the client.
  /// \param result The (partial/error) result.
  void abort(const Result &result) const {
    if (abort_) {
      abort_(detail::rtps_serialize<Result>(result));

components/rtps_embedded/include/rtps_action.hpp:237

  • Converting the ROS send_goal result to bool discards the generated GoalId, and this wrapper exposes no cancel_goal. Typed clients therefore cannot cancel a goal despite cancellation being part of the action API. Return a typed goal handle/id and provide cancellation through it (with an explicit unsupported result for native mode).
    if (ros_) {
      return ros_->send_goal(goal_bytes, std::move(fb_cb), std::move(res_cb)).has_value();
    }
    if (native_) {
      return native_->send_goal(goal_bytes, std::move(fb_cb),

lib/python_bindings/rtps_bindings.cpp:340

  • The Python ActionClient binding omits the existing C++ cancel_goal method, so Python clients cannot request cancellation at all. Expose cancellation and validate/convert the 16-byte goal ID returned by send_goal.
    python/rtps_bindings_smoke.py:29
  • This smoke test checks only the nested byte-level handles. The new typed surface in espp.rtps (ServiceServer, typed clients, ActionServer, and GoalHandle) can be absent or broken while the wheel still passes. Import espp.rtps and assert those classes and their public methods too.
    pc/tests/rtps_service_loopback.cpp:157
  • The deferred-call diagnostic reads the earlier synchronous reply, so it always prints that result instead of the deferred response being tested. This can report 42 even when dreply contains a different value.

Comment thread components/rtps_embedded/include/rtps/messages/MessageFactory.hpp
Comment thread components/rtps_embedded/src/rtps_participant.cpp
Comment thread components/rtps_embedded/src/rtps_participant.cpp Outdated
Comment thread components/rtps_embedded/src/rtps_participant.cpp
Comment thread components/rtps_embedded/src/rtps_participant.cpp
Comment thread components/rtps_embedded/example/main/rtps_embedded_example.cpp Outdated
Addresses the second PR #711 review batch (async RPC lifetime/leak issues):

- Service reply / action get_result no longer use-after-free on shutdown: a
  deferred ServiceResponder now holds a shared Liveness token and writes its
  reply only while holding its mutex with alive==true; stop() flips it false
  (under that mutex) before the domain/writers are destroyed, so a racing reply
  is a safe no-op. (participant.cpp:351)
- Action execute workers are owned (not detached), reaped as they finish, and
  joined in stop() before the domain is torn down, so no worker touches a
  destroyed participant. stop() is now a 4-phase teardown that joins without
  holding mutex_ (workers' final publish()/reply must be able to run to no-op).
  Cooperative callbacks get a cancel signal first. (participant.cpp:1337)
- Break the goals<->server shared_ptr cycle: the goal State holds a weak server
  ref and copies its feedback/status topics; goals are erased from the server
  map once their result is delivered, bounding map growth. (participant.cpp:696)
- Native action client buffers feedback/result that arrives before send_goal's
  reply installs the goal (bounded), replaying it on registration, so a fast
  native action no longer drops early messages. (participant.cpp:1213)
- RPC payloads carrying a related_sample_identity must fit one DATA (DATA_FRAG
  has no inline QoS, so fragmenting loses correlation): add
  MAX_UNFRAGMENTED_RPC_PAYLOAD, reject oversized request/reply at the facade,
  and guard both writers from fragmenting an RSI change. (MessageFactory.hpp:180)
- Example: cap the client demo at one in-flight action goal so an absent peer
  cannot leak a pending goal per timer tick. (example:206)

Gate: golden byte-for-byte unchanged, interop 27/27, native/self loopbacks and
python_rpc_demo (incl. stop()) all pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@finger563

Copy link
Copy Markdown
Contributor Author

Addressed the second review batch in c374237 — these were all async-RPC lifetime/leak issues, so I reworked the teardown and resource ownership coherently. Full gate is green (golden byte-for-byte unchanged, interop 27/27, native/self loopbacks + python_rpc_demo incl. stop() all pass).

rtps_participant.cpp:351 — deferred responder use-after-free. Added a shared Liveness{mutex, alive} token. ServiceResponder::reply() now writes only while holding that mutex with alive == true; stop() flips alive = false under the same mutex before the domain/writers are destroyed. A reply that races shutdown either completes against a still-valid writer or observes !alive and drops — never a freed writer. The token is a shared_ptr, so it outlives the participant.

rtps_participant.cpp:1337 — detached goal threads outliving stop(). Execute workers are now owned (not detached), stored per server context, reaped as they finish, and joined in stop() before the domain is torn down. stop() became a 4-phase teardown so the join happens without mutex_ held (a worker's final publish()/reply must be able to take mutex_/live_ and run to a no-op so it can exit). Accepted goals get a cancel signal first, so cooperative callbacks wind down — execute callbacks are expected to be finite / is_canceling()-aware, which I documented.

rtps_participant.cpp:696 — goals↔server cycle + unbounded map. The goal State now holds a weak_ptr back to the server and copies its feedback/status topic names (so publishing needs no server lock). Goals are erased from the server map once their result is delivered (either get_result answering a finished goal, or terminate() fulfilling a waiting get_result), bounding growth. A still-running worker keeps the State alive via its own reference, so the erase is safe.

rtps_participant.cpp:1213 — native action pre-registration race. The native client now buffers feedback/result that arrives for an unknown handle (bounded at 64 messages) and replays it when send_goal's reply installs the goal. A fast native action that publishes before its goal reply lands no longer drops early messages.

MessageFactory.hpp:180 — RPC + fragmentation loses correlation. A related_sample_identity DATA carries 60 bytes of inline QoS and cannot be fragmented (DATA_FRAG has no inline QoS → correlation lost → timeout). Added MAX_UNFRAGMENTED_RPC_PAYLOAD = MAX_UNFRAGMENTED_PAYLOAD − 60; the facade now rejects oversized service request/reply, and both StatefulWriter/StatelessWriter refuse to fragment an RSI change (drop rather than emit an uncorrelated fragment). Plain (non-RSI) samples still fragment normally — cross_process_frag_200k still passes.

rtps_embedded_example.cpp:206 — client demo leak with no peer. The demo now caps the action to one in-flight goal (an atomic flag reset on result), so an absent peer can no longer leak a pending goal per 5 s tick. The service call() already self-cleans on its 1 s timeout.

The native (espp<->espp) action path had no cancel channel, unlike the ROS 2
path. Add one, mirroring the ROS design:

- native_protocol: a '<action>/cancel' native service (request={goal_handle},
  reply={accepted}) alongside the existing send_goal service.
- Server: NativeGoalHandle gains is_canceling() + canceled(); the server tracks
  running goals by handle (weak) and the cancel service sets the flag, gated by
  an optional on_cancel callback. A long execute() polls is_canceling() and winds
  down with canceled().
- Client: NativeActionClient::send_goal exposes the assigned handle via an
  on_accepted callback; cancel_goal(handle) requests cancellation.
- Typed layer (rtps_action.hpp): ActionGoalHandle gains canceled() and native
  is_canceling() is wired; ActionClient gains cancel_goal() (tracks the most
  recent goal, works on both protocols).
- Python: bind is_canceling/canceled on NativeGoalHandle, cancel_goal +
  on_accepted on NativeActionClient, on_cancel on add_native_action_server, and
  cancel_goal on the ROS ActionClient; typed espp.rtps wrappers expose
  cancel_goal()/is_canceling()/canceled() uniformly.

Tests: native_action_loopback gains a cancel round-trip (long goal canceled
mid-flight -> CANCELED); rtps_rpc_demo adds a native_cancel leg; smoke test
covers the new binding surface.

Gate: golden byte-for-byte unchanged, interop 27/27 (incl. C++ + Python native
cancel), ROS interop + wheel demo all pass.

Co-Authored-By: Claude Opus 4.8 <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 59 out of 59 changed files in this pull request and generated no new comments.

Suppressed comments (11)

components/rtps_embedded/src/rtps_participant.cpp:1600

  • Shutdown requests cancellation for ROS goals, but this native branch immediately joins workers without setting their cancel_requested flags. A cooperative native execute callback that loops until is_canceling() (as the new examples recommend) can therefore make stop() block until natural completion or forever. Mark every live native goal canceling before joining, matching the ROS branch.
    components/rtps_embedded/src/rtps_participant.cpp:862
  • This always returns ERROR_NONE with an empty goals_canceling sequence, even when the goal is unknown or on_cancel rejects it. ROS 2 clients use both fields to determine whether cancellation was accepted, so they are told success while the goal keeps running. Encode the appropriate rejection/unknown/terminated return code, and include the accepted goal's GoalInfo when cancellation is accepted.
    components/rtps_embedded/src/rtps_participant.cpp:1524
  • Every client subscribes to the shared native feedback topic, so feedback for other clients' goal handles enters this “early” buffer and is never removed. After 64 such messages, the client drops its own legitimately early result/feedback, potentially leaving a fast goal permanently incomplete. Correlate feedback to the requesting client, or bound entries by outstanding goal requests with expiry/eviction rather than retaining every unknown handle.
    components/rtps_embedded/include/rtps/rpc/action_types.hpp:150
  • Splicing the nested result body here at CDR offset 4 silently produces invalid ROS 2 wire data whenever the result's first field requires 8-byte alignment. The generic byte-level and typed action APIs accept those valid message types, so documentation alone does not prevent corruption. Build the result envelope with alignment-aware CDR serialization, or reject/encode the unsupported alignment explicitly at the API boundary.
  if (detail::has_encap(result_cdr)) {
    v.insert(v.end(), result_cdr.begin() + 4, result_cdr.end());
  }

components/rtps_embedded/example/main/rtps_embedded_example.cpp:154

  • With the default embedded limits, this example cannot register this action server. The same participant already uses two stateful writers (pub/sub publisher plus service reply), while ActionServer adds five more; config_esp32.hpp permits only NUM_STATEFUL_WRITERS = 5. The later clients bring the total to 11 writers, also exceeding the per-participant cap of 10. The default example therefore returns at the validity check instead of demonstrating RMI/AMI. Select a larger limits profile in the example defaults or reduce/split its endpoints.
  espp::ActionServer<FibGoal, FibSeq, FibSeq> fib_action(

components/rtps_embedded/src/rtps_participant.cpp:619

  • The counter only overwrites two UUID bytes, so the stated uniqueness guarantee fails after 65,536 goals when random_device is deterministic or weak—exactly the MCU case this counter is meant to cover. Mix all four counter bytes into the UUID.

This issue also appears in the following locations of the same file:

  • line 862
  • line 1521
  • line 1597
    components/rtps_embedded/Kconfig:68
  • Disabling this option does not compile out the whole RPC layer as claimed. The engine-side CacheChange/ReaderCacheChange sample-identity fields, inline-QoS parsing, and writer emission paths are unconditional, so pure pub/sub builds still retain RPC code and per-history RAM overhead. Guard those engine additions with RTPS_NO_RPC, or narrow the option's documented savings.
            Compiles in the request/reply (services) and goal (actions) layers of
            the RtpsParticipant facade - both the ROS 2-interoperable
            (add_service_*/add_action_*) and native (add_native_*) variants.
            Disable to drop all of that code (and its std::thread/std::future use)
            when the device only needs pub/sub, saving flash. Pure pub/sub is

lib/python_bindings/rtps_bindings.cpp:285

  • The PR advertises Python bindings for the whole service surface, including deferred replies, but this exposes only the synchronous handler API; there is no ServiceResponder or add_service_server_deferred binding. Python users therefore cannot implement slow services without blocking an engine worker. Bind the deferred API (and cover it in the smoke/functional tests), or narrow the stated Python support.
    doc/en/protocols/rtps_rmi_ami.rst:172
  • The native implementation has separate goal and cancel services plus feedback, so it uses five DDS endpoints per participant, not roughly three. This undercounts the static pool/RAM requirement and contradicts the implemented topology. Document goal request/reply, cancel request/reply, and feedback explicitly (or fold cancel into the goal service as the design proposes).
    pc/tests/rtps_service_loopback.cpp:157
  • This diagnostic reads the earlier synchronous reply instead of the deferred dreply, so it can print a successful value even when the deferred response is missing or wrong. Report the response being tested.
    components/rtps_embedded/interop/run_interop.sh:271
  • This functional Python gate is not triggered by changes to the code it tests. .github/workflows/rtps_interop.yml:9-16 omits lib/python_bindings/rtps_bindings.cpp, lib/python_bindings/espp/rtps.py, and python/rtps_rpc_demo.py, so a binding-only regression can skip this round-trip entirely. Add those paths (and the smoke test/packaging inputs as appropriate) to the workflow filter.

@finger563
finger563 merged commit 555736b into main Aug 14, 2026
144 checks passed
@finger563
finger563 deleted the feat/rtps-services branch August 14, 2026 22:46
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