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 2022and start installation.Visual Studio Community(VS) is an integrated development environment (IDE) including compiler, debugger, code formatter etc. Compared to this tool,Visual Studio Codeis 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
Workloadstab, 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 2022will open up.
Creating the C# project#
Click on
Create a new project. A list of project templates will be displayed.Click on
Console Appand thenNext.Name your project, e.g.,
RobotTestthen clickNext.Additional informationwindow 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.csfile open.You may be asked about showing
news feedor not. It is not necessary.-
We need
UniversalRobots.NETlibrary for our project. VS can obtain .NET libraries usingNuGetwhich 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 - Solutionwill open up. Click on the nested tab
Browse.Search for
universalrobotusing the search bar.UnderAutomation.UniversalRobotsshould show up.Click on the package. A wide bar on the right called
UnderAutomation.UniversalRobotsshould show up.Tick the box on the left of your project, then click
Install.Outputwindow below will show up which shows installation messages. After that a new pop-up window calledPreview Changeswill open up.Click
Don't show this againand click onApply.License Acceptancewindow will show up.I Accept. You will be back on theNuGet - Solutiontab.Close this tab.
We will now download example code and place them into our project. In
Solution Exploreron the right, findProgram.csand 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 Endingswill ask which line ending to choose. You can chooseWindows (CR LF)and clickYes. Now you should see theProgram.csyou just downloaded.UniversalRobots.NETlibrary license must be provided in the program code. Fill in the stringsLicenseeandLicenseKeywith license information.Now try whether you can start your program: Press
F5to start a debug session. A black command shell window should open up, but your program should error out with an exception similar to:System.Exception: ....
Now you are ready to connect your program to the robot 🎉.
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.254enp0s13f0u3is the interface name which you used to connect to the robot network.172.20.254.254is the gateway addressdefaultor0.0.0.0is the network destination for the packets.defaultstands for all packets which do not fit to the other entries in the routing table, which are typically internet packets.
-
On Windows, start
Command Promptwith 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, setRobotAddressto 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
F5to 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.
Before we review the code together, try the following exercise which will give you a rough idea of how the code works.
Reverse the pick-and-place sequence so that the robot picks something from the right and places it on the left.
Code Overview#
The code consists of the following files:
Program.csthat implements the main app.UniversalRobot.csthat includesUniversalRobotandPoseclasses.
Main application#
The following excerpt demonstrates the most important functionality:
var r = new UniversalRobot(Licensee, LicenseKey, RobotAddress);
r.ActivateRobot();
var DefaultPose = new Pose(.1286, -.3465, 0.549, 2, 0, 0);
WriteLine("↩️ To default position");
r.MoveJ(DefaultPose);
WriteLine($"Current pose: {r.GetPose()}");
WriteLine("🟢 Opening gripper");
r.GripperGrip(width: 40, force: 40);
Sleep(400);
//while (r.gripperGetBusy()) ; // May not work reliably and slow down your sequence.
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 doubles 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:
290 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:
184 if (jd.ReceiveDate == DateTime.MinValue)
185 {
186 do
187 {
188 //WriteLine("Joint data has initial receive date, throw it away.");
189 Sleep(100);
190 jd = PrimaryInterface.JointData;
191 } while (jd.ReceiveDate == DateTime.MinValue);
192 lastReceiveDates.jointData = jd.ReceiveDate;
193 }
194 else
195 {
196 while (lastReceiveDates.jointData == jd.ReceiveDate)
197 {
198 //WriteLine("Pose is old, throw it away.");
199 jd = PrimaryInterface.JointData;
200 }
201 lastReceiveDates.jointData = jd.ReceiveDate;
202 }
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#
-
ReceiveDateof data is off:Do not compare
ReceiveDateof 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:
universalrobot-control-csharp-example/UniversalRobot.cs#290 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_detectedXML-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_methodscall to obtain a list of methods and then callsystem.methodHelpandsystem.methodExistto 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.