For each of my ROS projects, I create a Docker development environment to isolate the ROS environment from my host system’s environment. This allows me to easily run an old version of ROS (e.g., ROS Kinetic), on a newer version of Ubuntu (e.g., Ubuntu 22.04 Jammy). The inverse is also true: you can run a newer ROS version on an older Linux distribution. Docker is conceptually similar to a very light-weight virtual machine, but Docker allows you to more easily share hardware resources between the host machine and the Docker container.
Throughout this guide, I will be referencing an example git repository (https://github.com/SyllogismRXS/ros_docker_setup) that I created to demonstrate how to set up a ROS Docker development environment.
There are a number of other guides to setup a ROS Docker development environment:
I think that many of the articles focus on basic Docker commands, instead of an opinionated description of how to organize your ROS workspaces and repositories to facilitate collaborative development and continuous integration (CI).
In our ROS Docker setup, we want to be able to:
docker
command line arguments / options.As a warning, I’m writing this guide from the perspective of someone who uses Ubuntu for development, but most of the instructions should apply to anyone using a Linux-based (amd64) system. (Note to self: try these instructions on my Windows 10 partition).
Install Docker. Make sure to
follow the
post-installation
steps, so you don’t have to use sudo
to run Docker.
That’s all you should need! We will be installing all the messy ROS bits inside of a Docker container, so your host system will stay nice and clean.
On my host machine, I keep all of my ROS projects in a folder located at
~/ros
and I will use that directory in this guide to be explicit, but you can
use a different root directory.
Create the src
directory in a new ROS workspace:
mkdir -p ~/ros/ros_docker_example/workspace/src
In this case, the ros_docker_example
is the name of my current ROS
project. I keep multiple ROS projects in the ~/ros
folder. Each ROS
project gets its own set of Docker / Compose files.
Create a data
directory, which will be used to facilitate exchanging
files (e.g., ROS bags), between the ROS Docker container and your host system:
mkdir -p ~/ros/ros_docker_example/data
Clone the ROS Docker setup files from my GitHub repository under the
ros_docker_example
directory:
cd ~/ros/ros_docker_example
git clone https://github.com/SyllogismRXS/ros_docker_setup.git
The ~/ros/ros_docker_example
directory should now contain the
following:
Since this ROS Docker example targets the ROS2 Humble distribution, clone
the ROS2 example repository into your workspace’s src
folder. We will be
building these example projects to test our build system. You would replace
this repository with your own ROS repositories.
cd ~/ros/ros_docker_example/workspace/src
git clone -b humble https://github.com/ros2/examples.git
We will now create a .env
file in the same directory that contains the
docker-compose.yml
file with your user’s ID and group ID. This is needed,
so that we can launch ROS GUIs from within the Docker container.
cd ~/ros/ros_docker_example/ros_docker_setup
echo -e "USER_ID=$(id -u ${USER})\nGROUP_ID=$(id -g ${USER})" > .env
Docker compose will automatically
read the .env
file before running up
commands.
We can now build the Docker image in the ros_docker_setup
directory:
docker compose build
Bring up the Docker container, step into it, and run the minimal subscriber example
docker compose up -d dev
docker exec -it ros_humble /bin/bash
ros2 run examples_rclcpp_minimal_subscriber subscriber_member_function
In a different terminal, step into the already running container, and run the minimal publisher example:
docker exec -it ros_humble /bin/bash
ros2 run examples_rclcpp_minimal_publisher publisher_member_function
At this point, you should see ROS messages being printed to the terminal.
Launch the rviz2 GUI from inside the container:
docker exec -it ros_humble /bin/bash
rviz2
The primary use case for this Docker setup is to develop ROS packages. After
building the Docker image, you can start a container (once with the up
command,
enter into it (with the exec
command), run a build command (with colcon
),
and run other ROS commands. For example, you can start and enter a container:
docker compose up -d dev
docker exec -it ros_humble /bin/bash
At this point, you can use your host’s code/text editor to make changes to code
in your host’s workspace src
directory and they will immediately take effect
in the container’s src
directory. Then, inside of the container, build the
workspace with the command:
colcon build --symlink-install
You can use multiple Docker container terminals by running docker exec -it
ros_humble /bin/bash
in another terminal after the up
command was already
run once.
The following are detailed notes that describe some of the Docker
options/features and why they were chosen. This section is most helpful if you
following along with the docker-compose.yml
and Dockerfile
commands in the
ros_docker_setup
repository.
The
docker-compose.yml
file contains two separate “services”: dev
and dev-nvidia
. The dev-nvidia
service contains additional flags to share the host’s NVIDIA GPU with the
Docker container. In order to use the dev-nvidia
service, you will need both
an NVIDIA GPU and you will need to install nvidia-docker2
using NVIDIA’s
instructions.
The USER_ID
and GROUP_ID
variables are passed from .env
to the Docker
build command via the build arguments:
args:
USER_ID: ${USER_ID:-0}
GROUP_ID: ${GROUP_ID:-0}
The network_mode
is set to host
to allow the container to have full
access to the host’s networking system. This is fine for a development
environment, but should be made more restrictive in a deployed container
(not our current use-case).
network_mode: "host"
The DISPLAY
environment variable display is passed from the host to the
container to enable GUI applications. In addition, the .X11-unix
and
.docker.xauth
files are mounted from the host to the container in the
volumes
section to enable GUIs.
environment:
- DISPLAY=${DISPLAY} # Pass the display for GUI
- QT_X11_NO_MITSHM=1 # Needed by QT programs
- XAUTHORITY=/tmp/.docker.xauth
The src
and data
directories of the workspace are mounted from the host
to the container in the volumes
section. It is important to note that the
volumes
section of the docker-compose.yml
file is only relevant during
up
and run
commands, not during the build
phase. During build time,
the code is copied into the Docker image with the COPY
command in the
Dockerfile
.
volumes:
- /tmp/.X11-unix:/tmp/.X11-unix:rw
- /tmp/.docker.xauth:/tmp/.docker.xauth:rw
- ../workspace/src:/home/ros/workspace/src
- ../data:/home/ros/data
The .X11-unix
and .docker.xauth
files are mounted to help with
launching GUIs from the container.
The
Dockerfile
contains the instructions to build the ROS Docker image. Our Dockerfile
is
based on the osrf/ros:humble-desktop-full
Docker image provided by the
main ROS folks (OSRF, OpenRobotics, etc.) This image already contains an
installed version of ROS Humble and many of the GUIs and build utilities,
which we will be using.
FROM osrf/ros:humble-desktop-full
MAINTAINER Kevin DeMarco
ENV DEBIAN_FRONTEND noninteractive
SHELL ["/bin/bash", "-c"]
After declaring the FROM
image and some basic metadata, we install a
specific Debian package:
RUN apt-get update \
&& apt-get install -y \
python3-pip \
&& rm -rf /var/lib/apt/lists/*
In your own Dockerfile, you would augment this Debian package installation
list to fit your needs. You should list ROS package dependencies
explicitly in their
respective package.xml
files, but sometimes there are missing
dependencies in upstream packages.
Using the adduser
command, we create a ros
user with the same USER_ID
and GROUP_ID
as your host system, which enables GUI applications. In
addition, the ros
user can issue sudo
commands without a
password. Again, this is fine for a local development environment, but you
wouldn’t do this in a deployed Docker image.
ENV USERNAME ros
RUN adduser --disabled-password --gecos '' $USERNAME \
&& usermod --uid ${USER_ID} $USERNAME \
&& groupmod --gid ${GROUP_ID} $USERNAME \
&& usermod --shell /bin/bash $USERNAME \
&& adduser $USERNAME sudo \
&& adduser $USERNAME dialout \
&& echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
Run rosdep
update. Also, add the sourcing of the main ROS system’s setup
file (/opt/ros/humble/setup.bash
) and colcon’s setup file to the ros
user’s .bashrc
. This provides the ros
user with access to the ROS
environment every time the container is entered.
# Run rosdep update, add ROS, Gazebo, and colcon setup to ros user's .bashrc
RUN sudo apt-get update \
&& rosdep update \
&& echo 'source /opt/ros/${ROS_DISTRO}/setup.bash' >> /home/$USERNAME/.bashrc \
&& echo 'source /usr/share/colcon_cd/function/colcon_cd.sh' >> /home/$USERNAME/.bashrc
Near the end of the Dockerfile
, the code is copied into the image with the
COPY
command.
COPY --chown=ros ./src src
The code is copied into the image, so that we can install it’s dependencies
and make sure it builds deterministically. However, when the user runs the
docker compose up
command, the code on the host system is mounted over the
code that was previously copied into the image. This allows changes to the
repository code to show up immediately inside the container for incremental
builds. During the initial building of the image, the code that was copied
into the image is built with:
RUN source /opt/ros/${ROS_DISTRO}/setup.bash \
&& sudo rosdep install --from-paths . --ignore-src -r -y --rosdistro=${ROS_DISTRO} \
&& colcon build --symlink-install \
&& echo 'source ~/workspace/install/local_setup.bash' >> /home/$USERNAME/.bashrc
The ROS package dependencies listed in the package.xml
files are
installed during the rosdep install
command. colcon
is used to perform
the workspace build. In addition, the workspace’s local_setup.bash
is
added to the ros
user’s .bashrc
file, so that it’s resources are
immediately available when the container is entered.
By maintaining a Dockerfile
and a docker-compose.yml
file for each of your
ROS projects, you can isolate your ROS development environments from your
host’s operating system. It might be overkill to create a set of Docker files
for each ROS repository, but you might want to create a Docker development
environment for each set of ROS packages that you use for an overall
project. Typically, this would be at the ROS workspace level, but it could
cover multiple workspaces as well.
Finally, I use vcstool to track the various git
repositories in my workspace’s src
folder. It is common practice in ROS
development to maintain a vcstool .repos
file, which contains a list of the
git repositories that your project depends on for source builds. I track my
project’s .repos
file alongside the Dockerfile
and docker-compose.yml
files. Take a look at our Docker configuration
repo for the Clearpath Dingo for
an example.
In a future post I will describe how I use direnv
to
quickly open multiple ROS Docker container terminals.
It’s not that I have a hoarding problem. It’s that I don’t like waste. I don’t think our throw-away culture is healthy. The best place for a woodworker to store their wood is at the lumber yard, but woodworkers will always have extra pieces from previous projects. I want to be able to save the cut-offs from bigger woodworking projects for smaller projects.
See? I don’t have a problem! I should totally buy more wood to store my other wood! This guy’s wood storage design looks simple enough.
This empty white wall is just asking to be the supporting role in a terrifying hostage video. We better cover it up with a wood rack.
I started the project by cutting three 2x4’s to length, with a little 45° cut at the bottom to reduce chipping. These will be attached to the wall.
I then cut the pieces of metal conduit down to size with a reciprocating saw.
I don’t want to injure my delicate hands, so it’s important to clean up the rough metal edges. I don’t have a grinder or stationary belt sander, so I just clamped my belt sander to the table.
To increase the stability of the wood being stored on the rack, I wanted to slightly angle the conduit up. This means that I had to drill the holes into the 2x4’s at an angle. The drill press makes this job easy.
I knew I kept those two sets of forstner bits for a reason…
The hardest part about this project was installing the 2x4 supports on the cinder block wall. When drilling into a cinder block wall, the hammer drill makes a high-pitched crunching noise that makes me question home ownership and the integrity of the foundation.
I had to use a hammer drill to put holes in the cinder blocks to accept the Tapcon screws I used to hold the wood against the wall. Despite following the instructions for the Tapcon screws, it seems that the suggested masonry drill bit sizes never work with their anchors. I don’t know what’s wrong. It could be that maybe I’m illiterate, but Tapcon uses color codes to match the suggested drill bits size with the appropriate anchors, so that’s not it.
After a few hours of white-knuckling the hammer drill and grinding my teeth, I was able to get three good anchors through each 2x4 and into the wall.
To make sure the rack will hold the pieces of wood level, use a bubble level across the conduit between two 2x4’s while drilling the holes for the screws. This will ensure that the stored wood will distribute its weight evenly across the supports.
Yes, that’s satisfying. My shame is no longer on the ground, but proudly displayed on the wall.
]]>with scrap wood?
You get… Bamboomba!
You might be wondering why Kevin, being the professional researcher who he is, decided to build a half-wooden monstrosity with an obsolete robot base. Didn’t Kevin go to college for thirteen years or something? Why don’t you just buy a robot, Kevin? How is this robot going to help you pay your mortgage? Kevin.
Jeeeeeez, stop hassling me. I built Bamboomba to show what is possible with a couple of low-cost sensors and to learn about the new features in the Robot Operating System (ROS) 2 Navigation2 software stack. Also, I think it’s a really funny name.
To be completely honest, when my friend and I co-founded RIF Robotics in the fall of 2020, we submitted an application to Clearpath Robotics’ PartnerBot program and we won! That means we will be receiving an industrial-grade indoor ground robot in February 2021 named Dingo.
We will be using the ROS2 and Navigation2 software stacks to control Dingo and build maps of the environment with a LIDAR, but I wanted to get a head start on the software development, so I built Bamboomba as a surrogate platform. Both Bamboomba and Dingo will have the same sensor suite and software stack, so moving between the two platforms should be fairly seamless. I do know what I’m doing, right? The sensor suite consists of an Intel RealSense D435i and an RP LIDAR A2.
The D435i provides an inertial measurement unit (i.e., measurements of linear and rotational accelerations), a 2D RGB image, and a 3D point cloud of the environment. The RP LIDAR uses a rotating laser to measure distances to objects around the robot in 360°. There will be more on these sensors and how they help the robot to build maps and navigate around obstacles in a future post. The important part to note at this moment is that I hand cut and drilled the mounts for these professional sensors without using any CAD software or 3D printers. One of my industrial design friends said that he calls anything not cut by a CNC machine “folksy.” Like folk music. Not written down. I prefer the term, “Artisinal Robotics.”
That GIF isn’t from a random woodworking project. I was fine-tuning the mounting block for the LIDAR on the shooting board that I made with a chisel and hand router.
Bamboomba is loosely based on the Turtlebot design, which has been used extensively in the open source and academic robotics communities.
But Bamboomba is a lot cheaper, only took one day to build from scratch, and, literally, can’t be faithfully reproduced! The main idea I took from the Turtlebot was the multiple levels offset by aluminum stand-offs. This sparse design allows a roboticist to attach random sensors to the platforms and a closed laptop (the robot “brains”) can fit on one of the platforms. I was able to cut the platforms out of spare plywood using my scroll saw. I didn’t want to order aluminum stand-offs from an expensive mechanical parts store and, more importantly, wait two weeks for the parts to arrive, so I drove five minutes to Lowe’s and stared at the mechanical fasteners section for an hour until I came up with the following design for the stand-offs:
The aluminum rod cost $10 and I was able to cut it precisely with a pipe cutter:
Once the aluminum rod stand-offs were cut to size, it was fairly easy to assemble the platforms. The most tedious part was determining where to drill the holes for the screws to pass through without a template. This involved drawing intersecting lines to find the midpoint between mounting screw locations and transferring hole points by laying the wooden platforms directly on top of each other. If you look closely, you can see my layout lines.
When designing a ground robot with a single planar LIDAR, one of the most difficult design decisions is where to place the LIDAR. If the LIDAR is mounted at a low-point on the robot, it will ensure that the LIDAR doesn’t miss any obstacles close to the ground. However, if the LIDAR is close to the ground, the laser will most likely be obstructed by other mechanical structures on the robot. This is opposed to mounting the LIDAR on top of the robot, where it can have an unobstructed 360° view of its surroundings. However, the LIDAR will not be able to see objects that are below its line-of-sight, which could result in collisions with low-lying obstacles! Of course, you could use multiple LIDARs to cover the blind spots, but that costs more money and Bamboomba is not that kind of robot. I drilled mounting holes for the LIDAR in both the bottom and the top platforms, so that I could experiment with mounting the LIDAR at two different heights. When the LIDAR is in the bottom configuration, I route the LIDAR and serial cables behind the aluminum stand-off to reduce the obstructions in the LIDAR’s field-of-view. The cables are beautifully secured in place with zip ties.
When the LIDAR is mounted in the top configuration, it has to be raised slightly to ensure that its laser is not obstructed by the RealSense camera. This is done with the 3/4” piece of wood that played the starring role in the previous animated GIF.
Finally, the laptop is mounted to Bamboomba with velcro to make it easy to remove.
I hope this article will inspire others to hack their own Roombas into wooden abominations. There are other ways to build or buy a similar robot, but I like mine best. Bamboomba’s platform is made from readily accessible parts that can be put together in a day with some basic cutting tools.
I’ve got to come clean about something that has really been bothering me. The robot’s base is not really a Roomba. It’s an iRobot Create. But the bamboo + Roomba name was soooo good and it’s basically the Roomba without the vacuum. My roommate from college gifted me the Create back in 2008 for being in his wedding when I had not even considered studying robotics in graduate school. He knew what I wanted.
In a future post, I will describe how the ROS2 and Navigation2 software stacks are used to build maps and navigate around obstacles in the environment with the LIDAR.
Thanks for following along.
]]>My educational background is in Electrical and Computer Engineering with a strong bent towards Computer Science. This means that I spend most of my day typing on a computer, reading technical papers, designing systems, writing code, and communicating with team members. Even when I am designing mechanical systems for the real world or using CAD programs to 3D print objects, almost all the work is done on a computer. That’s why I prioritize woodworking. I believe, that as humans, we need short-term, tangible (i.e., physical) outcomes to keep us from obsessing over potentially non-real things. Woodworking provides the physical connection to the world that so many computer-addicted folks desire. I can’t tell you how many times I’ve come across a programmer’s blog who also listed “woodworking” as one of their hobbies. I should probably start a Meetup.com group for these people (myself included). Throw in “bicycling” and my target support group is pretty well defined.
Now that I have sufficiently pigeonholed myself, I can describe the dovetail box. I built this dovetail box as a Christmas present for my father. With the 2020 pandemic in full-force, which made visiting family members irresponsible, I decided that a hand-made gift was better than feeding the online e-commerce beast. After I bought the lumber from a store, I built it completely with hand tools, no electricity required.
The box is made from 3/4 inch poplar that I purchased from a local hardware store. Poplar is a nice wood for beginner hand tool woodworkers. It is considered a “hard” wood, which means it is less likely to splinter and shatter like pine, but it won’t dull the edges of your blades as quickly as other hard woods, such as red oak. Also, poplar is widely available in the big box stores at a reasonable price. Since my 7th grade wood shop class didn’t teach us how to use traditional hand tools, I learned from the masters on YouTube. When possible, I subscribe to the Paul Sellers school of thought when working wood. Many of the techniques that I used to build this dovetail box were from his video on the same subject.
What is both appealing and intimidating about Paul Seller’s approach to woodworking is that it requires practice, artistry, and learned skills. Anyone can read learn how to make straight cuts on a table saw in a matter of minutes, but learning how to set and use a plane to flatten a piece of wood could take months or years. For Paul Sellers, the woodworker’s skill with the saw or plane can overcome any imperfections in the wood or tool - “A Good Craftsman Never Blames His Tools” (of course this statement has multiple interpretations). From my perspective, woodworking is a hobby that I use to escape from the technical perfection that is required in my daily profession. I enjoyed rounding over the edges of this box with a hand plane, which felt like free-hand sculpting.
I have also borrowed techniques from David Barron. I prefer his technique of using a fine-toothed coping saw to cut the waste from dovetails instead of “chopping” them with a chisel, which Paul Sellers prefers. Still, I often use Paul Sellers’ technique for setting and installing hinges for box lids and doors. The accuracy on these recessed hinges, without using a measuring device or jig, is quite satisfying.
Poplar isn’t known for having nicely “figured” wood. However, I was able to identify an interesting feature on this poplar and I used it on the lid of the box.
To finish the box, I used a single coat of colonial maple stain and two coats of danish oil.
When the lock down of 2020 began, I made myself a commitment to improve my hand tool working skills. Getting better at using hand tools is very similar to getting better at playing a musical instrument (c.f. Jeff Miller]). Both activities require constant regular practice and assessment. Excuse me while I go make 365 dovetail boxes.
]]>However, when working remotely, pair programming can be difficult without a simple and secure process. In this post, I describe the steps required to setup a secure pair programming process in which one (remote) developer SSH’s into another (host) developer’s Linux machine. The host developer will be able to specify which remote developers have access and also see the command line keyboard entries made by the remote developer. We will also configure the host machine such that the remote user will not be able to SSH into the host machine unless the host developer runs a specific terminal sharing program (i.e., wemux). While this setup is fairly secure, you shouldn’t grant access to remote developers that you don’t trust.
A high-level diagram of the setup is shown in the following figure.
As shown in the diagram, the pair programming server is initiated by the host
developer running the wemux
command. The remote developer then uses SSH to
log into the pair
Linux user on the host machine. The pair user’s
~/.profile
file is configured to run the command, wemux pair; exit
, which
forces the remote user to immediately join the wemux session upon login and
exit the SSH session when the wemux session ends. Finally, to make it easier to
SSH into computers that are behind firewalls, proxies, and VPN servers, the
host machine will use ngrok
to provide an SSH tunnel (cf. ngrok).
Install the SSH server, tmux, and the snap package manager with your system’s package manager:
ngrok will be used to create an SSH tunnel to the host machine that can be securely accessed behind firewalls and VPN servers.
Create an ngrok account: https://dashboard.ngrok.com/login
After creating your ngrok account, you will need to use the tunnel
authorization token in your ngrok account to authorize your machine. Copy the
authorization token from the website,
https://dashboard.ngrok.com/auth, and run
the following command, where <authorization-token>
is the copied token:
wemux is a project that allows multiple developers to directly collaborate on the same command line. (wemux leverages tmux.) First, clone the wemux project to a local directory. Typically, I keep 3rd-party repositories in a separate directory:
Now, configure wemux to only allow the host user to start a wemux session:
Symbolically link the wemux
executable to a directory that is on the system
$PATH
and link the configuration file to the default location (you will need
to run the following commands with sudo
):
We will create a new Linux user, called pair
, that will only be used by
remote pair programming users. You can accept the default values when running
the adduser
command, but make sure you specify a non-trivial password for the
new user.
Now that the user has been created, let’s modify the user’s ~/.profile
file
such that when the remote user logs in, they are immediately dropped into the
currently running wemux
session.
If the remote user tries to log into the pair
user and the host isn’t running
wemux
, then the user’s SSH session is immediately terminated. Through this
mechanism, the host developer specifies when the remote user can access the
pair
user. Also, the exit
command terminates the pair
user’s session
when the wemux
session ends.
To hold the public keys from remote developers, let’s create the
authorized_keys
file for the pair
user:
For additional security, let’s configure the SSH server such that the pair
user can’t login with a password and then we’ll restart the SSH server:
The following are the typical steps required when initiating a new pair programming session.
The remote user will log into the pair
user, but we don’t want the remote
user to have to know the password for the pair
user, so we will use
public/private key authentication for SSH login. The remote user needs to send
their public key (typically located at ~/.ssh/id_rsa.pub
) to the host
developer. If the remote developer hasn’t generated SSH keys yet, the remote
user should generate SSH keys with the following command (substituting a valid
e-mail address):
The id_rsa.pub
file can be transferred to the host developer via e-mail,
chat, etc. Upon reception of id_rsa.pub
, the host developer can add the
public key to the pair
user’s authorized_keys
file to enable password-less
SSH login:
The host developer will now use the ngrok
command to expose port 22 of the
SSH server:
The ngrok connection information will be printed to the screen. Note the information in the “Forwarding” section as this will be sent to the remote developer to access the host’s SSH server. For example, if the “Forwarding” section contained the following information:
The remote user would use the following SSH command to log into the host’s
machine (the -p
flag specifies the SSH port):
The remote developer will not be able to log into the pair
user until the
host developer starts the wemux session in a separate terminal:
At this point, the remote developer can run the previously shown SSH command. When, the host exits the wemux session, both users will be removed from the session and the remote user will be logged off.
If you don’t want the remote user to be able to enter commands, you can force
the remote user into the “mirror” mode by changing wemux pair
to wemux
mirror
in the ~/.profile
file.
After you modify the pair
user’s ~/.profile
file, you won’t be able to
easily log into pair
user with the normal su pair
command without having to
start the wemux server. Instead, you can just directly modify the user’s
configuration files by prefixing your editor call with sudo
:
The following blog posts were used to put together this post:
]]>