Universal robot arm with gripper example code#

Background

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#

  1. Start Microsoft Store, search for Visual 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.

  2. The installer will ask about which components to install. On the Workloads tab, select .NET desktop development, then click on Install. The installation will start the IDE after the installation is finished.

  3. VS will ask to sign in, which is not necessary. Click on the Skip this for now..

  4. 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 titled Visual Studio 2022 will open up.

Creating the C# project#

  1. Click on Create a new project. A list of project templates will be displayed.

  2. Click on Console App and then Next.

  3. Name your project, e.g., RobotTest then click Next. Additional information window will open up asking e.g., for which .NET framework to use.

  4. Leave default settings and click on Create. The IDE window will show up with Program.cs file open.

  5. You may be asked about showing news feed or not. It is not necessary.

  6. We need UniversalRobots.NET library for our project. VS can obtain .NET libraries using NuGet which is a repository where developers can upload their packages so that other developers can leverage them.

    Click on ToolsNuGet Package ManagerManage NuGet Packages for solution. A new tab titled NuGet - Solution will open up.

  7. Click on the nested tab Browse.

  8. Search for universalrobot using the search bar. UnderAutomation.UniversalRobots should show up.

  9. Click on the package. A wide bar on the right called UnderAutomation.UniversalRobots should show up.

  10. 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 called Preview Changes will open up.

  11. Click Don't show this again and click on Apply. License Acceptance window will show up.

  12. I Accept. You will be back on the NuGet - Solution tab.

  13. Close this tab.

  14. We will now download example code and place them into our project. In Solution Explorer on the right, find Program.cs and right-click on it. Click on Open Containing Folder. Your project folder will show up in File Explorer.

  15. 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.

  16. Turn back to VS.

  17. 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 choose Windows (CR LF) and click Yes. Now you should see the Program.cs you just downloaded.

  18. UniversalRobots.NET library license must be provided in the program code. Fill in the strings Licensee and LicenseKey with license information.

Establishing network connection to the robot#

  1. 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.

  2. 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 address

      • default or 0.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#

  1. In Program.cs, set RobotAddress 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.

  2. 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 includes UniversalRobot and Pose classes.

Main application#

The following excerpt demonstrates the most important functionality:

universal-robot-arm-with-gripper-example-code/Program.cs#
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 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:

universal-robot-arm-with-gripper-example-code/UniversalRobot.cs#
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:

universal-robot-arm-with-gripper-example-code/UniversalRobot.cs#
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:

universal-robot-arm-with-gripper-example-code/UniversalRobot.cs#
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()) or while (!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:

    universal-robot-arm-with-gripper-example-code/UniversalRobot.cs#
    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.

  • 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 call system.methodHelp and system.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.