A first User Interface (UI) for your robot application

🏁 Goal of this tutorial

By the end of this tutorial, you will know how to create a user interface for your robot application, how to display it on the robot’s display, and how to connect the user interface to the robot’s main control script.

Pre-requisites

Note

As of PAL OS edge, this tutorial requires some familiarity with the command-line, and basic experience with running Docker.

What are we building?

Creating a UI with a rpk template

Step 1: generating the task skeleton

  1. start your Docker container:

xhost + # this allows your Docker container to access your host X server
mkdir -p ~/exchange # this is a shared folder between your host and the container

docker run -it --rm --name pal_docker \
           --device /dev/video0:/dev/video0 \
           -e DISPLAY=$DISPLAY \
           -v /tmp/.X11-unix:/tmp/.X11-unix \
           -u user:user \
           -v ~/exchange:/home/user/exchange \
           <robot-type>-<serial>-dev:alum-<release> bash

Replace <robot-type> and <serial> with the type and serial number of your robot(eg tiago-123), and <release> with the PAL OS version you are using (e.g., 25.01).

Once inside the container, source your ROS 2 environment:

source /opt/pal/alum/setup.bash
  1. go to your exchange folder and create a new workspace:

cd ~/exchange
mkdir ws
cd ws
  1. run rpk to create the task with a sample GUI:

$ rpk create -p src/ task
ID of your application? (must be a valid ROS identifier without spaces or hyphens. eg 'robot_receptionist')
> gui_task

Full name of your skill/application? (eg 'The Receptionist Robot' or 'Database connector', press Return to use the ID. You can change it later)
> <leave empty to re-use the ID>

Choose a template:
1: base task template [python]
2: simple task template with a graphical user interface [python]
3: 'greet' task mock-up [python]


Your choice?
> 2

What robot are you targeting?
1: Generic robot (generic)
2: Generic PAL robot/simulator (generic-pal)
3: PAL ARI (ari)
4: PAL TIAGo (tiago)
5: PAL TIAGo Pro (tiago-pro)
6: PAL TIAGo Head (tiago-head)

Your choice? (default: 1: generic)
> 2

Choose the simple_ui template (option 2), and the generic-pal robot (option 2).

The tool will then create a simple yet complete ROS 2 task, with a graphical interface we can use as a starting point.

Note

You can also pass all the rpk parameter directly from the command-line:

rpk create -p src/ --robot generic-pal task -y --id gui_task --template simple_ui

Check rpk --help for more information on the available parameters.

  1. build and source the workspace:

colcon build

Step 2: running the task

To run the task and display the user interface, you need to: 1. start the UI server, which will display the user interface; 2. start the task node. The task, however, will not run unless instructed to 3. call the task’s control server, to effectively start the task.

To do so, we need three terminals, each connected to the Docker container.

Note

Every time you need to connect to your Docker container from a different terminal, run the same two commands:

docker exec -it pal_docker bash
source /opt/pal/alum/setup.bash
  1. open a new terminal, connect to your Docker container, and start the UI server.

    In a new terminal:

    docker exec -it pal_docker bash
    source /opt/pal/alum/setup.bash
    ros2 run ui_server ui_server
    

    The UI server window should open:

    UI server window
  2. start the task. In the same terminal as the one use to compile your task, launch the node:

    source install/setup.bash
    ros2 launch gui_task gui_task.launch.py
    

    Nothing is displayed yet, as the task is currently ‘sleeping’: waiting to be explicitely started.

    You should however see something similar to this in the terminal, indicating that the task is correctly started:

    [INFO] [gui_task]: Initialising...
    [INFO] [gui_task]: Task gui_task started, but not yet configured.
    [INFO] [gui_task]: Task gui_task is configured, but not yet active
    [INFO] [gui_task]: Listening for UI messages on topic </gui_task/ui_msg>
    [INFO] [gui_task]: Task gui_task is active and running
    
  3. run the task. In a third terminal, run:

    docker exec -it pal_docker bash
    source /opt/pal/alum/setup.bash
    source ~/exchange/install/setup.bash # needed for the 'task_msgs' package
    
    ros2 action send_goal /gui_task/control task_msgs/action/TaskControl "task_data: ''"
    

    You should now see the user interface of the task:

    GUI task window

Important

You might have noticed that the ros2 action call did not return: this is expected! while the task is not complete, the action remains active, and the terminal is waiting for the task to finish.

Interacting with the task

Try to press the Do some work button a few times in the user interface. You will see the progress bar going up:

GUI task progress bar

If you look at the terminal where the task is running, you will see the following messages:

[INFO] [gui_task]: Received UI message: button pressed 1 times. Increasing task completion by 10%
[INFO] [gui_task]: Completed 10% of the task
[INFO] [gui_task]: Received UI message: button pressed 2 times. Increasing task completion by 10%
[INFO] [gui_task]: Completed 20% of the task
[INFO] [gui_task]: Received UI message: button pressed 3 times. Increasing task completion by 10%
[INFO] [gui_task]: Completed 30% of the task

In reality, the task script is interacting with the user interface in a bi-directional way:

image/svg+xml /ui/set_fragment[ui_msgs/SetUiFragment.srv] /gui_task/ui_msg[std_msgs/String.msg] /gui_task/task_progress[std_msgs/Int16.msg] taskgui_task

The initial visuals are set by calling the /ui/set_fragment service (exposed by the robot’s ui_server), and passing a QML fragment.

Note

QML is a declarative language used to create user interfaces, and is the language used by the ui_server to display the user interface on the robot’s screen.

Learn more about QML.

The task script then starts listening to the /gui_task/ui_msg topic, which is the topic where the user interface sends messages to the task script.

Every time the user clicks on the button, the user interface sends a message on this topic:

gui_task/res/ui/TaskUI.qml
 1import QtQuick 2.15
 2import Ros 2.0
 3
 4Item {
 5    // [...]
 6
 7    StringTopic {
 8        id: stringTopic
 9        isSubscriber: false
10        isPublisher: true
11        topic: "/gui_task/ui_msg"
12    }
13
14    // [...]
15
16    MainScreen {
17        id: mainScreen
18        property int counter: 0
19
20        // [...]
21
22        // btnAction1 is defined in MainScreen.ui.qml
23        btnAction1.onClicked: {
24            counter += 1;
25            stringTopic.value = "button pressed " + counter + " times";
26            stringTopic.publish();
27        }
28    }
29}

The task script then receives this message, and updates the task progress by publishing a message on the /gui_task/task_progress topic:

gui_task/gui_task/task_impl.py
 1# [...]
 2
 3 class TaskImpl(Node):
 4     def __init__(self) -> None:
 5         super().__init__('task_gui_task')
 6         # [...]
 7         self.completed = 0
 8         self._ui_msg_sub = None
 9         self._task_progress_pub = None
10
11     # [...]
12
13     def on_ui_msg(self, msg: String) -> None:
14         self.get_logger().info(f"Received UI message: {msg.data}. "
15                             "Increasing task completion by 10%")
16         self.completed += 10
17         self._task_progress_pub.publish(Int16(data=self.completed))
18
19     def on_activate(self, state: State) -> TransitionCallbackReturn:
20         # [...]
21         self._ui_msg_sub = self.create_subscription(String,
22                                                     '/gui_task/ui_msg',
23                                                     self.on_ui_msg,
24                                                     10)
25
26         self._task_progress_pub = self.create_publisher(Int16,
27                                                         '/gui_task/task_progress',
28                                                         10)
29         # [...]
30         return super().on_activate(state)
31
32     # [...]

Finally, the UI listen to the /gui_task/task_progress topic, and updates the progress bar accordingly:

gui_task/res/ui/TaskUI.qml
 1import QtQuick 2.15
 2import Ros 2.0
 3
 4Item {
 5    // [...]
 6
 7    StringTopic {
 8        id: stringTopic
 9        isSubscriber: false
10        isPublisher: true
11        topic: "/gui_task/ui_msg"
12    }
13
14    IntTopic {
15        id: taskProgressTopic
16        value: 0
17        isSubscriber: true
18        isPublisher: false
19        topic: "/gui_task/task_progress"
20    }
21
22    // [...]
23
24    MainScreen {
25        id: mainScreen
26        property int counter: 0
27
28        taskProgress: taskProgressTopic.value
29
30        // [...]
31
32        // btnAction1 is defined in MainScreen.ui.qml
33        btnAction1.onClicked: {
34            counter += 1;
35            stringTopic.value = "button pressed " + counter + " times";
36            stringTopic.publish();
37        }
38    }
39}

The visuals are actually updated in the implementation of the MainScreen QML component, in MainScreen.ui.qml:

gui_task/res/ui/MainScreen.ui.qml
 1 import QtQuick 2.15
 2 import "js/Constants.js" as Constants
 3
 4 Rectangle {
 5     id: mainScreen
 6     // [...]
 7     // this property is updated from TaskUI.qml when a msg is received
 8     // on /gui_task/task_progress
 9     property int taskProgress: 0
10
11     // aliasing the button so that it can be accessed from the parent component
12     // (TaskUI.qml in this case)
13     property alias btnAction1: btnAction1
14
15     // [...]
16     IconButton {
17         id: btnAction1
18         x: 95
19         y: 398
20         label: "Do some work"
21     }
22     // [...]
23     Rectangle {
24         id: taskProgressBar
25         x: desc.x
26         anchors.top: btnAction1.bottom
27         anchors.topMargin: 30
28         width: desc.width * (mainScreen.taskProgress/100)
29         height: 10
30         color: Constants.accentColor
31     }
32
33     Text {
34         id: taskProgressLabel
35         anchors.left: taskProgressBar.left
36         anchors.top: taskProgressBar.bottom
37         anchors.topMargin: 10
38         text: mainScreen.taskProgress + "%"
39         font: Constants.font
40         color: Constants.fgColor
41     }
42
43     // [...]
44 }

Summary

In this tutorial, you have learned how to create a simple user interface for your robot application, using the rpk tool to generate a task skeleton with a graphical user interface, and how to run it from a Docker image.

You have also learned how to interact with the user interface, and how to connect it to your task script, so that the user interface can send messages to the task script, and the task script can update the user interface.

In this tutorial, bi-directional communication between the UI and the main script is achieved using ROS topics. Other mechanisms are available (using ROS actions, services or parameters, or using /ui/update_state): you can combine them to best fit your needs, as we might see in follow-up tutorials.

Next steps

Modify the user interfaces

The user interface is written in QML. You can edit it either with Qt Design Studio, or by directly editing the QML files in a text editor.

You can modify the user interface by editing the QML files in the gui_task/res/ui/ folder. The main file is TaskUI.qml, which contains the

Install on the robot

You can also deploy this sanple task on your robot, and run it directly from the robot’s touchscreen. Check the Deploying ROS 2 packages on your robot page.

See also