Skip to content

COMPAS FAB 2.0 release#458

Open
gonzalocasas wants to merge 457 commits into
mainfrom
prep-release
Open

COMPAS FAB 2.0 release#458
gonzalocasas wants to merge 457 commits into
mainfrom
prep-release

Conversation

@gonzalocasas
Copy link
Copy Markdown
Member

@gonzalocasas gonzalocasas commented May 19, 2026

image image

This PR superceedes #456 and contains the entire Project Theseus + ROS 2 / MoveIt 2 support.

What type of change is this?

  • Bug fix in a backwards-compatible manner.
  • New feature in a backwards-compatible manner.
  • Breaking change: bug fix or new feature that involve incompatible API changes.
  • Other (e.g. doc update, configuration, etc)

Checklist

Put an x in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code.

  • I added a line to the CHANGELOG.md file in the Unreleased section under the most fitting heading (e.g. Added, Changed, Removed).
  • I ran all tests on my computer and it's all green (i.e. invoke test).
  • I ran lint on my computer and there are no errors (i.e. invoke lint).
  • I added new functions/classes and made them available on a second-level import, e.g. compas_fab.robots.CollisionMesh.
  • I have added tests that prove my fix is effective or that my feature works.
  • I have added necessary documentation (if appropriate)

gonzalocasas and others added 3 commits May 29, 2026 09:53
The old default sat on top of the rosbridge port used when remapping a
ROS 2 stack to coexist with a ROS 1 stack (rosbridge 9090 + ROS 2 rosbridge
9091). A user pointing the loader at their local ROS 2 stack got HTTP 404s
because the loader was hitting the bridge, not the file server.

9190 stays mnemonic ("near rosbridge") while sitting clear of the 909x
cluster where ROS-adjacent stuff tends to land. Both reference docker
stacks (integration tests and the ros2-ur10e-demo) default the host AND
container-internal port to 9190 so the stack speaks one number end to end.

The ROS 2 rosbridge port (9091) is unchanged — only the file server moved.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The Unreleased section had grown four ### Changed, two ### Fixed, two
### Added, and two ### Removed blocks as separate prep-release patches
were merged. Collapsed each Keep-a-Changelog category into a single
heading and dropped the custom \"Fixed (examples)\" heading by folding
example fixes into the regular ### Fixed list. No entries added or
removed; pure reorganization.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The official ros:jazzy image is smaller (no desktop metapackage we don't
use — moveit/ur/rosbridge/zenoh are installed explicitly anyway) and
publishes multi-arch images including arm64, which lets Apple Silicon
hosts run the demo natively.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@gonzalocasas gonzalocasas mentioned this pull request May 29, 2026
10 tasks
Credit for contributions in #449
(not merged as-is, but the author is recognized in the project's author list
and CITATION metadata).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
gonzalocasas and others added 12 commits May 29, 2026 11:09
Atomic builders that let users construct a RobotCell and RobotCellState
from scratch on the canvas, plus the missing waypoint and Configuration
helpers needed to drive Cartesian motion planning.

New components (Robot Cell):
  - Cf_LoadRobotCellFromUrdfSrdf  (offline URDF/SRDF loader)
  - Cf_RigidBodyFromMesh          (Rhino mesh -> RigidBody)
  - Cf_RigidBodyFromLibrary       (RigidBodyLibrary entries)
  - Cf_ToolFromLibrary            (ToolLibrary entries)
  - Cf_AddToolToCell              (passthrough builder)
  - Cf_AddRigidBodyToCell         (passthrough builder)

New components (Cell State):
  - Cf_AttachToolToRobot          (set_tool_attached_to_group)
  - Cf_AttachRigidBodyToTool      (set_rigid_body_attached_to_tool)
  - Cf_AttachRigidBodyToLink      (set_rigid_body_attached_to_link)
  - Cf_SetRigidBodyFrame          (place static body at WCF frame)
  - Cf_SetTouchLinks              (allowed-collision links)

New components (Targets):
  - Cf_FrameWaypoints             (planes -> FrameWaypoints)
  - Cf_PointAxisWaypoints         (points + axes -> PointAxisWaypoints)
  - Cf_Configuration              (joint values + names + types -> Configuration)

All cell-state builders use deepcopy to keep the input state immutable —
chaining Attach*/Set* components composes cleanly without surprise.

Placeholder icons recycled as before.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…s compas_fab.robots

The Sphinx -> MkDocs Material migration left RST role markup in the
docstrings. This sweep converts the entire compas_fab.robots module
(and its reachability_map subpackage) to mkdocstrings syntax:

  :class:`compas_fab.robots.Foo`    -> [\`Foo\`][compas_fab.robots.Foo]
  :class:`~compas_fab.robots.Foo`   -> [\`Foo\`][compas_fab.robots.Foo]
  :meth:`Foo.bar()`                  -> \`Foo.bar()\` (unqualified - no cross-ref)
  :meth:`compas_fab.robots.Foo.bar` -> [\`Foo.bar\`][compas_fab.robots.Foo.bar]
  :attr:`compas_fab.robots.Foo.bar` -> [\`Foo.bar\`][compas_fab.robots.Foo.bar]
  :obj:`bool/float/str/...`          -> \`bool\`/\`float\`/\`str\`/...
  :exc:`ValueError`                  -> \`ValueError\`
  :ref:`targets`                     -> \`targets\`

Also fixes RST hyperlinks (\`Label <url>\`_) to markdown ([Label](url)),
the singular typo \`compas_fab.robot.plan_motion\` -> \`compas_fab.robots...\`,
and a handful of stale cross-references to the removed \`Robot\` class
(updated to point at \`RobotCell\`).

509 roles converted across 14 files. Net diff is roughly neutral on
line count (-492/+478). All files still parse.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
\`uv run invoke docs\` was emitting 6 warnings from the doc build:

- trajectory.py: \`compas_robots.Joint.TYPE\` / \`.REVOLUTE\` cross-refs.
  Joint lives at \`compas_robots.model.Joint\`, but class-level attributes
  aren't in compas_robots' Sphinx inventory. Switched to plain backticks
  since cross-linking external class attributes isn't supported.
- semantics.py: stale refs to \`PyBulletClient.load_semantics\` and
  \`RosClient.load_robot\` (both methods removed/renamed during the API
  redesign). Replaced with a single working ref to \`RosClient.load_robot_cell\`.
- targets.py: refs to \`compas_fab.robots.plan_motion\` /
  \`plan_cartesian_motion\`. These were never top-level functions; the
  methods live on planner backends. Reworded to "the planner's plan_motion
  method".
- robot_library.py: ref to instance attribute \`RobotCellState.tool_states\`.
  mkdocstrings can't resolve instance attributes set in __init__; kept the
  class link and made \`.tool_states\` a plain trailing access.

Docs build now warning-free.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…lToRobot

Attaching a tool with no `group` wired in was a silent no-op — confusing for
the common case where the user has a single-group robot (UR, ABB, Panda)
and shouldn't have to look up the group name.

Added an optional `robot_cell` input. When `group` is empty:
  - if `robot_cell` is wired, fall back to `robot_cell.main_group_name`
  - otherwise, raise with a message that points at the fix.

Reported during Phase 3 testing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…matics

Swallowing the exception with `print(...) + return None` hid the failure —
on the canvas you got an empty output and a console line that's easy to
miss. Removed the try/except so the exception bubbles up; Grasshopper
will then turn the component red and show the message in its tooltip.

Reported during Phase 3 testing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…nents

Replaces silent exceptions and console prints with Grasshopper-native
runtime messages (component balloons), so failures are visible without
needing the user to wire an `error` output or watch the console.

Components updated to use error() instead of raise / print:
  Cf_AddRigidBodyToCell, Cf_AddToolToCell, Cf_AnalyticalKinematicsPlanner,
  Cf_AttachToolToRobot, Cf_Configuration, Cf_InverseKinematics,
  Cf_LoadRobotCellFromLibrary, Cf_LoadRobotCellFromRos,
  Cf_PointAxisWaypoints, Cf_RigidBodyFromLibrary, Cf_ToolFromLibrary.

Components updated to use warning() (partial success) + error() (no result):
  Cf_PlanMotion, Cf_PlanCartesianMotion.

Cf_RosClient: warning() when the previous client fails to close cleanly,
remark() when ROS distro detection fails (informational).

Also adds a `visual_meshes` output to Cf_ToolFromLibrary so users can preview
the tool's geometry (Rhino meshes, drawn in the tool's own base frame) before
attaching it to a robot.

Reported during Phase 3 testing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…mLibrary

Adds compas_fab.ghpython.ensure_value_list, a reusable helper that
auto-creates a Grasshopper Value List on an unconnected component input.
Cf_LoadRobotCellFromLibrary now uses it to expose every RobotCellLibrary
entry as a dropdown (default "ur5") instead of a free-text panel.
Loader calls are wrapped in try/except and surfaced via compas_ghpython.error
with the sticky cache cleared on failure, so retries actually retry.

Also fixes ToolLibrary.cone(): it now calls add_link("cone_link", ...) like
ToolLibrary.printing_tool() does. Without the link, the cone's underlying
ToolModel.root tree was malformed and iter_joints() crashed with
"'NoneType' object has no attribute 'joints'". This is what made
RobotCellLibrary.ur5_cone_tool(load_geometry=False) always crash and
manifested in the GH component when toggling load_geometry off while
clicking through robot names.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@jf---
Copy link
Copy Markdown

jf--- commented May 29, 2026

witnessing you pushing HARD Mr @gonzalocasas
one EPIC PR... hang in there ;)

gonzalocasas and others added 11 commits May 30, 2026 01:27
ensure_value_list wired the new GH_ValueList to the input during the
current solve, but the input's value collection had already happened, so
the dropdown's selection didn't reach the component until the user
manually re-triggered the solver. Calling doc.ScheduleSolution(5)
immediately after AddSource queues a fresh solve so the value flows on
the very first drop with no user intervention.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds compas_fab.ghpython.ensure_joint_sliders, a helper that on first
solve (or whenever the wired robot's joint signature changes) registers
one Param_Number input per configurable joint and auto-creates a
GH_NumberSlider feeding each input, with min/max set to the joint's URDF
limits (±2π fallback for continuous joints, ±1m for prismatic with no
declared limit). Joint signature is tracked in sticky so swapping
robot_cell rebuilds the bank rather than appending to it; obsolete
sliders are removed from the document on rebuild. doc.ScheduleSolution(5)
is used so values flow on the first drop.

The new Cf_RobotConfiguration component (subcategory Targets) takes one
robot_cell input, returns a Configuration with joint_names and
joint_types populated from the cell — the ergonomic counterpart to the
existing Cf_Configuration, which stays as the manual-list escape hatch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The CPython userobjects build (invoke build-cpython-ghuser-components,
run in CI by the build-cpython-components job) requires icon.png in every
component folder; without it the build fails. Used Cf_Configuration's
icon as a placeholder since the two components are siblings in subcategory
Targets — can be swapped for a dedicated icon later.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
integration.yml brings up two docker compose stacks (ROS 1 on 9090, ROS 2
on 9091) with `up -d --build` and immediately runs pytest. On cold-cache
runs the rosbridge containers aren't accepting websocket connections yet
when `--doctest-modules` reaches `RosClient`'s `>>> with RosClient()`
example at client.py:162, and roslibpy's 10s connection timeout fires
with `RosTimeoutError('Failed to connect to ROS')`. The dedicated
tests/backends/ros/test_ros_client.py cases pass because they run later
in the session when the bridge is warm.

Added a `Wait for rosbridge endpoints` step that TCP-probes ports 9090
and 9091 on localhost for up to 120s before pytest. 120s is well above
the worst observed cold-start; hitting it means the container itself is
unhealthy, in which case `docker ps -a` output is dumped before exit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ation

Temporarily point requirements.txt at the lifecycle-fixes branch on
gramaziokohler/roslibpy to exercise the 2.1 candidates (singleton
twisted log observer, Ros.close() blocking until clientConnectionLost)
against the integration workflow. If the RosClient doctest at
client.py:162 stops flaking under --doctest-modules, the upstream
roslibpy fixes are doing their job and we revert this pin back to
roslibpy >= 2.0, < 3 once 2.1 is released.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The two >>> with RosClient() examples in compas_fab.backends.ros.client
(RosClient class docstring at line 162; RosClient.load_robot_cell at
line 257) keep flaking under pytest --doctest-modules in the integration
workflow with `RosTimeoutError('Failed to connect to ROS')` — always on
the FIRST one, with the second succeeding milliseconds later. Marking
both # doctest: +SKIP eliminates the flake; the actual code path is
exercised by tests/backends/ros/test_ros_client.py (9 cases under the
module-scoped ros1_client fixture, never observed to flake), so the
doctests are only losing their role as a self-verifying illustration.

Also reverts the temporary `roslibpy @ git+...#lifecycle-fixes` pin
back to `roslibpy >= 2.0, < 3`. The pin was added to validate two
candidate roslibpy 2.1 fixes (singleton log observer, close() blocking
until clientConnectionLost) against this exact symptom — the
integration run with that branch failed identically, falsifying the
hypothesis that this flake was caused by either of those bugs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…yncio

Pin roslibpy to the websockets-asyncio branch (new asyncio/websockets
transport) and opt the integration job into it via ROSLIBPY_TRANSPORT.
Drop the two `# doctest: +SKIP` markers on the RosClient examples so
we actually exercise the live path again.

Validation only — not for merge as-is. Reverts the temporary +SKIP from
2cc6602 and the lifecycle-fixes pin from b64d020.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Previous design registered one dynamic Param_Number per joint and tried to
read slider values via `param.VolatileData`. Dynamic input params on a
`GH_ScriptInstance` don't get `CollectData()` called by the parent
component's solve, so the values never flowed through and the resulting
Configuration was always the per-joint defaults regardless of slider
position.

Redesigned so the component declares a single static `joints` list-access
input; `ensure_joint_sliders` creates one `GH_NumberSlider` per
configurable joint and wires them all to that input in canonical order.
GH collects the values via the standard solve path and the script reads
`joints` as a plain Python list. No more VolatileData or sticky-tracked
GUID lookups.

Also retires Cf_Configuration — once Cf_RobotConfiguration works from a
RobotCell with auto-sliders and auto names/types, hand-typing joint
names + integer joint-type enums in a separate component is just busywork.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`joints` is a list-access input, so its arg must be annotated as
`System.Collections.Generic.List[float]` for GH to collect it correctly
(same pattern as Cf_PointAxisWaypoints). Also dropped the redundant
`joints[: len(metadata)]` slice — the upstream wiring already guarantees
the lengths match, and the fallback branch above handles the only case
where they wouldn't.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ally work

Four Rhino 8 CPython quirks were causing the auto-slider helper to
either no-op silently or accumulate dozens of duplicate sliders per
robot switch. Each one is its own .NET/Python interop gotcha:

1. `isinstance(obj, Grasshopper.Kernel.Special.GH_NumberSlider)` returns
   False for actual slider instances in Rhino 8 CPython (the .NET interop
   surfaces `IGH_DocumentObject` as the Python type). Replaced with
   `_is_number_slider(obj)` using `obj.GetType().Name == "GH_NumberSlider"`.
   Without this, neither `_strip_slider_sources` nor the
   `has_any_slider_source` check ever recognised a slider, so every solve
   re-entered the rebuild branch and added 8 more sliders.

2. `IGH_Param.RemoveSource(IGH_Param)` is a silent no-op for our case
   (probably marshalling); `RemoveSource(Guid)` works. Strip now uses the
   Guid overload — verified going from 72 sources to 0 in one pass.

3. `component.ExpireSolution(False)` called inside our own SolveInstance
   is silently reset when the solve unwinds, so the deferred
   `ScheduleSolution(5)` had nothing to re-solve. Switched to the
   `ScheduleSolution(delay, callback)` overload where the callback fires
   right before the deferred solve and expires the component then.
   Without this, the first solve created the sliders but their values
   only flowed after the user manually pressed Play.

4. `doc.AddObject` and `Params.OnParametersChanged` during the rebuild
   trigger nested solves of this same component. Each nested call saw
   the still-stale sticky signature and re-entered the rebuild branch,
   adding N sliders per joint per nesting level (6 × 9 = 54 sliders
   after one robot switch). Added a `__busy` sticky flag as a re-entrance
   guard.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.

5 participants