Thursday 15 August 2024

Using a Game Controller with ROS2 in Docker

Continuing our learning of ROS2 running in Docker containers on top of Raspberry Pi OS (see previous posts in this blog). ROS comes with joystick support in the form of the joy package. https://docs.ros.org/en/jazzy/p/joy/ 
To get this working inside Docker, we need to map our joystick input to our container. In the previous post, we created a Docker Compose configuration file which looked like this:

services:

    sim:

      image: ros_docker:latest

      command: ros2 run turtlesim turtlesim_node

      environment:

        DISPLAY:

        QT_X11_NO_MITSHM: 1

      volumes:

        - /tmp/.X11-unix:/tmp/.X11-unix:rw

    dev-build:

      image: ros_docker:latest

      command: rqt

      environment:

        DISPLAY:

        QT_X11_NO_MITSHM: 1

      volumes:

        - /tmp/.X11-unix:/tmp/.X11-unix:rw

        - ~/ros2_ws:/ros2_ws


Before we try mapping our controller, let’s check it is actually connected to the Raspberry Pi. We are using a bluetooth game controller here. This should work with a PS4 Dualshock Controller, an X-Box One S wireless controller, or an 8BitDo Pro2 controller (which can mimic various controllers). Start by pairing your controller through the Raspberry Pi OS bluetooth tray icon. Select ‘Add Device’, put the controller into pairing mode, wait for it to appear in the detected devices list and then select it and press ‘Pair’.

Once you have a controller connected, type the following command in a terminal:


ls /dev/input


You should see among the various event items, there is a 'js' item.

 

$ ls /dev/input  

by-id    event0  event2  event4  event6  event9  mice

by-path  event1  event3  event5  event7  js0     mouse0


In my case I saw js0 which indicates my game controller is available as an input device.


Now we can map our input devices into our docker container. I mapped all of them, so any input device becomes available. Add the following section to dev-build service section in the docker-compose.yml file:


      devices:

        - /dev/input:/dev/input


Now we have changed our docker-compose.yml file, we need to stop and restart our container. Docker Compose will detect the change, and destroy the old container and recreate it. So once it starts up again, we will need to source our ROS2 packages again (just once if we put the command into the bash.rc file again). Open a shell in the container:

docker exec -it jazzy-dev-build-1 bash


Then run following command from the shell:

echo "source /opt/ros/jazzy/setup.bash" >> ~/.bashrc


Now exit the shell, and use docker exec to start a new one. You should be able to run ROS2 commands from the shell now. You can also confirm that the joystick input is available inside the container by using the command ‘ls /dev/input’ from the container shell prompt. If you can see the 'js' item then we are ready to use it in ROS.


First, we can use the joy package to list all the available joysticks:

ros2 run joy joy_enumerate_devices


This should list the game controller. There are some gotchas to be aware of here. I found that if my game controller was not connected to the host Pi over bluetooth before I started my container then it would not be detected inside the container after it was connected. So if the controller turns off/loses the connection, then it still appears in the input devices list under /dev/input but does not get detected by ROS. You have to restart the container after reconnecting the controller.


Once you have seen your controller in the list output by the joy_enumerate_devices node, you can start the joy node as follows:

ros2 run joy joy_node


Then in a new terminal, open another shell in your container and run the following to see the outputs from the controller:

ros2 topic echo /joy


This confirms the joy node is posting messages to the /joy topic which you can consume in ROS nodes which subscribe to this topic.


You can also monitor the joy topic in the rqt tool. Open the ‘Plugins/Topics’ menu and select the ‘Topic Monitor’. Tick the /joy topic and you should be able to see the values of the various game controller buttons and joysticks (hold them and wait a little as the refresh rate appears to be only once per second or so in the topic monitor).


Now this is all working, we can try and write a node to subscribe to the /joy topic and make use of the game controller input. We will start with the existing ros2-teleop_twist_joy package: https://index.ros.org/p/teleop_twist_joy/github-ros2-teleop_twist_joy/#jazzy 


We can initially run this directly on our ROS desktop container as it is installed there already. Run the command:

ros2 launch teleop_twist_joy teleop-launch.py


This worked without needing more specific configuration and detected my game controller. It launches the joy node, so you do not need to run that separately. The teleop_twist_joy node subscribes to the /joy topic, and converts the data received from the game controller to 'Twist' data which it publishes to the /cmd_vel topic. In a new shell, you can monitor the /cmd_vel topic:

ros2 topic echo /cmd_vel


The teleop twist node only sends messages when ‘button 8’ is pressed on the controller. This was the left trigger (L2) on my controller. I had to hold it down to see messages in the /cmd_vel topic. 


We can remap the topic name which teleop_twist_joy uses, by passing in a different topic via a command line parameter. So let’s set it up to use the /turtle1/cmd_vel topic:

ros2 launch teleop_twist_joy teleop-launch.py joy_vel:='turtle1/cmd_vel'


You should now be able to control the turtle1 in the turtlesim node, using the game controller. Hold down the enable button (L2 in the case of the default mapping) and the left joystick should be able to drive the turtle around.


Open another shell in the container, and run the command:

rqt_graph


This should open a window showing the node graph. Change the drop down selector to show ‘Nodes/Topics (all)’ and you should see the complete node and topics graph as follows.





Tuesday 13 August 2024

ROS2 with Docker Compose on a Raspberry Pi 5

Continuing our learning of ROS2 running in Docker containers on top of Raspberry Pi OS (see previous post in this blog). The management of the Docker containers can be further simplified with the Docker Compose tool. If you followed the Docker installation instructions in my previous post then you already have Docker Compose installed. So you can get straight on with using it.
Docker Compose allows us to manage multiple containers with a single configuration file. A Docker network is automatically created so the containers can communicate with each other. The tool works from a yaml file named docker-compose.yml which contains the complete configuration of each container.

In the previous post, we created our container with a command line containing several parameters. These can be put into the docker-compose.yml file to simplify the process.
Starting with an empty directory, create a new file named docker-compose.yml with the following contents:

services:

    sim:

      image: ros_docker:latest

      command: ros2 run turtlesim turtlesim_node

      environment:

        DISPLAY:

        QT_X11_NO_MITSHM: 1

      volumes:

        - /tmp/.X11-unix:/tmp/.X11-unix:rw


I put this file in a folder named 'jazzy'. Docker Compose will use the folder name to generate the container names. This docker-compose.yml file contains a single container definition 'sim'.
From a terminal in the jazzy folder, run the command:

docker compose up -d


This will create a new container, start it up and run the command to launch the turtlesim_node in the turtlesim package. The '-d' option runs the commands in the background, returning the terminal prompt to you (otherwise the terminal window is tied up until the node is terminated).

You should see the TurtleSim window open. Run the command docker ps to see what containers are running. You should see a new container was created named 'jazzy-sim-1'. This time instead of being randomly generated the container name has been taken from the folder name, and service name used. As it is the first instance, it is appended with '-1'.

If you close the TurtleSim window, the container will exit as the node it was running has terminated. If you run the 'docker compose up -d' command again it will restart the existing container, rather than creating a new one each time, compared to the 'docker run' command we used previously.
We can add more services to run additional containers and launch them as a group. Extend the docker-compose.yml file to contain the following:


services:

    sim:

      image: ros_docker:latest

      command: ros2 run turtlesim turtlesim_node

      environment:

        DISPLAY:

        QT_X11_NO_MITSHM: 1

      volumes:

        - /tmp/.X11-unix:/tmp/.X11-unix:rw

    dev-build:

      image: ros_docker:latest

      command: rqt

      environment:

        DISPLAY:

        QT_X11_NO_MITSHM: 1

      volumes:

        - /tmp/.X11-unix:/tmp/.X11-unix:rw


Now when you run 'docker compose up -d' you should see you have 2 containers, one named 'jazzy-sim-1' and a second named 'jazzy-dev-build-1' running the rqt tool. As these are automatically in the same docker network, the rqt tool can control the TurtleSim node as in the ROS Tutorials. If you close one of the tool windows (stopping its container) and run 'docker compose up -d' again, it will detect that one of the containers is already running, and just launch the one which is stopped.


As before, you can open an interactive shell in these containers, using the exec command:

docker exec -it jazzy-dev-build-1 bash


This allows us to set up the shell to source the ROS packages as before, by running the following command from the shell:

echo "source /opt/ros/jazzy/setup.bash" >> ~/.bashrc


Now exit the shell, and use docker exec to start a new one. You should be able to run ROS2 commands from the shell now. Interestingly, it was not necessary to source the setup.bash script in the docker-compose.yml file to run the ROS2 commands there. I am not clear why it works, but it does so I'm happy with it.

Access to the file system of the Host

Now we can use Docker Compose to manage our containers, we can take a look at mapping the host Pi file system inside our containers. Following the Client Libraries tutorial at: https://docs.ros.org/en/jazzy/Tutorials/Beginner-Client-Libraries/Colcon-Tutorial.html we need to create a folder on our host Pi called 'ros2_ws'. I created this folder in my home directory. Next add the path to this folder on the host, to the path '/ros2_ws' in the dev-build container, by changing the container definition in the docker-compose.yml file to the following:

    dev-build:

      image: ros_docker:latest

      command: rqt

      environment:

        DISPLAY:

        QT_X11_NO_MITSHM: 1

      volumes:

        - /tmp/.X11-unix:/tmp/.X11-unix:rw

        - ~/ros2_ws:/ros2_ws


Stop the container (by closing the rqt tool window) and start it again by running the 'docker compose up -d' command. Now if you open a shell in the jazzy-dev-build-1 container, you should see a folder /ros2_ws which will be mapped to the folder ~/ros2_ws on the host. This should enable you to run the package building tutorials with the files being saved outside your container. So if your container is ever destroyed, the files are not lost when a new container is created by Docker Compose. Note that as we have changed our docker-compose.yml file, Docker Compose will detect the change and destroy the old container and recreate it. So once it starts up again, we will need to source our ROS2 packages again (just once if we put the command into the bash.rc file again).

In the next post, we will look at using a game controller in ROS in a Docker container.

Running ROS2 on a Raspberry Pi 5

ROS2 on the Raspberry Pi requires the 64bit version of Pi OS Bookworm, and docker to run ROS. I found the tutorials and documentation assumed a lot of knowledge of both Docker and ROS so it was a challenge to get started. ROS on Linux is not natively supported on Debian (which the Raspberry Pi OS is based on). ROS runs on Ubuntu, so you could install that OS on the Pi, but many users want to run the Raspberry Pi OS to benefit from the support for hardware and libraries provided for Pi projects. So we need to run ROS inside an Ubuntu docker container. Most of the guides either tell you a series of commands to run which gets you through, but feeling like you don’t understand much of what is going on, while the documentation offers you endless choices or decisions to make, leaving it difficult to know which way to turn at each step. This guide is my attempt to lead you through this forest, and get you to the other side empowered with some understanding of the path we took and how things work. Links to the source documentation are provided at each stage, and these should be followed alongside this guide. We will start with a vanilla installation of Raspberry Pi OS Bookworm. Make sure you are using the 64bit version.

Installing Docker

Install docker on Debian bookworm following the instructions at https://docs.docker.com/engine/install/debian/ 


I used the method in the section: Install using the apt repository


# Add Docker's official GPG key:

sudo apt-get update

sudo apt-get install ca-certificates curl

sudo install -m 0755 -d /etc/apt/keyrings

sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc

sudo chmod a+r /etc/apt/keyrings/docker.asc


# Add the repository to Apt sources:

echo \

  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \

  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \

  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt-get update


sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin


Next follow the post install steps:

https://docs.docker.com/engine/install/linux-postinstall/ 


Create the docker group.

sudo groupadd docker


Add your user to the docker group.

sudo usermod -aG docker $USER


Log out and log back in so that your group membership is re-evaluated.


Now you should be able to run docker commands without needing to use sudo.


Test docker is working:

docker run hello-world


This should show a success message.


Building a ROS Docker Container

Following the documentation at https://docs.ros.org/en/jazzy/How-To-Guides/Installing-on-Raspberry-Pi.html 


I built a docker image myself, in order to get the desktop variant required to run the tutorials. First clone the ROS  docker images git repo from your home directory:

git clone https://github.com/osrf/docker_images


The docs then say to navigate to the folder for the release you want and build the container. But which folder to choose? I went with the ‘Jazzy Jalisco’ version of ROS2, as the latest stable supported release so it has the longest support lifetime. You also need to choose a version of Ubuntu to run it on. I chose noble as the only option for Jazzy docker files for all the ROS distributions. We are going to build the desktop version, as this has all the tools we need for the tutorials. But the container image will be around 2.4GB in size, so make sure you have enough free disk space. The ROS tutorials should work with other supported distributions, so you could choose the rolling/jammy option if you want to try the latest development build. But while I found this worked for the ROS CLI tools tutorials, it failed to build all the examples in the Client libraries tutorial. Also the Rolling desktop docker image was 3.3GB in size, and used python 3.10 whereas the jazzy image used python 3.12. So I have switched back to Jazzy for now.


Navigate to the folder ~/docker_images/ros/jazzy/ubuntu/noble/desktop

Then run the command:

docker build -t ros_docker .


Note the ‘.’ on the end of the command, it is important, as it tells docker to use the docker file in the folder you are running the command in.


This creates a docker container image, not a container. It will take a while (a go and make a coffee amount of time). When it completes, you should be able to see a docker image named ros_docker if you run the command:

docker image ls


Now we can create a docker container from our image:

docker run -it ros_docker


This will create a contain from the image and open an interactive shell in the container, so now the command prompt is open in a shell inside the container. 


At this point we have a ROS container. Open a new terminal window on the Pi and type:

docker ps


You should see your container listed as a running container.


We need to configure the environment for ROS. https://docs.ros.org/en/jazzy/Tutorials/Beginner-CLI-Tools/Configuring-ROS2-Environment.html 

We want this configuration to apply for any future shell sessions so we will put the commands into the bash.rc file in our container by entering the command:

echo "source /opt/ros/jazzy/setup.bash" >> ~/.bashrc


Now if we exit and re-enter our container the environment configuration should stick. Type ‘exit’ to leave the container shell. The container remains running (check with docker ps). We can enter a new interactive shell in it by using the container name. Names are generated randomly when containers are created. So you need to note the name your container has when you list it with the docker ps command. Then run:

docker exec -it <container_name> bash


But substitute <container_name> with the actual name of your container. Once in the new shell, we can test that our environment is configured as expected:

printenv | grep -i ROS


This should list all the ROS environment variables. So you should see something like this:

ROS_VERSION=2 ROS_PYTHON_VERSION=3 PWD=/ros2_ws AMENT_PREFIX_PATH=/opt/ros/jazzy CMAKE_PREFIX_PATH=/opt/ros/jazzy/opt/gz_math_vendor:/opt/ros/jazzy/opt/gz_utils_vendor:/opt/ros/jazzy/opt/gz_cmake_vendor ROS_AUTOMATIC_DISCOVERY_RANGE=SUBNET PYTHONPATH=/opt/ros/jazzy/lib/python3.12/site-packages LD_LIBRARY_PATH=/opt/ros/jazzy/opt/rviz_ogre_vendor/lib:/opt/ros/jazzy/lib/aarch64-linux-gnu:/opt/ros/jazzy/opt/gz_math_vendor/lib:/opt/ros/jazzy/opt/gz_utils_vendor/lib:/opt/ros/jazzy/opt/gz_cmake_vendor/lib:/opt/ros/jazzy/lib PATH=/opt/ros/jazzy/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin ROS_DISTRO=jazzy


If all that is as expected, then we should be able to run a ros2 command:

ros2 pkg executables 


This should list all the ros2 packages which are installed (the desktop version has quite a lot of them).


We now have a working ROS2 install on our Raspberry Pi. But before we get into the tutorials, it will be helpful to know a few more docker commands.


Some Useful Docker Commands

Now we have played around with docker a bit, it is useful to know how to manage the docker artifacts we may have created. For example, we will have created a local copy of the hello-world image, and a hello-world container for each time we ran the docker run hello-world command.


List your docker images:

docker image ls


List your docker containers:

docker ps -a


(The -a flag includes stopped containers. Without it, only running containers are shown).


To stop and start containers, we can use their container names with these commands:

docker start <container_name>

docker stop <container_name>


To clean up docker containers, note their container ID shown with the docker ps -a command and delete them using their ID:

docker rm <container ID>


Each time we use the docker run command with an image name, we create a new container. We don’t want this, as our environment will not be configured in the new container. So use start and stop instead once we have a container. (This goes against the philosophy of Docker, where containers are created and destroyed so you always run from a clean state. But we are just running some tutorials here, so our container will be treated as persistent with the configuration here. We’ll go deeper into docker and the docker compose tools in another tutorial).


We have run some shell commands, but our container will not have a graphical context so the GUI tools will not run. Try running the rqt command in your container and you will get an error about not being able to access the display. We need to give our container the hooks to access our display.

Following: https://wiki.ros.org/docker/Tutorials/GUI 

I used the simple (insecure) option. Unfortunately you have to provide the parameters when you create the container, so we will have to start again with a fresh container.


docker run -it \

    --env="DISPLAY" \

    --env="QT_X11_NO_MITSHM=1" \

    --volume="/tmp/.X11-unix:/tmp/.X11-unix:rw" \

    ros_docker


Now we have a container which can access the graphics display of the host Pi. We need to configure the environment again in our new container:

echo "source /opt/ros/jazzy/setup.bash" >> ~/.bashrc


This only needs to be done once in the new container. We also need to grant permissions to allow the display to be accessed. In a terminal in the host Pi, run the command:

xhost +local:root


Note this is not secure, but it only lasts until the next reboot, so I was happy with this on a private network. The linked article goes into more detail, and gives some other options. But this was the simplest one to get working on a Raspberry Pi.


Now, in the shell in the container, you should be able to run the command:

rqt


The GUI tool window should open successfully on your host Pi desktop. Now you have a container which can run graphical tools. If you get an authorisation error, check you have run the xhost +local:root command again since you last booted up the Pi.


Remember we just created a new container, so run docker ps -a to find out what the new container name is, and use this one for the graphical tools in the tutorials. You can rm the older container as we no longer need it.


Recap

First we built a docker container image for a specific version of Ubuntu and a specific ROS2 distribution. We used the desktop variant as it contains all the packages needed for graphical demos and the tutorials.

Next we created a docker container using this image to run ROS2. We created it with a configuration so it can run graphical tools on the host desktop. We set up the bash.rc file to configure the shell each time we start a new shell, so the ROS2 tools can be run. Once we had created this container, we start it with docker start <container_name> and enter a new shell with the command docker exec -it <container_name> bash


We don’t need to use docker run again, unless we want to create a new container with a different configuration.


Running the TurtleSim Tutorial in Docker

The turtlesim tutorial takes you through some basics of ROS using a graphical simulation tool and the ROS GUI tool RQT. The documentation is at:

https://docs.ros.org/en/jazzy/Tutorials/Beginner-CLI-Tools/Introducing-Turtlesim/Introducing-Turtlesim.html

But we can skip the install steps with our preconfigured docker container and jump straight to the run step. First open an interactive shell in your docker container (you need it to be started first). Then in the shell, run the command:

ros2 run turtlesim turtlesim_node


The turtlesim window should appear on your host desktop. 

The shell window will be tied up now, running the simulation node and displaying output from it. So to run another node to control the turtle, we need to open a new terminal window on the host, and start a second interactive shell in the same docker container:

docker exec -it <container_name> bash


Now in this second shell, we can run the turtle_teleop_key node:

ros2 run turtlesim turtle_teleop_key


This should launch the turtle_teleop_key node and you should be able to control the turtle with the arrow keys. The tutorial then suggests some ros2 commands you can run. So you will need a new terminal window to execute another interactive shell in the docker container. You can use this to run the ros2 commands to list nodes, and later run the rqt tool to follow the tutorial steps to execute various api calls to the turtlesim node.


Hopefully this has got you up and running with ROS in Docker on the raspberry Pi. If there are any steps that didn’t make sense, please let me know so I can fill in any gaps in this guide.


See the next post in this blog for how to extend this using Docker Compose.