Universal robot arm as .NET WPF application#

Background

Previously, we created an app that picks up and releases an object. The application we created in the previous post focuses on the usage of the class UniversalRobot using top-level statements. Moreover it uses a console, which may be not convenient to use compared to a graphical user application.

../_images/universalrobot-control-wpf.png

In the following we will setup a graphical WPF (Windows Presentation Foundation)-based application and replace the console with a window. WPF supports you to build a graphical user interface.

Creating the C# project#

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

  2. Use the search bar and search for wpf.

  3. Click on WPF Application that is for C# and then Next.

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

  5. Leave default settings and click on Create. The IDE window will show up with MainWindow.xaml and MainWindow.xaml.cs files open. The first file will have the focus.

  6. Build your project by pressing F5. A white window should pop up.

    🎉 You have a working program!

  7. Close your program by closing the window. Now you should be back on MainWindow.xaml.

Example application that integrates our previous robot control logic#

  1. Install UniversalRobots library using the NuGet package manager as in the previous post.

  2. If you only want to browse the files to copy some code, browse the code on the repository. Otherwise:

    You can directly download the files here:

    After downloading copy the files and paste them into your project.

  3. Build your project. You should see the user interface.

Ideas used in this example project#

Separation of concerns#

In a graphical app, we have a graphical user interface which can grow in complexity. Complexity is prune to errors, so we should separate responsibilities or concerns if a component of our program grows. The WPF framework template already enforces some separation of concerns by its project structure. App and MainWindow are separate. App should be responsible for the business logic and MainWindow for presenting information to the user and interaction.

Due to the low complexity of this example, I did not try to separate the business logic from the presentation logic completely. This step requires defining clear interfaces between these components, which is not the primary goal when creating a simple prototype.

Nevertheless, as an example for separation of concerns, I created a separate class for the robot command sequences called RobotProgram.

MainWindow instantiates UniversalRobot and RobotProgram and RobotProgram uses UniversalRobot to send commands to the robot.

Another aspect enforced by WPF is the separation of graphical structure and the functions-related to the interaction. xaml file is based on XML and is similar to HTML that you may be already familiar with. The xaml files describes the structure of a window and the events associated with them. An example event is Click on a button. For example <Button ... Click="buttonClicked" ... will execute the function buttonClicked whenever the user clicks a button. The function buttonClicked must be implemented in the corresponding C# code MainWindow.xaml.cs

Use of events#

In a GUI application, many back-end (business logic) components may want to communicate to the GUI. Instead of using a GUI function like printThisOnTheGUI, they can send strings to the room (metaphorically) and interested parties can receive them. RobotProgram uses EventHandler to do this. MainWindow listens to these events and prints them on the log screen

Use of asynchronous functions#

Our robot program can take many seconds. If we activate the sequence directly using a button, then the GUI must wait until the program completes and run its GUI-related functions like interaction with the user. This leads to a non-responsive GUI. We typically say GUI freezed.

To avoid this, we should create subprograms that run separately from the GUI program. An example is:

private void StartClicked(object sender, RoutedEventArgs e)
{
    Task.Run(() => robotProgram.Run());
}

Use of columns and rows#

Columns and rows in a grid can structure the elements in a convenient way. Example:

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="*" />
        <RowDefinition Height="*" />
        <RowDefinition Height="10*" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="10*" />
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>
    <TextBox x:Name="Logs" TextWrapping="Wrap" Text="" Grid.Column="0" Grid.RowSpan="4" FontFamily="Segoe UI Emoji"/>

    <Button x:Name="Connect" Content="Connect" Grid.Column="1" Grid.Row="0" Click="ConnectClicked"/>
    <Button x:Name="Disconnect" Content="Disconnect" Grid.Column="1" Grid.Row="1" IsEnabled="False" Click="DisonnectClicked"/>
    <Button x:Name="Start" Content="Start" Grid.Row="2" Grid.Column="1" IsEnabled="False" Click="StartClicked"/>
    <Button x:Name="Stop" Content="Stop" Background="OrangeRed" Grid.Row="3" Grid.Column="1" IsEnabled="False" Click="StopClicked"/>
</Grid>

First we declare each row and column available on our grid. The * notation allows describing the size of elements relative to each other. For example, the element with Width="10*" will be 10 times larger than an element with *.

Grid.RowSpan allows an element to span over many rows.

Use of IsEnabled to enable or disable GUI elements#

The xaml files describes the default state of the GUI elements:

<Button x:Name="Disconnect" Content="Disconnect" Grid.Column="1" Grid.Row="1" IsEnabled="False" Click="DisonnectClicked"/>

This information can be changed during runtime of the program, which is described in C# code.

private void DisconnectClicked(object sender, RoutedEventArgs e)
{
    robot.Disconnect();
    Disconnect.IsEnabled = false;
    Start.IsEnabled = false;
    Stop.IsEnabled = false;

Persisting settings#

Settings like IP and license data should be persisted when the app is closed and opened. This can be implemented by creating a settings file and loading it when the program starts.

Using a for loop to repeat similar robot sequences#

Loop is a superpower of a computer. If we have similar sequences, we can repeat these using a loop:

for (int x = 0; x < 2; x++)
{
    for (int y = 0; y < 2; y++)
    {
        var movingOverPose = new Pose(.125 + x * .05, -.425 + y * .05, -.08, 3.1, 0, 0);
        var grippingPose = new Pose(movingOverPose);
        grippingPose.Z -= 0.05;

Creating functions and using meaningful variable names to make the code more readable#

If you use movej most of the time with another function that checks whether the position is reached, then we could pack these two function into a single function.

public void MoveJ(Pose pose, double acceleration = 1.4, double velocity = 1.05, double time = 0, double blend_radius = 0)
{
    SendMoveJ(pose, acceleration, velocity, time, blend_radius);
    while (!GetPose().IsClose(pose)) ;

Moreover, using good variable names, e.g.:

var movingOverPose = new Pose(.125 + x * .05, -.425 + y * .05, -.08, 3.1, 0, 0);
var grippingPose = new Pose(movingOverPose);
grippingPose.Z -= 0.05;

These will make your robot control sequence more readable.