Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions .github/workflows/robot-cell-snapshots.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
name: robot-cell-snapshots

on:
workflow_dispatch:
push:
branches:
- main
pull_request:
branches:
- main

permissions:
contents: read

concurrency:
group: robot-cell-snapshots-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
generate:
name: Generate robot-cell GLB snapshots
runs-on: ubuntu-latest
timeout-minutes: 45

steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: pip
cache-dependency-path: |
requirements.txt
requirements-dev.txt

- name: Bring up ROS 1 and ROS 2 integration stacks
run: |
docker compose -f tests/integration_setup/docker-compose.yml up -d --build
docker compose -f tests/integration_setup/docker-compose-ros2.yml up -d --build
docker ps -a

- name: Install project and snapshot dependencies
run: |
python -m pip install --upgrade pip wheel
python -m pip install --no-cache-dir -e ".[dev]"

- name: Generate local robot-cell snapshots
run: |
python scripts/generate_robot_cell_gltf_snapshots.py \
--cells all \
--output-dir artifacts/robot-cell-snapshots/local

- name: Generate ROS 1 robot-cell snapshots
env:
ROSLIBPY_TRANSPORT: asyncio
run: |
python scripts/generate_robot_cell_gltf_snapshots.py \
--solvers ros \
--ros-host localhost \
--ros-port 9090 \
--output-dir artifacts/robot-cell-snapshots/ros1

- name: Generate ROS 2 robot-cell snapshots
env:
ROSLIBPY_TRANSPORT: asyncio
run: |
python scripts/generate_robot_cell_gltf_snapshots.py \
--solvers ros \
--ros-host localhost \
--ros-port 9091 \
--ros-http-file-server http://localhost:9190 \
--output-dir artifacts/robot-cell-snapshots/ros2

- name: Upload GLB snapshots
if: always()
uses: actions/upload-artifact@v5
with:
name: robot-cell-glb-snapshots
path: |
artifacts/robot-cell-snapshots/**/*.glb
artifacts/robot-cell-snapshots/**/manifest.json
if-no-files-found: error
retention-days: 14

- name: Show ROS container logs on failure
if: failure()
run: |
docker compose -f tests/integration_setup/docker-compose.yml logs --no-color
docker compose -f tests/integration_setup/docker-compose-ros2.yml logs --no-color

- name: Tear down ROS integration stacks
if: always()
run: |
docker compose -f tests/integration_setup/docker-compose.yml down
docker compose -f tests/integration_setup/docker-compose-ros2.yml down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Migrating

Three changes in this release alter results rather than the API. Code that compensated for the old behaviour needs the compensation removed.

* **Tools from `ToolLibrary` mount along +Z.** If you attached one with an `attachment_frame` that rotated it to bridge the old +X convention — `state.set_tool_attached_to_group(tool_id, group, attachment_frame=Frame([0, 0, 0], [0, 0, 1], [1, 0, 0]))`, which is what the library's own cells used to do — drop that argument. The tool now attaches with an identity frame, and a leftover rotation rotates it twice. This is the one break that fails silently: nothing errors, the tool simply hangs off the flange sideways. Tools you authored yourself are untouched; if one was modelled along +X to match the old library convention, either re-express it with the new `base_frame` argument of `ToolModel` (`base_frame=Frame([0, 0, 0], [0, 1, 0], [0, 0, 1])`, the plane whose z+ runs along the tool's x+) or keep attaching it with the rotation you already use.
* **`TargetMode.TOOL` targets against library tools moved 90 degrees.** The TCF's Z axis now runs along the tool's working direction, where its X axis used to. A target frame that was correct before now points the tool sideways. Re-author such targets so their Z axis points where the tool should point — which is what the mode meant all along.
* **Analytical kinematics for UR robots returns different numbers.** Forward kinematics now reports the orientation of the `tool0` link (matching the robot model and PyBullet) instead of a version rotated by the never-applied `flange_frame`, and inverse kinematics interprets targets in that same corrected frame. Positions are unchanged; orientations are not. Frames and configurations recorded from the old solver — golden values in tests, targets tuned by trial and error, cached trajectories — have to be regenerated. Targets that were correct in `TargetMode.ROBOT` under PyBullet or MoveIt now work unchanged with the analytical planner too, which was the point.

Requires `compas_robots >= 1.1`.

### Added

* The `Tool From Mesh` Grasshopper component gained a `base_plane` input: where the robot's flange takes hold of the geometry, expressed in the coordinates the mesh was modelled in. Its Z axis points away from the robot, so a tool drawn reaching along world Z needs none, and a tool drawn along another axis is mounted by wiring a plane instead of redrawing the geometry. Backed by the new `base_frame` argument of `compas_robots.ToolModel`; nothing is baked into the mesh, so the plane can be re-wired at any time. The component also surfaces a remark when the TCP does not sit roughly on the tool's +Z, since that means the tool will point sideways once attached — the direction from the mount to the TCP is only a hint (it says nothing about roll), so it is reported rather than applied.

### Changed

* The tools in `ToolLibrary` now mount along the +Z axis of their base frame instead of +X. Every planning group in `RobotCellLibrary` ends at a link whose +Z points away from the arm (`tool0` for the industrial robots, `panda_hand_tcp` for the Panda), so with this the same tool attaches to any of them with an identity attachment frame — previously each cell carried a rotation to bridge the two conventions, and a tool authored for one robot did not necessarily fit another. Their TCF states the tool's working direction with its own Z axis too, so a `TargetMode.TOOL` target aligns the tool along the target's Z — previously the TCF's X axis ran along the tool, which put every tool-mode target 90 degrees out. The tools are still modelled along +X internally and re-framed on the way out via `ToolModel.reframe_base`. The beams held by the gripper cells are authored in TCF coordinates and were re-authored to match, so they stay put. Poses are unchanged: the attached tools and workpieces of every cell land exactly where they did, only the tool's base frame is now the end effector link's frame rather than a rotated version of it. Requires the `reframe_base` support of `compas_robots >= 1.1`.
* `MoveItInverseKinematics` now sends the target pose with an explicit `ik_link_name` (the planning group's end effector link, overridable via the new `"link"` option) instead of relying on MoveIt's default tip link for the group. The two are the same link for the robots in this library, but nothing guarantees that for a planning group configured with a different tip, in which case MoveIt would interpret the target pose at the wrong frame. `PositionIKRequest` gained the corresponding `ik_link_name` field.
* `AnalyticalKinematicsPlanner` now honours the `flange_frame` of its kinematics solver, and the UR solvers (`OffsetWristKinematics` and its subclasses) declare one. Their DH chain ends at the robot's flange, whose x+ points away from the last link, while the planning group ends at `tool0`, whose z+ does — a fixed rotation apart. `flange_frame` was documented as the way to state that offset but was never applied, so the solver and the URDF model disagreed on the orientation of the end effector: every target was interpreted rotated and the robot never reached the frame it was asked for. Analytical forward kinematics now agrees with the robot model (and with PyBullet) to 1e-10, and a `TargetMode.TOOL` target is reached exactly.

### Removed


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@
robot_cell_state.rigid_body_states["beam"].frame = Frame([1.2, 0, 0], [0, 0, 1], [1, 0, 0])

# Attach the gripper to the robot
# The gripper mounts along z+, same as the last link of the UR5's planning group
# ('tool0'), so the attachment frame does not need any rotation.
robot_cell_state.tool_states["gripper"].attached_to_group = robot_cell.main_group_name
robot_cell_state.tool_states["gripper"].attachment_frame = Frame([0, 0, 0], [0, 0, 1], [1, 0, 0])
robot_cell_state.tool_states["gripper"].attachment_frame = Frame.worldXY()

# Set the robot cell state in the planner
planner.set_robot_cell_state(robot_cell_state)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from compas.geometry import Frame

from compas_fab.backends import MoveItPlanner
from compas_fab.backends import RosClient
from compas_fab.robots import ToolLibrary
Expand All @@ -16,11 +14,12 @@
gripper = ToolLibrary.kinematic_gripper()
robot_cell.tool_models[gripper.name] = gripper
# Attach the gripper to the robot
# The tools in the ToolLibrary mount along z+, same as the last link of the UR5's
# planning group ('tool0'), so no attachment_frame (i.e. no rotation) is needed here.
robot_cell_state = robot_cell.default_cell_state()
robot_cell_state.set_tool_attached_to_group(
gripper.name,
robot_cell.main_group_name,
attachment_frame=Frame([0.0, 0.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0]),
touch_links=["wrist_3_link"], # This is the link that the tool is attached to
)

Expand Down
10 changes: 6 additions & 4 deletions docs/backends/ros/files/03_forward_kinematics_target_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@
# The default robot cell state already have the gripper and beam attached
# We modify the robot's configuration here to a specific joint configuration
robot_cell_state.robot_configuration.joint_values = [-2.238, -1.153, -2.174, 0.185, 0.667, 0.0]
# Offset across the tool axis: the TCF's Y, since its Z now runs along the tool
robot_cell_state.rigid_body_states["beam"].attachment_frame = Frame(
[0.0, 0.0, -0.1], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]
[0.0, -0.1, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]
)

# Note that the values for these frames are not the same.
# Note that these frames sit at different origins. The gripper works along its
# mounting axis, so its TCF has the same orientation as the PCF.
print("Robot Planner Coordinate Frame (PCF) relative to the world coordinate system (WCF):")
frame = planner.forward_kinematics(robot_cell_state, TargetMode.ROBOT)
print("- {}".format(frame))
Expand All @@ -44,7 +46,7 @@
>>> Robot Planner Coordinate Frame (PCF) relative to the world coordinate system (WCF):
>>> - Frame(point=Point(x=0.300, y=0.100, z=0.500), xaxis=Vector(x=-0.000, y=-1.000, z=0.000), yaxis=Vector(x=-0.000, y=-0.000, z=-1.000))
>>> Tool Coordinate Frame (TCF) relative to the world coordinate system (WCF):
>>> - Frame(point=Point(x=0.350, y=0.100, z=0.500), xaxis=Vector(x=1.000, y=-0.000, z=-0.000), yaxis=Vector(x=-0.000, y=-1.000, z=0.000))
>>> - Frame(point=Point(x=0.350, y=0.100, z=0.500), xaxis=Vector(x=-0.000, y=-1.000, z=0.000), yaxis=Vector(x=-0.000, y=-0.000, z=-1.000))
>>> Workpiece's Object Coordinate Frame (OCF) relative to the world coordinate system (WCF):
>>> - Frame(point=Point(x=0.350, y=0.100, z=0.600), xaxis=Vector(x=1.000, y=-0.000, z=-0.000), yaxis=Vector(x=-0.000, y=-1.000, z=0.000))
>>> - Frame(point=Point(x=0.350, y=0.100, z=0.600), xaxis=Vector(x=-0.000, y=-1.000, z=0.000), yaxis=Vector(x=-0.000, y=-0.000, z=-1.000))
"""
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@
robot_cell_state = robot_cell.default_cell_state()
# Modify the tool state to attach the cone to the robot
robot_cell_state.tool_states["cone"].attached_to_group = robot_cell.main_group_name
robot_cell_state.tool_states["cone"].attachment_frame = Frame([0.0, 0.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0])
# The tools in the ToolLibrary mount along z+, same as the last link of the UR5's
# planning group ('tool0'), so the attachment frame does not need any rotation.
robot_cell_state.tool_states["cone"].attachment_frame = Frame.worldXY()
# Specify the link of the robot that the tool is allowed to collide with
robot_cell_state.tool_states["cone"].touch_links = ["wrist_3_link"]
# Move the robot to a different configuration
Expand Down Expand Up @@ -62,7 +64,6 @@
robot_cell_state.set_tool_attached_to_group(
gripper.name,
robot_cell.main_group_name,
attachment_frame=Frame([0.0, 0.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0]),
touch_links=["wrist_3_link"],
)
# Specify the location of the detached cone tool
Expand Down
Loading
Loading