Bridging Digital Outputs to ROS2 Gripper Control

1. Objective and Why This Matters

In this lesson, we build a ROS 2 bridge for controlling a gripper, or in general any end effector driven by a digital output trigger.

The main idea is simple: robot manufacturers usually expose digital outputs through their own SDK syntax, such as vendor-specific commands like SetDO(...). That works, but it ties your application to one specific robot brand.

What we want instead is a more ROS-style architecture:

  • high-level ROS 2 interfaces for the application layer
  • vendor-specific SDK details isolated inside one bridge node
  • easier portability to another robot in the future

So the objective of this lesson is to show how to convert:

  • a low-level robot SDK command for digital outputs
    into
  • a reusable ROS 2 interface for gripper or end effector control

This is important because the same pattern can be reused not only for a pneumatic gripper, but also for:

  • suction tools
  • vacuum valves
  • pneumatic clamps
  • tool changers
  • any end effector actuated by a digital output

2. GitHub Packages to Clone

For this lesson, you can clone the ready-made packages directly into their workspace.

Clone these two repositories inside the src folder of your ROS 2 workspace:

The first package is the bridge for the robot arm.
The second package is the bridge for the gripper or DO-controlled end effector.

In this lesson, I focus on the architecture and the code logic, not on typing every file manually from scratch.

3. Core Idea of the Code

This node creates a bridge between two worlds:

  • the ROS 2 world, where we want clean interfaces like /gripper/open
  • the robot SDK world, where the robot expects strings like SetDO(0,1)

So the node hides the Fairino-specific syntax and exposes a ROS-friendly control layer.

At a high level, the node does four things:

  1. it connects to the Fairino remote command service
  2. it exposes ROS 2 services for high-level gripper commands
  3. it translates those commands into digital output SDK strings
  4. it publishes a fake gripper joint state so that MoveIt and RViz can track the gripper state

That is the core architectural pattern.

4. Code Explanation — Only the Essential Blocks

Block 1 — Parameters

The first important part is the parameters.

Here the node defines:

  • which service to call for low-level robot commands
  • which DO is used for open
  • which DO is used for close
  • which DO is used for suction
  • timing parameters like interlock delay and timeout
  • the fake gripper joint name and positions

This is one of the first places where students must adapt the code to their own case.

For example, these lines are critical:

self.declare_parameter('do_open', 0)

self.declare_parameter('do_close', 1)

self.declare_parameter('do_suction', 2)

If your robot wiring is different, this mapping must change.

So if your pneumatic valve is connected to different digital outputs, you update those values.

Block 2 — Connection to the Vendor Service

The second important block is the client creation:

self.cli = self.create_client(

  RemoteCmdInterface,

  self.remote_service,

  callback_group=self.cb_group

)

This is the bridge to the Fairino controller.

The ROS node itself does not directly toggle outputs.
Instead, it sends a request to the low-level Fairino service, and that service executes the vendor SDK command.

So this block is where the ROS layer connects to the hardware layer.

Block 3 — High-Level ROS 2 Services

The next key block is the service creation:

self.create_service(Trigger, 'gripper/open', self.handle_open, callback_group=self.cb_group)

self.create_service(Trigger, 'gripper/close', self.handle_close, callback_group=self.cb_group)

self.create_service(Trigger, 'gripper/idle', self.handle_idle, callback_group=self.cb_group)

This is the high-level interface that the rest of your ROS 2 application will use.

Instead of writing vendor strings directly, the application simply calls:

  • /gripper/open
  • /gripper/close
  • /gripper/idle

This is exactly why we build the bridge.

We want the application to speak ROS, not SDK syntax.

Block 4 — The Real Vendor-Specific Part

The most important adaptation point is here:

def set_do(self, do_id: int, value: int) -> bool:

  return self.call_cmd(f"SetDO({do_id},{int(value)})")

This is the line that transforms the high-level ROS 2 logic into the exact SDK command expected by the robot.

For Fairino, the syntax is:

SetDO(...)

But if your robot uses another SDK, this is where you must put your hands.

For another brand, this could become something else entirely, such as:

  • another command string
  • another service
  • another API call
  • another driver method

So the key message for you is:

if you want to adapt this node to another robot, the first place to modify is the low-level command generation function.

Everything above that layer can remain conceptually the same.

Block 5 — Safety Logic

Another important block is the safety logic:

def _break_before_make(self, do_to_enable: int) -> bool:

This logic makes sure that the open and close outputs are never active at the same time.

That is especially important for pneumatic grippers and valves.

So the sequence is:

  • first switch everything off
  • wait a small delay
  • then activate only the requested output

This is good engineering practice and avoids conflicting commands.

Block 6 — High-Level Commands

Then we have the high-level methods:

  • open_gripper()
  • close_gripper()
  • idle_gripper()
  • suction_on()
  • suction_off()

These methods define the application behavior.

This is the business logic of the end effector.

If your end effector behaves differently, this is another place where you may adapt the node.

For example:

  • maybe your tool only needs one DO
  • maybe open and close logic are inverted
  • maybe you need pulse logic instead of maintained output
  • maybe you need vacuum on/off only

So this block should reflect the real behavior of your hardware.

Block 7 — Fake Joint State for MoveIt

The last important block is the gripper joint state publisher:

self.gripper_js_pub = self.create_publisher(JointState, self.gripper_joint_state_topic, 10)

and then:

def _publish_gripper_joint_state(self):

This publishes a fake joint state for the gripper.

Why do we need this?

Because MoveIt and RViz need some form of state feedback to represent the gripper in the robot model.

Even if the gripper is driven only by a digital output, we still publish a software state like:

  • 0.0 for open
  • 1.0 for close
  • 0.5 for idle

This is not a real encoder feedback.
It is a software representation that makes the ROS 2 ecosystem work consistently.

5. Where You Need to Adapt the Code

If students want to reuse this node for their own robot or end effector, the most important places to adapt are:

First: the DO mapping

Change which digital outputs correspond to:

  • open
  • close
  • suction

Second: the low-level SDK command

This is the most important adaptation point:

return self.call_cmd(f"SetDO({do_id},{int(value)})")

If your robot does not use SetDO(...), this line must be changed.

Third: the end effector logic

Adapt the high-level behavior depending on your hardware:

  • pneumatic gripper
  • suction cup
  • valve
  • clamp
  • other DO-based tool

Fourth: the fake joint state naming and positions

If your URDF or SRDF uses a different joint name, update:

  • gripper_joint_name
  • open/close/idle position values

So in practice, students do not need to rewrite the whole node.
They mainly need to adapt:

  • mapping
  • command syntax
  • tool behavior
  • joint naming

That is the real takeaway.

6. Demo Section

Here I will show the full video demonstration.

In the demo, I will:

  • clone the packages into the workspace
  • build the workspace
  • run the low-level Fairino command server
  • run the gripper bridge node
  • test the ROS 2 services
  • show the digital output trigger working on the real robot
  • explain how this becomes the foundation for MoveIt gripper control

This part is where students can see the architecture working in practice.

7. Conclusion — Key Takeaways

In this lesson, you learned how to build a ROS 2 bridge for a gripper, or any end effector controlled by a digital output.

The most important idea is not the specific Fairino syntax.

The important idea is the architecture:

  • keep the application layer ROS-native
  • isolate vendor-specific SDK syntax in one bridge node
  • expose high-level ROS interfaces to the rest of the system

You also learned the four essential building blocks:

  • connect to the low-level robot service
  • expose high-level ROS 2 gripper services
  • translate those services into DO commands
  • publish a gripper state for the ROS ecosystem

And finally, you saw where to adapt the code to your own case:

  • DO mapping
  • SDK command syntax
  • tool behavior
  • joint naming for MoveIt and RViz

Once you understand this pattern, you can reuse it for almost any DO-controlled end effector.

Complete and Continue  
Discussion

0 comments