feat(rtps_embedded): ROS 2-interoperable services (RMI) + actions (AMI) + native protocol - #711
Conversation
… 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>
|
✅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>
There was a problem hiding this comment.
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
ActionClientbinding exposessend_goal()but notcancel_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.
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>
|
Addressed the review comments and static-analysis findings in 8d50039. Static analysis (cppcheck):
Review comments:
Gate: interop 27/27 (golden byte-identical, refactored |
There was a problem hiding this comment.
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_NONEwith an emptygoals_cancelinglist. 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 ingoals_cancelingand 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 onlysucceed()andabort(). It cannot terminate the goal as CANCELED, even though the byte-level handle supportscanceled(), so cancellation cannot be completed through the recommended typed API. Add a typedcanceled(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_goalresult tobooldiscards the generated GoalId, and this wrapper exposes nocancel_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
ActionClientbinding omits the existing C++cancel_goalmethod, so Python clients cannot request cancellation at all. Expose cancellation and validate/convert the 16-byte goal ID returned bysend_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, andGoalHandle) can be absent or broken while the wheel still passes. Importespp.rtpsand 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 report42even whendreplycontains a different value.
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>
|
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 +
|
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>
There was a problem hiding this comment.
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_requestedflags. A cooperative native execute callback that loops untilis_canceling()(as the new examples recommend) can therefore makestop()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_NONEwith an emptygoals_cancelingsequence, even when the goal is unknown oron_cancelrejects 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'sGoalInfowhen 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
ActionServeradds five more;config_esp32.hpppermits onlyNUM_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_deviceis 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/ReaderCacheChangesample-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 withRTPS_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
ServiceResponderoradd_service_server_deferredbinding. 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
replyinstead of the deferreddreply, 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-16omitslib/python_bindings/rtps_bindings.cpp,lib/python_bindings/espp/rtps.py, andpython/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.
Adds services (RMI) and actions (AMI) to the
rtps_embeddedcomponent, inboth 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
(
add_service_server/_client) and typed (espp::ServiceServer<Req,Resp>/ServiceClient<Req,Resp>). Three client call styles: blockingcall,call_async,call_future. Deferred-reply support (ServiceResponder) forslow responses.
(3 services + 2 topics; no new wire primitive). Byte-level and typed
(
ActionServer/ActionClient/ActionGoalHandle).correlation header,
es_rq/es_rrtopics, ~3 endpoints per action vs ROS's~10), same typed wrappers via a
RtpsProtocolconfig field.rtps_service.hpp/rtps_action.hpp, the RMI/AMI analogueof
Publisher<T>/Subscriber<T>: reflectable structs (de)serialized by thecdrcomponent, no manual bytes.python/rtps_rpc_demo.py.doc/en/protocols/rtps_rmi_ami.rst(function, use, rationale, wireformat, when to choose native); refreshed component + example READMEs.
RTPS_ENABLE_RPC(default on) compiles the whole services/actionslayer out for pure-pub/sub devices.
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_identityinline QoS under bothPID
0x0083(OMG) and0x800f(eProsima), present on both request and reply(not the
0x8002/request-only guess). Result: no SEDP type-hash is needed — anespp service/action even appears in
ros2 service list/ros2 action list.Testing
data_related_sample_identitysection (all prior bytes unchanged).
rtps_embedded/**):in-process loopbacks (pub/sub, services, actions, native, typed), byte-level
codec unit tests, live
ros2 service call/ros2 action send_goalbothdirections, and a functional Python demo (5/5) built in-container.
and the example (now in the CI build matrix).
Notes for reviewers
correlation; plain pub/sub is byte-identical (proven by golden + interop).
libc++ (forward-declared context in a
unique_ptrmember) — fixed; a goodargument for keeping the wheel build in the review loop.
main.cpp→rtps_embedded_example.cppto prepare forretiring the old
rtpscomponent in a follow-up.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