Skip to content

Add ros2 nodl describe: rosgraph_msgs/Node -> NoDL document (#53)#88

Draft
luke-alloy wants to merge 18 commits into
ros-tooling:mainfrom
lsy3:lsy3/describe
Draft

Add ros2 nodl describe: rosgraph_msgs/Node -> NoDL document (#53)#88
luke-alloy wants to merge 18 commits into
ros-tooling:mainfrom
lsy3:lsy3/describe

Conversation

@luke-alloy

Copy link
Copy Markdown

Implements #53 — the interpret half of Observe/Describe: a pure transform from a captured rosgraph_msgs/Node into a validated NoDL document, behind ros2 nodl describe (live, or --from yaml|mcap).

⚠️ Waiting on observe (#83) — do not merge first. This is stacked on lsy3/observe and drives the C++ observe binary, so the diff currently includes #83's commits. Merge after #83 lands; I'll then rebase onto main for a describe-only diff. Kept in draft until then.

What it does

  • describe/: pure, duck-typed _qos / _endpoints / _parameters / _filter / _convert + _source (live binary or from-file) + a thin verb.
  • Drops observation-only noise, maps RMW QoS ints → NoDL enums, recovers action types (_SendGoal strip), filters framework infrastructure; output validates against the schema.
  • Best-effort draft generator: recoverable fields are filled, genuinely-unknowable ones are reported as gaps; --strict fails on any gap. Flags: --from / --no-params / --keep-hidden / --strict / --raw / -o.

Testing

  • tools/run_describe_tests.sh runs the real suite in the ROS image: 272 passed / 7 skipped (skips = live-observe smoke tests that need the built observe binary → CI).
  • Pure tests run with no ROS via committed stub_msgs; 20 golden fixtures across 5 distro/RMW pairs; a real-vs-stub fidelity guard + RMW/ParameterType constant-drift guards.

Mapping reference: nodl/doc/describe.md.

lsy3 added 18 commits June 7, 2026 19:29
Stage one of the Observe -> Describe pipeline (ros-tooling#68): observe_node() fills
a rosgraph_msgs/Node from the live graph - topics with actual QoS and RIHS
type hash, services with honest *_UNKNOWN QoS (not observable externally),
actions folded from their hidden _action/* constituents, and parameter
descriptors/values via the target's parameter services with graceful
degradation under a shared timeout ceiling.

Tested in three layers: pure-builder unit tests (no executor), scenario
graphs diffed against per-distro golden YAML files, and ROS-free rendering
tests fed by those goldens. The goldens double as input fixtures for
Describe (ros-tooling#53). Requires rosgraph_msgs >= 2.0.4 (Node.msg).

Signed-off-by: Luke Sy <sylukewicent@gmail.com>
Observes the target node via nodl_observe, prints the rosgraph_msgs/Node
as YAML to stdout (or -o FILE with the format inferred from the .yaml/.yml/
.json extension), and publishes it latched (reliable, transient_local,
keep_last(1)) on /nodl/observed_node.

Publish-once semantics: delivery to currently-matched subscribers is
confirmed via wait_for_all_acked, bounded by --timeout; the latched history
dies with the publisher, so consumers must subscribe before the verb runs -
locked in by the smoke test. --no-params skips the only part of observation
that contacts the target.

Signed-off-by: Luke Sy <sylukewicent@gmail.com>
nodl_observe's tests are distro-aware: golden files live under
test/expected/<ROS_DISTRO>/ (jazzy committed), and every test module skips
cleanly on distros whose rosgraph_msgs predates 2.0.4 (no Node.msg), so the
existing humble..rolling matrix stays green and flips tests on per distro
as the rosgraph_msgs sync lands.

Signed-off-by: Luke Sy <sylukewicent@gmail.com>
Signed-off-by: Luke Sy <sylukewicent@gmail.com>
__init__ now just re-exports the public surface (observe_node,
NodeNotFoundError, latched_qos); the graph polling and endpoint collection
move to _observe, matching the _-prefixed private-module pattern already
used for _endpoints/_parameters/_qos.  No behaviour change.

Signed-off-by: Luke Sy <sylukewicent@gmail.com>
Adds rmw_cyclonedds_cpp alongside rmw_fastrtps_cpp in CI (distro x RMW
matrix) and proves they observe QoS differently: cyclonedds propagates a
remote endpoint's history policy and depth over discovery, fastrtps does
not (history -> UNKNOWN, depth -> 0).  Goldens are now resolved
most-specific-first under expected/<distro>/<rmw>/, falling back to a shared
expected/<distro>/ when RMWs agree, so identical sets are stored once.

Commit a single YAML golden per case (the canonical, human-readable form);
the JSON renderer is proven by an equivalence test instead of a duplicate
JSON golden.  The one RMW-divergent assertion is now keyed by RMW so each
middleware's behaviour is locked in explicitly.

Signed-off-by: Luke Sy <sylukewicent@gmail.com>
Node.msg ships on the jazzy line (>= 2.0.4) but is not yet in kilted
(rosgraph_msgs 2.3.1 = Clock only) or current rolling, so version numbers
are not comparable across distros.  The package builds regardless; the
import guard skips observation tests where Node.msg is absent.

Signed-off-by: Luke Sy <sylukewicent@gmail.com>
The graph messages landed in rolling first and were backported to jazzy
(the only distro shipping them in a pullable image today).  The earlier
'rolling lacks them' note was an artifact of probing the EOL Ubuntu 24.04
rolling image (frozen at 2.4.4); live rolling/lyrical run on Ubuntu 26.04
images that are not published yet.  Kilted's backport release simply has
not been cut (2.3.1 ships Clock only).

Signed-off-by: Luke Sy <sylukewicent@gmail.com>
The RMW axis is now driven entirely by the CI matrix 'rmw:' list: the
install step derives each apt package name (rmw_x_cpp -> ros-<distro>-rmw-x-cpp),
the golden resolver is already (distro, RMW)-keyed, and the per-RMW
history-over-discovery expectation is a documented _HISTORY_OVER_DISCOVERY
map.  Adding an RMW is 'drop in goldens' -- the harness needs no per-RMW
setup (every scenario runs in one process / one session).

Also closes the silent-skip trap: observation tests importorskip when
rosgraph_msgs lacks Node.msg, which on a distro that *should* support observe
reads as green-having-tested-nothing.  A best-effort step pulls Node.msg from
ros2-testing where it leads the main index (e.g. jazzy 2.0.4), and a
matrix-gated assertion (requires_node_msg) fails the leg loudly if Node.msg
is still missing where it is required.

Signed-off-by: Luke Sy <sylukewicent@gmail.com>
Zenoh slots in with no harness change -- scenarios run in one process / one
session, so it discovers without a router daemon.  Behaviourally it matches
cyclonedds (propagates history and depth over discovery; identical endpoint
set and type hashes to the other RMWs).  Adds its (jazzy) goldens and its
_HISTORY_OVER_DISCOVERY entry.

Signed-off-by: Luke Sy <sylukewicent@gmail.com>
Node.msg has now landed across jazzy (2.0.4), kilted (2.3.2), lyrical
(2.4.5) and rolling (2.5.0) -- all via the best-effort ros2-testing install
where it leads the main index -- so all four are flagged requires_node_msg
and run the observation suite (humble still skips; no Node.msg yet).

Empirically, every (distro, RMW) observes the same Node *except* two gaps:
cyclonedds reports a KEEP_ALL queue's depth as 0 (every distro), and jazzy's
older fastrtps drops history/depth entirely (kilted-onward fastrtps does
propagate).  Goldens are deduplicated to match: a single _base/ holds the
full observation, with overrides only where a combination differs --
rmw_cyclonedds_cpp/s2 and jazzy/rmw_fastrtps_cpp/.  The resolver searches
<distro>/<rmw>/ -> <rmw>/ -> _base/.  Nine files now cover 4 distros x 3
RMWs x 4 scenarios (verified green on noble and resolute).

Drops the _HISTORY_OVER_DISCOVERY assertion map: history/depth is
(distro, RMW)-specific (fastrtps differs by distro), and the per-combination
golden already locks it exactly.

Signed-off-by: Luke Sy <sylukewicent@gmail.com>
Node.msg has now reached every distro including humble (rosgraph_msgs 1.2.3
via ros2-testing), so the package gets exercised on pre-Iron rclpy for the
first time -- and it did not import there.  Two defensive fixes make it
import-safe everywhere: map the BEST_AVAILABLE QoS enum only where rclpy
defines it (added in Iron), and read TopicEndpointInfo.topic_type_hash via
getattr (REP-2011, Iron+) so an absent hash is simply left unset like a
service's.

Full observation still needs Iron+ (type hashes, BEST_AVAILABLE, and an
int32-safe infinite QoS deadline that humble's builtin_interfaces overflows
on), so the observation/rendering tests and the describe smoke tests are
capability-gated to Iron+ (BEST_AVAILABLE presence as the proxy).  Humble's
CI leg now builds, runs the pure-argument and serialization tests, and skips
the rest -- green instead of crashing.  Full pre-Iron support is tracked as
a working-group follow-up.  No change on Iron+ (jazzy 154/0).

Signed-off-by: Luke Sy <sylukewicent@gmail.com>
Replace the ament_python nodl_observe with an ament_cmake C++ package: a
reusable observe_node(...) library plus an `observe` executable that
latch-publishes the observed rosgraph_msgs/Node on /nodl/observed_node.
The Python implementation is kept locally as an untracked reference.
Addresses the WG review on this PR (points ros-tooling#2-ros-tooling#5 of plan_observe_cpp.md).

- Port the pure builders 1:1 (QoS enum mapping, topic/service endpoints
  with REP-2011 type hash, action folding, parameter pairing, FQN split),
  with gtest unit tests mirroring the Python tests.
- Actions use the rcl_action C API directly (no rclcpp_action wrapper).
- Parameters via AsyncParametersClient driven by a short-lived executor,
  with graceful degradation on an unresponsive target (covered by a test).
- Canonicalise infinite/overflow QoS durations to {INT32_MAX, 0} uniformly
  on every distro -- CDR-valid for MCAP and fixes Humble's int32 overflow.
- Humble (pre-Iron) is a supported, tested runtime target: the type hash
  and BEST_AVAILABLE QoS enum are compiled out via ROS2_<DISTRO>; the
  message stays structurally identical, differences live only in unfilled
  fields.
- Replace the ~2k-line YAML goldens with MCAP fixtures (one per
  (distro, RMW), most-specific-first resolver: <distro>_<rmw> -> <rmw> ->
  base) plus a human-readable print/diff helper. Verified field-for-field
  parity with the rclpy output (only the duration sentinel differs).
- Rewire `ros2 nodl describe` to shell out to the `observe` binary and
  render via rosidl_runtime_py.
- CI: one job per distro, build once, re-run the RMW-sensitive integration
  test over fastrtps/cyclonedds/zenoh; drop requires_node_msg.

Validated in Docker on humble, jazzy, kilted, and lyrical (build + gtest
unit tests + integration across each distro's RMWs).

Signed-off-by: Luke Sy <sylukewicent@gmail.com>
- ARCHITECTURE.md: layered data-flow diagram, module table, and the
  observe_node step-by-step for contributors.
- mcap_fixtures.py: add node_to_json + 'print -f yaml|json' so the
  fixture viewer matches the verb's -o output (was YAML only).

Signed-off-by: Luke Sy <sylukewicent@gmail.com>
…_msgs on humble

- rosidl_runtime_py was transitive via the old Python nodl_observe; the
  C++ rewrite dropped it, but the integration test (mcap_fixtures.py) and
  the describe verb still use it -> declare it (+ rclpy, ament_index_python
  for the verb, which the C++ lib no longer provides transitively).
- humble's rosgraph_msgs ships Node.msg only via ros2-testing; the bridge
  used --only-upgrade, a no-op when the package isn't pre-installed (the
  rostooling image), so rosdep then pulled the main version without Node.msg
  and the C++ build failed. Plain install + apt-mark hold instead.

Signed-off-by: Luke Sy <sylukewicent@gmail.com>
- Bridge silently no-op'd on humble: the keyring glob ros2*archive-keyring
  missed the actual ros-archive-keyring.gpg, so '[ -n key ] || exit 0' bailed
  and rosdep then installed main rosgraph_msgs (no Node.msg). Broaden the glob
  and fall back to [trusted=yes] instead of skipping.
- Per-RMW integration steps passed the test but 'colcon test-result --all'
  defaulted --test-result-base to 'build'; the action-ros-ci workspace is
  'ros_ws/build'. Point it there.

Signed-off-by: Luke Sy <sylukewicent@gmail.com>
The CI image (rostooling/setup-ros-docker) installs ros2cli but not the
ros2run package, so 'ros2 run rmw_zenoh_cpp rmw_zenohd' errors with
"invalid choice: 'run'"; the zenoh router never starts, cross-process
discovery fails, and every observation times out (this is why jazzy/kilted/
lyrical/rolling failed their zenoh leg while humble -- which has no zenoh --
passed).  Locate and exec the rmw_zenohd binary directly.

Validated end-to-end in the actual rostooling images: jazzy
(fastrtps/cyclonedds/zenoh all 14 passed) and humble (fastrtps/cyclonedds).

Signed-off-by: Luke Sy <sylukewicent@gmail.com>
…oling#53)

Implements the interpret half of the Observe/Describe split: a pure,
duck-typed transform from a captured rosgraph_msgs/Node into a validated
NoDL document. observe records everything unfiltered; describe interprets.

- describe/ package: _qos, _endpoints, _parameters, _filter, _convert
  (pure, no rclpy/rosgraph_msgs imports) + _source (live observe binary
  or --from yaml|mcap) + a thin verb.
- Best-effort draft contract: reliably-recoverable fields are filled;
  genuinely-unknowable fields become Gaps. The verb warns the gap list and
  exits 0; --strict fails. Flags: --from/--no-params/--keep-hidden/--strict/
  --raw/--topic/-o.
- Mapping: QoS int->enum (UNKNOWN policy, INT32_MAX duration sentinel,
  conditional depth), action-type recovery (strip _SendGoal/_GetResult),
  the infrastructure denylist, and parameter type/value/range.
- nodl_schema: export rostype_pattern() so the endpoint type regex has a
  single source of truth instead of a hardcoded copy in describe.
- Tests: stub_msgs duck-typed builders (pure tests run with no ROS),
  real-message tests, a real-vs-stub fidelity guard, RMW/ParameterType
  constant-drift guards, and 20 golden fixtures across 5 distro/RMW pairs.
- tools/run_describe_tests.sh: Docker harness that runs the real suite
  against the rosgraph_msgs fork (no ROS install needed on the host).
- Docs: nodl/doc/describe.md page + the describe lane in ARCHITECTURE.md.

Stacked on lsy3/observe (it drives the C++ observe binary); land after
observe merges, then rebase onto main for a describe-only diff.
@read-the-docs-community

Copy link
Copy Markdown

Documentation build overview

📚 nodl | 🛠️ Build #33277364 | 📁 Comparing c408a9a against latest (9bd4a26)

  🔍 Preview build  

3 files changed
+ describe.html
± index.html
± schema.html

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants