ROS packages for Robotiq grippers and sensors.
| Package | Description | ROS Version |
|---|---|---|
| robotiq_tsf | TSF-85 tactile sensor driver | ROS 2 Humble / Jazzy / Lyrical (main) / ROS 1 Noetic (noetic) |
| grippers | ROS 2 ros2_control driver for Robotiq 2F adaptive grippers (2F-85, 2F-140), on the Robotiq C++ SDK |
ROS 2 Humble / Jazzy / Lyrical |
main supports the three live LTS distros from one branch — build it on whichever
one your robot already runs.
| Distro | Ubuntu | Gripper action | gripper_cmd action type |
|---|---|---|---|
| Humble | 22.04 Jammy | position_controllers/GripperActionController |
control_msgs/action/GripperCommand |
| Jazzy | 24.04 Noble | parallel_gripper_action_controller/GripperActionController |
control_msgs/action/ParallelGripperCommand |
| Lyrical | 26.04 Resolute | parallel_gripper_action_controller/GripperActionController |
control_msgs/action/ParallelGripperCommand |
Rolling builds and passes the full suite as well, and CI watches it, but it is not a supported target: it is a moving development branch, so a break there is a heads-up rather than a release blocker. Its job does not gate merges. Kilted and other non-LTS releases are not built.
The action type differs because parallel_gripper_controller does not exist on
Humble — ROS itself only gained it in Jazzy. robotiq_control.launch.py picks the
matching controller config automatically from $ROS_DISTRO, and everything else
(package names, launch files, controller names, topics, the
/robotiq_gripper_controller/gripper_cmd namespace, the xacro macro arguments)
is identical across all three. On Humble that makes this repo a drop-in
replacement for PickNik's humble branch — see
Migrating from PickNik's ros2_robotiq_gripper.
The repository is versioned as a whole: every package carries the same version
and each release is one tag, v<version>, covering both the gripper and TSF
stacks. A release touching only one stack still moves the other's version.
VERSION at the repo root is the authoritative copy, on its first
line. package.xml must hold a literal version string — ament, rosdep and bloom
parse it statically — so the value is necessarily duplicated there;
dev/version.py propagates it, and a pre-commit hook fails on drift:
dev/version.py # show the version, report any drift dev/version.py --set 1.2.0 # bump VERSION and every package.xml
VERSION also lists the package.xml files holding a copy, as a reminder of
what a bump touches. That list is generated by --set and checked by the hook,
so adding or removing a package fails until it is regenerated.
Tags predating this scheme were per-stack: V1.0.0 released the TSF packages,
and 0.0.1 is PickNik's original gripper release.
Bug reports, fixes and features are welcome. Robotiq maintains these packages; contributions come in as pull requests from a fork. See CONTRIBUTING.md for the development setup, the formatting and test requirements, and the distro and PickNik compatibility constraints that shape review.
| Repository | Description | ROS Version |
|---|---|---|
| robotiq | Original ROS 1 Industrial Robotiq packages (archived, 3D CAD models are outdated) | ROS 1 Indigo / Kinetic / Melodic |
Also check out these community-maintained ROS drivers for Robotiq products.
| Repository | Description | ROS Version |
|---|---|---|
| ros2_epick_gripper | ROS 2 driver for the EPick vacuum gripper | ROS 2 Humble |
| rq_fts_ros2_driver | ROS 2 driver for the Robotiq force-torque sensor | ROS 2 Humble |
| ros2_RobotiqGripper_UR | ROS 2 driver for Robotiq grippers on UR robots | ROS 2 Humble |
Launch the sensor node (SDK-backed — parses the sensor via the
extern/tactile_sensors submodule, so clone with --recurse-submodules):
ros2 run robotiq_tsf poll_data_sdk_node
The device is autodetected (udev symlink, then USB descriptor); override with
--ros-args -p device:=/dev/ttyACM0.
The node publishes on the following topics:
| Topic | Message Type | Description |
|---|---|---|
TactileSensor/StaticData |
robotiq_tsf/StaticData |
Taxel pressure data — two fingers, each with 28 uint16 values |
TactileSensor/Dynamic |
robotiq_tsf/Dynamic |
Dynamic force data — two fingers, each with one int16 value |
TactileSensor/Accelerometer |
robotiq_tsf/Accelerometer |
Raw accelerometer — two readings of [x, y, z] int16 |
TactileSensor/Gyroscope |
robotiq_tsf/Gyroscope |
Raw gyroscope — two readings of [x, y, z] int16 |
TactileSensor/EulerAngle |
robotiq_tsf/EulerAngle |
Fused orientation — two readings of [roll, pitch, yaw] float32 |
TactileSensor/Quaternion |
robotiq_tsf/Quaternion |
Fused orientation — two readings of [w, x, y, z] float64 |
TactileSensor/Timestamp |
robotiq_tsf/Timestamp |
Per-finger firmware timestamp — two uint16 values |
Note:
EulerAngleandQuaternionare only published after the IMU bias calibration period completes at startup.
tactile_viz_node renders TactileSensor/StaticData as per-taxel 3D markers and per-finger 2D heatmap images, with the pad TFs to place them. Two launches:
ros2 launch robotiq_tsf tactile_viz.launch.py # driver + viz + RViz, standalone ros2 launch robotiq_tsf gripper_tactile_viz.launch.py \ com_port:=/dev/ttyUSB0 # single window: 2F-85 gripper + pads on its fingertips
Common args (see --show-args for the full list):
| Arg | Default | Description |
|---|---|---|
poller |
poll_data_sdk_node |
Driver executable publishing StaticData — override to use an alternative poller |
rviz / rviz_config |
true / packaged config |
Toggle RViz / point it at your own config (tactile_viz.launch.py) |
use_fake_hardware |
false |
Gripper ros2_control mock (gripper_tactile_viz.launch.py) |
tactile_delay |
8.0 |
Seconds to delay the viz start so the baseline is captured after gripper activation (gripper_tactile_viz.launch.py) |
The node publishes visualization_msgs/MarkerArray on /tactile/markers and sensor_msgs/Image heatmaps on /tactile_viz/finger0_heatmap / /tactile_viz/finger1_heatmap. On startup it averages the first baseline_frames messages into a per-taxel baseline and subtracts it (re-zero anytime: ros2 topic pub --once /tactile_viz/zero std_msgs/msg/Empty); readings below noise_floor render quiet. Pad geometry, frames, color scale, and heatmap options are parameters of tactile_viz_node.
In the combined launch the pad frames are TF-mounted on the gripper fingertip links, so the heatmaps follow the fingers as the gripper opens and closes.
ROS 2 ros2_control driver for Robotiq 2F adaptive grippers, under grippers/.
Descriptions ship for the 2F-85 and the 2F-140; robotiq_control.launch.py defaults to the
2F-85, so pass gripper_model:=2f_140 for a 2F-140. That argument selects the gripper macro in the
xacro and the joint the controller drives (robotiq_85_left_knuckle_joint or finger_joint); a
custom description keeps model:=<path> and sets gripper_joint explicitly. The controller configs
in config/ write that joint as $(var gripper_joint), which only the launch resolves — loaded
directly into your own ros2_control_node they are not valid as they stand. Hardware validation to
date is on a 2F-85 — the 2F-140 description ships untested against hardware.
The driver itself is model-agnostic: it needs a serial link and the gripper_closed_position of
whatever is attached. A Hand-E therefore works once you supply a URDF for it, but no Hand-E
description ships here yet.
The driver runs on the Robotiq C++ grippers SDK, which arrives as
the extern/grippers submodule — so clone with --recurse-submodules. The SDK owns the serial link
and the Modbus exchange (one FC 0x17 transaction per cycle, on its own thread); robotiq_driver is
the ros2_control layer above it, and its read() / write() copy the SDK's process image rather
than touching the bus.
Robotiq maintains this driver. It started from PickNik Robotics'
ros2_robotiq_gripper (BSD-3-Clause),
imported with git subtree at upstream commit 3b6cf8f so its history came along, and has been
developed here since: the in-tree Modbus implementation and the vcs imported serial package it
depended on were replaced by the SDK, three distros build from one branch, and bugs still open
upstream (#114,
#88) are fixed here. Upstream
copyright and <author> tags are retained; issues and support go to
robotiq/ros/issues, not to PickNik.
This repository is the maintained continuation of the PickNik package. Package names (robotiq_driver, robotiq_controllers, robotiq_description, robotiq_hardware_tests), launch files, controller names, and the /robotiq_gripper_controller/gripper_cmd action are all unchanged, so at the workspace level it is a drop-in replacement.
Docker (recommended):
git clone --recurse-submodules https://github.com/robotiq/ros.git cd ros && ./docker/run.sh gripper
Existing ROS 2 workspace (Humble, Jazzy or Lyrical — same steps on all three):
cd ~/ws/src rm -rf ros2_robotiq_gripper # remove the PickNik clone to prevent duplicate package name failures rm -rf serial # no longer used: the gripper SDK talks to the port itself git clone --recurse-submodules https://github.com/robotiq/ros.git cd ~/ws rosdep install --from-paths src --ignore-src -y rm -rf build install # clear artifacts built from the PickNik sources colcon build
There is no vcs import step any more: the driver's transport comes from the
extern/grippers submodule instead of the external serial package, so a
--recurse-submodules clone is the whole dependency story. rosdep resolves
the right gripper action controller for your distro on its own.
This repository also contains the TSF-85 sensor stack. To build only the gripper packages and their dependencies, replace the last step with:
colcon build --packages-up-to robotiq_description robotiq_controllers robotiq_hardware_tests
Staying on Humble: nothing to change. This repository builds on Humble and keeps PickNik's Humble controller and action surface, so your existing action clients, launch overrides and xacro arguments work untouched:
PickNik humble |
This repository on Humble | |
|---|---|---|
| ROS distro | Humble | Humble |
| Serial transport | serial package, vcs imported |
the extern/grippers SDK submodule (libserialport) |
robotiq_gripper_controller type |
position_controllers/GripperActionController |
position_controllers/GripperActionController |
gripper_cmd action type |
control_msgs/action/GripperCommand |
control_msgs/action/GripperCommand |
As on PickNik's humble, the Humble controller cannot claim the hardware's
set_gripper_max_effort / set_gripper_max_velocity interfaces — Humble's
gripper_controllers has no parameters for them (PickNik's
use_effort_interface / use_speed_interface entries were silently ignored
there). Per-goal speed and force therefore come from the xacro's
gripper_speed_multiplier / gripper_force_multiplier, exactly as before.
Moving to Jazzy or Lyrical at the same time is the one breaking step, and the
break is upstream ROS's, not this repository's — gripper_controllers is gone in
Kilted+, and parallel_gripper_controller is its replacement from Jazzy on
(upstream PickNik PR #103).
Action clients then switch to the ParallelGripperCommand goal — a
sensor_msgs/JointState naming the knuckle joint:
# Humble (PickNik humble, and this repo on Humble) ros2 action send_goal /robotiq_gripper_controller/gripper_cmd \ control_msgs/action/GripperCommand \ "{command: {position: 0.4, max_effort: 50.0}}" # Jazzy / Lyrical ros2 action send_goal /robotiq_gripper_controller/gripper_cmd \ control_msgs/action/ParallelGripperCommand \ "{command: {name: ['robotiq_85_left_knuckle_joint'], position: [0.4], effort: [40.0]}}"
position is still the knuckle angle in radians (≈ 0.0 open → ~0.8 closed on a 2F-85), now as an array on the named joint; max_effort becomes the optional effort / velocity arrays. See Commanding the gripper for details.
use_fake_hardwareandbaudratelaunch args onrobotiq_control.launch.py(hardware-free bringup, previously hardcoded off; a custom baudrate, previously an edit to the xacro)- Actionable error messages when the gripper does not respond (24 V power, RS-485 wiring,
slave_address/baudratehints) - A unified Docker image with device mapping (Docker)
- Active maintenance — PickNik's in-tree README and CI were removed; docs live in this README, and issues go to robotiq/ros/issues
| Package | Description |
|---|---|
robotiq_driver |
ros2_control hardware interface, over the extern/grippers SDK (Modbus RTU on serial) |
robotiq_controllers |
Gripper command / activation controllers |
robotiq_description |
URDF/xacro, meshes, RViz + bringup launch |
robotiq_hardware_tests |
Hardware integration tests |
Bring up a gripper:
ros2 launch robotiq_description robotiq_control.launch.py # real hw, com_port:=/dev/ttyUSB0 ros2 launch robotiq_description robotiq_control.launch.py use_fake_hardware:=true # ros2_control mock ros2 launch robotiq_description robotiq_control.launch.py launch_rviz:=true # + RViz visualization ros2 launch robotiq_description robotiq_control.launch.py baudrate:=<rate> ros2 launch robotiq_description robotiq_control.launch.py sim_isaac:=true \ isaac_joint_commands:=/isaac_joint_commands isaac_joint_states:=/isaac_joint_states # topic_based_ros2_control
This activates joint_state_broadcaster, robotiq_gripper_controller, and robotiq_activation_controller.
sim_isaac swaps the hardware plugin for topic_based_ros2_control/TopicBasedSystem (any
simulator that exchanges sensor_msgs/JointState on two topics — Isaac Sim is the usual one, hence
the argument names). That plugin exports only the standard joint interfaces, so the launch loads
config/robotiq_controllers.sim.yaml for it (per-goal effort/velocity are accepted and ignored;
a stall aborts the goal rather than succeeding, since a simulator that publishes no joint velocities
would otherwise report every failed grasp as success) and skips robotiq_activation_controller,
whose reactivate_gripper GPIO only the driver and the mock declare. TopicBasedSystem matches
joints to the simulator's JointState by name, so the simulator must publish this description's
six joint names for the model you launch, each with your prefix, or nothing moves:
gripper_model |
driven joint | mimic joints |
|---|---|---|
2f_85 |
robotiq_85_left_knuckle_joint |
robotiq_85_right_knuckle_joint, robotiq_85_left_inner_knuckle_joint, robotiq_85_right_inner_knuckle_joint, robotiq_85_left_finger_tip_joint, robotiq_85_right_finger_tip_joint |
2f_140 |
finger_joint |
right_outer_knuckle_joint, left_inner_knuckle_joint, right_inner_knuckle_joint, left_inner_finger_joint, right_inner_finger_joint |
The plugin is not a dependency of this package — build
topic_based_ros2_control yourself.
The ros2_control xacros also carry a sim_gazebo parameter (gz_ros2_control/GazeboSimSystem)
for cells that embed the gripper macro in their own Gazebo description; this launch does not wire
it, since Gazebo hosts its own controller_manager.
On Jazzy and Lyrical, robotiq_gripper_controller is a parallel_gripper_action_controller/GripperActionController, so its action /robotiq_gripper_controller/gripper_cmd takes a control_msgs/action/ParallelGripperCommand — a sensor_msgs/JointState goal (not the older GripperCommand). On Humble it is position_controllers/GripperActionController taking control_msgs/action/GripperCommand; see Supported ROS 2 distros.
The bringup above holds its terminal, so open a second terminal, exec into the running container, then send a goal:
# host: open a second shell into the running container (per-distro container name) docker exec -it robotiq_ros2_jazzy bash # inside the container — Jazzy / Lyrical: ros2 action send_goal /robotiq_gripper_controller/gripper_cmd \ control_msgs/action/ParallelGripperCommand \ "{command: {name: ['robotiq_85_left_knuckle_joint'], position: [0.4], effort: [40.0]}}" # inside the container — Humble: ros2 action send_goal /robotiq_gripper_controller/gripper_cmd \ control_msgs/action/GripperCommand \ "{command: {position: 0.4, max_effort: 50.0}}"
position is the joint angle in radians (≈ 0.0 open → ~0.8 closed on a 2F-85); on Jazzy/Lyrical effort and velocity are optional max limits, mapped to the controller's set_gripper_max_effort / set_gripper_max_velocity interfaces.
robotiq_driver exports four state interfaces on the gripper joint, all read out of the same status block the SDK exchanges every cycle. Read them from /dynamic_joint_states, or with ros2 control list_hardware_interfaces.
| Interface | Unit | Description |
|---|---|---|
position |
rad | Knuckle joint angle, from gPO (≈ 0.0 open → ~0.8 closed on a 2F-85) |
velocity |
rad/s | Always 0.0. The status block carries no velocity, and the driver does not differentiate the position |
motor_current |
A | gCU, the motor current the manual gives as 10 mA per count, so 0.0 to 2.55 |
object_status |
— | gOBJ verbatim: 0 moving, 1 object held while opening, 2 object held while closing, 3 at the requested position |
All four read NaN until the component is activated; activation seeds them from the gripper's first settled status read, and read() refreshes them every cycle after that. The sentinel matters most for object_status, where every value in range is meaningful — 0 is "moving", not "no reading" — so NaN is the only way to say the gripper has not answered yet. Test for it before comparing against 0..3.
motor_current is motor current, not grip force, and it is not convertible to one.
motor_current and object_status are not ros2_control standard interface names (there are HW_IF_ constants for position, velocity and effort, and nothing for either of these), so consumers spell them out. The descriptions declare them on the real-hardware branch only — mock_components/GenericSystem, Gazebo and Isaac never write them — so under use_fake_hardware:=true both are absent rather than wrong.
robotiq_driver reads these from the <hardware> block of the ros2_control description (robotiq_description/urdf/2f_*.ros2_control.xacro):
| Parameter | Default | Description |
|---|---|---|
gripper_closed_position |
required | Joint angle in radians at a fully closed gripper — the scale of the whole position mapping |
COM_port |
/dev/ttyUSB0 |
Serial port |
baudrate |
115200 |
Must match the gripper's persisted setting, which is why it is also a launch argument, baudrate:=<rate>. Rejected outside 1..1000000. Most units stay at 115200; the gripper's own rate is changed in the Robotiq User Interface (Modbus RTU Parameters), not from here, and the gripper must be rebooted afterwards |
timeout |
0.5 |
Per-transaction serial timeout, in seconds |
slave_address |
0x09 |
Modbus slave address; 0x09 as the manual prints it, a bare number as the decimal it looks like |
connection_frequency |
100 |
Rate of the SDK's background exchange cycle, in Hz; 0 free-runs |
activation_timeout |
15 |
Seconds allowed for activation and for fault recovery |
gripper_max_speed / gripper_max_force |
0.150 m/s / 235 N |
Full scale used to turn the speed/effort command interfaces into rSP / rFR register fractions. Command-side only — nothing scales a state interface by them. Rejected unless finite and above zero |
gripper_speed_multiplier / gripper_force_multiplier |
1.0 |
Initial fractions published on those interfaces |
use_dummy |
false |
Drive a fake gripper instead of hardware. Off for the usual falsey spellings — empty, 0, false, no, off, in any case — on for anything else |
A malformed value is reported and the default stands; only gripper_closed_position fails the transition — missing, malformed, zero, or non-finite. A speed or force command the driver cannot turn into a register leaves that register at its previous value.
use_dummyselects the SDK's fake gripper: no port is opened, activation is instant, and the fingers report wherever they were last commanded. It keeps the real plugin loaded, so the gripper and activation controllers still bind. That is what distinguishes it from theuse_fake_hardware:=truelaunch argument, which swaps the plugin out forros2_control'smock_components/GenericSystem.
Activating the hardware component runs the gripper's reset handshake: it clears any latched fault and runs the calibration sweep. It therefore releases any grip and moves the fingers through their full range — activate with the workspace clear. Deactivating clears the command block including rACT, which resets the gripper and likewise releases any grip. This is what the driver has always done; the SDK exposes a conservative alternative (leave a healthy gripper alone, refuse to reset a faulted one) that is not wired up here yet.
A fault can also be cleared without cycling the lifecycle state, by writing to the reactivate_gripper/reactivate_gripper_cmd command interface (exposed as a GPIO on the description, and driven by robotiq_activation_controller) — same handshake, same consequences. reactivate_gripper_response reads 1.0 once the recovery succeeds; a failure is logged but leaves the interface as it was, as it did before. A deactivation arriving while a recovery is in flight waits for it to finish before resetting.
activation_timeout bounds both. The old driver had no timeout and could block the transition indefinitely.
Pass launch_rviz:=true to open RViz with the gripper model (default config robotiq_description/rviz/view_urdf.rviz); the joints update live from joint_state_broadcaster. Override with rvizconfig:=/path/to/your.rviz. Args combine — e.g. use_fake_hardware:=true launch_rviz:=true to visualize without hardware. Running via docker/run.sh already forwards X11, so the RViz window displays from inside the container (if it can't connect to the display, run xhost +local:root on the host once).
To inspect the model in RViz with no gripper attached (no ros2_control), use the visualization-only launch — robot_state_publisher, RViz, and a joint_state_publisher_gui slider:
ros2 launch robotiq_description view_gripper.launch.py
Drag the robotiq_85_left_knuckle_joint slider; the five finger joints follow it via URDF mimic (≈ 0.0 open → ~0.8 closed).
Goal-based commanding also works without hardware: robotiq_control.launch.py use_fake_hardware:=true launch_rviz:=true brings up all three controllers against mock_components/GenericSystem, and gripper_cmd goals drive the model — the five finger joints follow the knuckle via URDF mimic, exactly as on hardware. Use the slider above when you want to pose the model by hand instead.
Mock and hardware publish the same /joint_states contract on every distro: the knuckle joint alone, with robot_state_publisher deriving the mimicked finger joints from the URDF. Only the Gazebo and Isaac paths declare the mimicked joints to ros2_control, since those simulators supply their own joint state.
Unit tests live in each package's test/ (or tests/) directory and run without hardware. Build and run all tests from the repository root:
source /opt/ros/jazzy/setup.bash # or humble / lyrical colcon build colcon test colcon test-result --verbose
To scope to a single package, pass --packages-select <package> to colcon build and colcon test.
Test executables land under build/<package>/, mirroring the package's test-directory layout; run one directly for gtest options such as --gtest_filter:
./build/<package>/test/<test_executable> --gtest_filter='<TestSuite>.*'
CI builds the packages and runs their unit tests on pull requests, once per supported distro — Humble, Jazzy and Lyrical (ci-ros-build-test.yml).
The docker/ folder provides scripts to build and run the TSF-85 and 2F grippers inside a container. Clone with submodules to pull in the required utilities:
git clone --recurse-submodules https://github.com/robotiq/ros.git
If you already cloned without --recurse-submodules:
git submodule update --init
Two SDKs live under extern/ as submodules and are built into the image:
| Submodule | Repository | Used by |
|---|---|---|
extern/tactile_sensors |
Robotiq/tactile_sensors | robotiq_tsf's poll_data_sdk_node |
extern/grippers |
Robotiq/grippers | robotiq_driver |
extern/COLCON_IGNORE keeps colcon from building either SDK's standalone
CMake project as a workspace package; the packages that need them compile them
in-tree.
| Script | Description |
|---|---|
run.sh |
Builds a single ROS 2 image with the whole workspace (sensor + grippers) and launches a shell with that product's devices mapped: ./run.sh [gripper|sensor|both]. |
sensor_install.sh |
Sets up udev rules and permissions for bare-metal (non-Docker) use |
Dockerfile_TSF85_ROS2andbuild_launch_docker_ros2.share kept as deprecation shims pointing toDockerfile/run.sh sensor.
The Dockerfile is a multi-stage build: a builder stage compiles the workspace, and a slim runtime stage (the default) ships only the built workspace plus runtime deps — no compiler, no colcon/rosdep, no test deps.
Build it directly (the context must be the repo root — the Dockerfile COPYs robotiq_tsf/, grippers/ and extern/tactile_sensors/sdk_cpp):
docker build -f docker/Dockerfile -t robotiq_ros2:jazzy .Build options:
--build-arg ROS_DISTRO=...(defaultjazzy) —humble,jazzyandlyricalare all built in CI.--build-arg WITH_GUI=false— headless: drop rviz2/rqt/joint-state-publisher-gui (and their mesa/Qt/VTK), for a much smaller image on robots that don't visualize.--target builder— a dev image with the full toolchain, for building inside the container.
docker build --build-arg WITH_GUI=false -f docker/Dockerfile -t robotiq_ros2:jazzy-headless . docker build --target builder -f docker/Dockerfile -t robotiq_ros2:dev .
Then run it with the sensor/gripper devices mapped via ./docker/run.sh [gripper|sensor|both].
Pick the distro with ROS_DISTRO (default jazzy); each one gets its own image
tag and container name, so switching does not reuse the other's build:
./docker/run.sh gripper # robotiq_ros2:jazzy / robotiq_ros2_jazzy ROS_DISTRO=humble ./docker/run.sh gripper # robotiq_ros2:humble / robotiq_ros2_humble ROS_DISTRO=lyrical ./docker/run.sh both # robotiq_ros2:lyrical / robotiq_ros2_lyrical