Skip to content

Commit 890fccc

Browse files
authored
Refactor tool communication into standalone socat initialization script (UniversalRobots#470)
- The `tool_communication.py` logic has been moved to `ur_client_library`. - The script is no longer implemented as a ROS node. - The script can be executed independently and focuses solely on initializing the `socat` bridge. - Improved error handling has been added directly to the script.
1 parent 5a1a066 commit 890fccc

2 files changed

Lines changed: 167 additions & 2 deletions

File tree

CMakeLists.txt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,10 @@ if(CATKIN_PACKAGE_BIN_DESTINATION)
154154
DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
155155
)
156156
else()
157-
install(PROGRAMS scripts/start_ursim.sh
157+
install(
158+
PROGRAMS
159+
scripts/start_ursim.sh
160+
scripts/tool_communication.py
158161
DESTINATION lib/${PROJECT_NAME}
159162
)
160163
endif()
@@ -186,4 +189,4 @@ file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/share/${PROJECT_NAME}/hook/ros_package_pa
186189
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/share/${PROJECT_NAME}/hook/ros_package_path.dsv DESTINATION share/${PROJECT_NAME}/hook)
187190
#### End iport ####
188191

189-
install(DIRECTORY resources DESTINATION share/${PROJECT_NAME})
192+
install(DIRECTORY resources DESTINATION share/${PROJECT_NAME})

scripts/tool_communication.py

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
#!/usr/bin/env python3
2+
# -- BEGIN LICENSE BLOCK ----------------------------------------------
3+
# Copyright 2026 Universal Robots A/S
4+
#
5+
# Redistribution and use in source and binary forms, with or without
6+
# modification, are permitted provided that the following conditions are met:
7+
#
8+
# * Redistributions of source code must retain the above copyright
9+
# notice, this list of conditions and the following disclaimer.
10+
#
11+
# * Redistributions in binary form must reproduce the above copyright
12+
# notice, this list of conditions and the following disclaimer in the
13+
# documentation and/or other materials provided with the distribution.
14+
#
15+
# * Neither the name of the {copyright_holder} nor the names of its
16+
# contributors may be used to endorse or promote products derived from
17+
# this software without specific prior written permission.
18+
#
19+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
23+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29+
# POSSIBILITY OF SUCH DAMAGE.
30+
# -- END LICENSE BLOCK ------------------------------------------------
31+
32+
33+
"""Small helper script to start the tool communication interface."""
34+
35+
import subprocess
36+
import logging
37+
import argparse
38+
import socket
39+
import os
40+
41+
42+
# Custom formatter to show both default values and description formatting in the help message
43+
class Formatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter):
44+
pass
45+
46+
47+
def get_args():
48+
# Arguments to configure socat
49+
arg = argparse.ArgumentParser(
50+
description=(
51+
"Starts socat to create a PTY symlink for the UR tool communication interface."
52+
),
53+
epilog="""
54+
IMPORTANT:
55+
56+
This script requires the ToolComm Forwarder URCap to be running on the robot.
57+
Make sure it is installed and started before launching this script.
58+
59+
More information can be found in the following ToolComm Forwarder URCap repositories:
60+
- ToolComm Forwarder URCap (Polyscope X):
61+
https://github.com/UniversalRobots/Universal_Robots_ToolComm_Forwarder_URCapX
62+
- ToolComm Forwarder URCap (Polyscope 5)
63+
https://github.com/UniversalRobots/Universal_Robots_ToolComm_Forwarder_URCap
64+
65+
For background information on how tool communication works on UR robots, see:
66+
https://docs.universal-robots.com/Universal_Robots_ROS2_Documentation/doc/ur_robot_driver/ur_robot_driver/doc/setup_tool_communication.html
67+
""",
68+
formatter_class=Formatter
69+
)
70+
71+
arg.add_argument("robot_ip", help="IP address of the robot to connect to.")
72+
arg.add_argument("--tcp-port", type=int, default=54321, help="TCP Port. Likely, this should not be changed")
73+
arg.add_argument("--device-name", default="/tmp/ttyUR", help="PTY symlink device name.")
74+
return arg.parse_args()
75+
76+
77+
def check_tcp(ip, port, timeout=5.0):
78+
try:
79+
with socket.create_connection((ip, port), timeout=timeout):
80+
return True
81+
except OSError:
82+
return False
83+
84+
85+
def main(args):
86+
RED = "\033[31m"
87+
RESET = "\033[0m"
88+
89+
logging.basicConfig(level=logging.INFO, format="[%(levelname)s] %(message)s")
90+
91+
# Get parameters from arguments
92+
robot_ip = args.robot_ip
93+
logging.info("Robot IP: " + robot_ip)
94+
tcp_port = args.tcp_port
95+
logging.info("TCP Port: " + str(tcp_port))
96+
local_device = args.device_name
97+
98+
# Check IP and port reachability
99+
if not check_tcp(robot_ip, tcp_port):
100+
logging.error(
101+
f"{RED}Cannot reach {robot_ip}:{tcp_port}.\n"
102+
"Check that the IP address and port are correct.\n"
103+
"If so, ensure that the robot is powered on, reachable on the network, "
104+
f"and that the ToolCommForwarder URCap is running.{RESET}"
105+
)
106+
logging.info("Exiting tool communication script.")
107+
return
108+
109+
# Check if the device_name is a directory
110+
if os.path.isdir(local_device):
111+
112+
logging.error(
113+
f"{RED}'{local_device}' exists and is a directory.\n"
114+
"Socat needs a file path to create a PTY symlink, but it cannot replace a directory.\n"
115+
"Fix:\n"
116+
" - Remove the directory.\n"
117+
f" - Use a different device name, e.g. '--device-name /tmp/ttyUR0'. {RESET}"
118+
)
119+
logging.info("Exiting tool communication script.")
120+
return
121+
122+
# Configure socat command
123+
socat_config = [
124+
"pty",
125+
f"link={local_device}",
126+
"raw",
127+
"ignoreeof",
128+
"waitslave",
129+
]
130+
131+
socat_command = [
132+
"socat",
133+
",".join(socat_config),
134+
f"tcp:{robot_ip}:{tcp_port}",
135+
]
136+
137+
logging.info(f"Configuring PTY symlink at '{local_device}'")
138+
139+
# Start socat
140+
try:
141+
logging.info("Starting socat with following command:\n" + " ".join(socat_command))
142+
subprocess.call(socat_command)
143+
logging.info("Socat terminated")
144+
145+
# Error case when socat is not installed
146+
except FileNotFoundError:
147+
logging.error(f"{RED}Socat not found in PATH. Install it (e.g. apt-get install socat). {RESET}")
148+
logging.info("Exiting tool communication script.")
149+
return
150+
151+
# Other errors
152+
except Exception as e:
153+
logging.error(f"{RED}Unexpected error launching socat: {e} {RESET}")
154+
logging.info("Exiting tool communication script.")
155+
return
156+
157+
return
158+
159+
160+
if __name__ == "__main__":
161+
args = get_args()
162+
main(args)

0 commit comments

Comments
 (0)