Universal robot arm with gripper example code#
Previously I had searched for Universal Robot RTDE libraries with C# bindings. Even I was first against it, we decided at DTU to go for the UniversalRobots.NET library due to time constraints and I prepared a tutorial including code that our students can base their projects on.
Learning goals#
-
Be able to set up your computing environment to run the example code on a UR3e robot with an OnRobot RG2 gripper as follows:
Know which software components to modify for adding extra functionality.
Installation#
Visual Studio Community#
-
Start
Microsoft Store
, search forVisual Studio Community 2022
and start installation.Visual Studio Community
(VS) is an integrated development environment (IDE) including compiler, debugger, code formatter etc. Compared to this tool,Visual Studio Code
is only a text or code editor, which depends on other tools to compile or debug code.Installation likely needs admin rights.
The installer will ask about which components to install. On the
Workloads
tab, select.NET desktop development
, then click onInstall
. The installation will start the IDE after the installation is finished.VS will ask to sign in, which is not necessary. Click on the
Skip this for now.
.-
VS will ask about your development settings. Choose
Visual C#
. This will probably tailor your IDE to C# development compared to other environments like C++ or web development.Click on
Start Visual Studio
. A window titledVisual Studio 2022
will open up.
Creating the C# project#
Click on
Create a new project
. A list of project templates will be displayed.Click on
Console App
and thenNext
.Name your project, e.g.,
RobotTest
then clickNext
.Additional information
window will open up asking e.g., for which .NET framework to use.Leave default settings and click on
Create
. The IDE window will show up withProgram.cs
file open.You may be asked about showing
news feed
or not. It is not necessary.-
We need
UniversalRobots.NET
library for our project. VS can obtain .NET libraries usingNuGet
which is a repository where developers can upload their packages so that other developers can leverage them.Click on
Tools
→NuGet Package Manager
→Manage NuGet Packages for solution
. A new tab titledNuGet - Solution
will open up. Click on the nested tab
Browse
.Search for
universalrobot
using the search bar.UnderAutomation.UniversalRobots
should show up.Click on the package. A wide bar on the right called
UnderAutomation.UniversalRobots
should show up.Tick the box on the left of your project, then click
Install
.Output
window below will show up which shows installation messages. After that a new pop-up window calledPreview Changes
will open up.Click
Don't show this again
and click onApply
.License Acceptance
window will show up.I Accept
. You will be back on theNuGet - Solution
tab.Close this tab.
We will now download example code and place them into our project. In
Solution Explorer
on the right, findProgram.cs
and right-click on it. Click onOpen Containing Folder
. Your project folder will show up inFile Explorer
.-
We will place our source files in this directory after downloading them:
Download the following files and place them in your project.
Do not be afraid of replacing
Program.cs
. Turn back to VS.
The files you downloaded may have different or inconsistent line endings. In this case a window titled
Inconsistent Line Endings
will ask which line ending to choose. You can chooseWindows (CR LF)
and clickYes
. Now you should see theProgram.cs
you just downloaded.UniversalRobots.NET
library license must be provided in the program code. Fill in the stringsLicensee
andLicenseKey
with license information.
Establishing network connection to the robot#
Take an Ethernet cable and connect it to your computer and to the network port through which you can access the robot you want to control. You will probably need a USB-Ethernet dongle. If there is a connection, you should see blinking lights on the Ethernet port.
-
After connection your internet access may be lost, if the robot network is not connected to the internet and if your computer tries to route internet packets through the robot network.
To fix this, you can delete the corresponding route using:
-
On Linux, e.g.,
sudo ip route delete default via 172.20.254.254
enp0s13f0u3
is the interface name which you used to connect to the robot network.172.20.254.254
is the gateway addressdefault
or0.0.0.0
is the network destination for the packets.default
stands for all packets which do not fit to the other entries in the routing table, which are typically internet packets.
-
On Windows, start
Command Prompt
with administrator rights and use:route delete 0.0.0.0 mask 0.0.0.0 172.20.254.254
-
Preparing the robot#
Switch the robot on using the pendant. After PolyScope has booted, switch the robot to Remote
control mode, if Local
mode is selected on the right upper quadrant of the screen .
Note
You do not have to enable the robot on the pendant. The program will take care of powering the robot on and releasing the brakes.
Running the test program#
In
Program.cs
, setRobotAddress
to the IP address of the robot you are connected to. The IP address may be available on a sticker on the robot control box.Press
F5
to start a debug session. A black command shell window should open up and show you messages written by the program, like you have seen in the video.
Code Overview#
The code consists of the following files:
Program.cs
that implements the main app.UniversalRobot.cs
that includesUniversalRobot
andPose
classes.
Main application#
The following excerpt demonstrates the most important functionality:
var r = new UniversalRobot(Licensee, LicenseKey, RobotAddress);
var DefaultPose = new Pose(.1286, -.3465, 0.549, 2, 0, 0);
r.sendMovej(DefaultPose);
WriteLine("↩️ To default position");
while (!r.getPose().isClose(DefaultPose)) ;
WriteLine($"Current pose: {r.getPose()}");
r.gripperGrip(width: 40, force: 40);
WriteLine("🟢 Opening gripper");
while (r.gripperGetBusy()) ;
We create our robot object. We will use it very often, so we choose a short name for it: r
. DefaultPose
a neutral pose where the robot stands clear off the workspace. We issue the URScript command movej
to the pose. The function does not block until movej
is executed, but only sends the command to the robot controller through primary interface. Sending another command through primary interface would cancel the previous command.
Gripper uses the XML-RPC interface instead of the primary interface, so if we issue a gripper command without waiting then we would move the gripper before the movement is finished.
To wait until the action is finished, we wait until current pose is close to our target pose DefaultPose.
🤔 Question
Why do we check for close values instead of equal?
We want close values instead of equal, because Pose
consists of six double
s and not every existing decimal number can be represented by the double precision floating point number format double
.
Similarly we can check whether the gripper has finished its operation using gripperGetBusy()
.
UniversalRobot
class#
This class implements the remote control for the arm robot and the attached gripper and is based on the main class UR
provided by UniversalRobots.NET
:
public class UniversalRobot : UR
Let us go through the available function on the source code.
Pose
class#
Pose
class in the UniversalRobots.NET
represents a position with orientation. To check whether we reached our target pose, we must compare six components of two poses. Comparing two arrays with each other in a single line is beneficial for the readability of our code, so I augmented the Pose
class of the UniversalRobots.NET
with a method that can compare two poses in a single function:
266 public bool isClose(Pose other, double tolerance = 1e-3)
You may have noticed that I used the same as the class in UniversalRobots.NET
. This is one of the superpowers of object-oriented programming (OOP) and namespaces. Using OOP we can base our work on the shoulder of others and using namespaces we can isolate different parts of our code, which helps to avoid name clashes. To differentiate between these two classes we use:
public class Pose : URCommon.Pose
Pay attention to the URCommon
prefix. If we had used using UnderAutomation.UniversalRobots.Common
, then the name Pose
would be available, and we could not have introduced our own Pose
. If we want to avoid the full name UnderAutomation.UniversalRobots.Common.Pose
, then we can also use a using alias instead:
using URCommon = UnderAutomation.UniversalRobots.Common;
Through which channel does the program control the robot?#
Universal Robots are typically controlled using URScript. Sending URScript commands to a Universal Robots robot can be done using primary, secondary and real-time (RTDE) interfaces. UniversalRobots.NET implements all of them. Even our goal was to use the RTDE interface, for moving the gripper in world coordinates, we decided to fall back to the high-level functions provided by the primary interface like movej
, because UniversalRobots.NET
seems to focus on the functions provided by the interfaces and does not provide functions for inverse kinematics compared to ur_rtde
library. Inverse kinematics is used to transform world coordinates to the angles of joints, which is necessary to move the robot from the perspective of the gripper.
The external devices connected to the robot like a gripper are controlled using a socket or XML-RPC over HTTP.
Event-based vs sequential programming#
In computing systems that interact with their physical environment, the program must react to external events when for example someone pushes a button.
🤔 Question
How can we check in our program if the button is pushed?
We can either periodically check in a while
statement if the button is pushed or not. This is also called polling. Typically, a better alternative is that the computer program can be interrupted by an external signal if the button is activated – similar to a push notification, called interrupt. Programming languages implement interrupts by registering a function that must be called when an external event happens. In this case the sequential nature of our program is broken and this kind of programming is called event-based programming, which looks like this:
...
r.Rtde.OutputDataReceived += newDataArrived;
// Adds `newDataArrived` to the functions which will be called
// whenever `OutputDataReceived`.
void newDataArrived(object? sender, RtdeDataPackageEventArgs e)
{
react(e.OutputDataValues.ActualTcpPose);
}
...
The target group was non-engineering students who learn C# as their first language, so I avoided event-based programming. This led to the problem of reading old sensor values from the robot, so I had to integrate functionality that checks for the message receive date and waits for the next message before returning it, for example:
178 if (jd.ReceiveDate == DateTime.MinValue)
179 {
180 do
181 {
182 //WriteLine("Joint data has initial receive date, throw it away.");
183 Sleep(100);
184 jd = PrimaryInterface.JointData;
185 } while (jd.ReceiveDate == DateTime.MinValue);
186 lastReceiveDates.jointData = jd.ReceiveDate;
187 }
188 else
189 {
190 while (lastReceiveDates.jointData == jd.ReceiveDate)
191 {
192 //WriteLine("Pose is old, throw it away.");
193 jd = PrimaryInterface.JointData;
194 }
195 lastReceiveDates.jointData = jd.ReceiveDate;
196 }
PrimaryInterface
implemented by UniversalRobots.NET
provides the latest data with a timestamp called ReceiveDate
. In the code above, if
checks whether ReceiveDate
has the initialization value meaning no data has been received after the program has been started. When ReceiveDate
changes from DateTime.MinValue
, then we must have received new data, and we save the new date into lastReceiveDates.jointData
. The else
path will be typically taken when the program is running. It will discard data according to ReceiveDate`.
If we had used event-based programming for getting joint data then we would have avoided these checks.
Troubleshooting#
-
ReceiveDate
of data is off:Do not compare
ReceiveDate
of received data to the current date, e.g.,DateTime.Now
, if robot clock is not synchronized, e.g., using NTP. The clock of PolyScope could be off, because network time synchronization may be unavailable due to lack of internet connectivity. -
Robot waits too long at
while (r.gripperGetBusy())
orwhile (!r.gripperGetGripDetected())
:This happened to me often. I do not know the reason. Switching to a
Sleep()
-based approach is a workaround. -
Robot waits too long at
while (!r.getPose().isClose(newPose))
:Try a lower tolerance in:
266 public bool isClose(Pose other, double tolerance = 1e-3)
-
Gripper does not move:
Check if the LED on the side of the gripper is red. I believe red means a failure. Unplug and plug the gripper to reset the gripper.
Improvement ideas#
The goal of the course is to use C# programming (instead of URScript). So the code should integrate inverse-kinematics functions and switch to RTDE if teaching event-based programming is pedagogically feasible.
-
The following tasks are done manually. They can be done by a reliable library.
XML request string creation
XML-RPC for the gripper, e.g., using Sharer - a library for XML-RPC calls
-
rg_get_grip_detected
XML-RPC call detects a grip very late. Maybe I am using the call in a wrong way. I could not find any official documentation about XML-RPC methods supported by the RG2 gripper. According to OnRobot support:… we do not have official documentation available for the specific XML-RPC functions, such as
rg_stop
…However, one can use the
list_methods
call to obtain a list of methods and then callsystem.methodHelp
andsystem.methodExist
to get more information about these methods. For example here is a list of OnRobot-related methods.
Conclusion#
This was my first C# experience. With prior knowledge of other languages and also with OOP and functional-programming, an important part of the programming process was to ask a GPT assistant questions like:
How do I compare two arrays of doubles using zip and any (like in Python)
How do I inherit from another class in C#
But in more than 50% of the time the answer was not satisfactory, and I checked the C# manual.
I have not been using Visual Studio since my first semester at the university and I liked the integrated features like code and error inspection by hovering the mouse on arguments or variables. On a general text editor like Visual Studio Code, setting up a project would involve installing the right plugins – Visual Studio Code saves this struggle. I also liked the fixes recommended by the IDE. I did not try GitHub Copilot integration which could increase productivity.
If you are learning your first language, you should be patient until you understand how computational thinking and programming languages work. Your next languages will probably take less time like in my case starting C# from scratch.
Last but not least, for industrial robot programming I recommend to use ROS2 which ships all common software that a robot application needs. PolyScope will eventually integrate a ROS2 API. In our course we decided not to confuse the students with ROS concepts like topics, services etc.
Update 2024-11-15: PolyScope X open beta announcement.