Сага о том, как я клеил ROS и Docker
Это в общем-то первая статья на хабре, пробная и экспериментальная. Цель статьи изложить процесс создания темплейта под разработку для ROS (Robot Operating System) внутри контейнера и сделать это в шутливой манере.
Началось с того, что мне потребовалось установить контейнер с cuda. Все готовые контейнеры с ROS и cuda на докерхабе имели либо проблемы со стартом, либо имели битый пакетный менеджер. Я бы хотел сделать его несколько универсальным, чтобы адаптировать к любым своим проектам.
Dockerfile, собирали всем селом
Конечно, каждый хотел бы схалтурить и взять уже готовый образ, видит бог, я этого не хотел, но придётся ставить все ручками, наследуясь от безпроблемных образов. Поэтому идем на страничку и смотрим как ставить ROS на простую систему.
Любой контейнер начинается с докерфайла, поэтому начнем с установки ROS в Docker. Во-первых для ROS нужен полноценный контейнер, что-то типо убунты одной из не сильно допотопных версий, второе нужно выдернуть строчки из гайда по установке. Единственное что, для какого-то из пакетов от ROS требуется таймзона. Аргументы в свою очередь нужны будут дальше для фокусов, да и в целом полезно в них разобраться, чтобы сделать немного динамический и универсальный докерфайл.
FROM ubuntu:20.04
ARG hostname
ARG host_ip
ARG ros_master_uri
RUN apt update
ENV TZ=Europe/Kiev
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN sh -c 'echo "deb http://packages.ros.org/ros/ubuntu focal main" > /etc/apt/sources.list.d/ros-latest.list'
RUN apt install -y curl gnupg gnupg2 gnupg1
RUN curl -s https://raw.githubusercontent.com/ros/rosdistro/master/ros.asc | apt-key add -
RUN apt update && apt install -y ros-noetic-ros-base
ENV ROS_DISTRO noetic
RUN apt install -y python3-rosdep python3-rosinstall python3-rosinstall-generator python3-wstool build-essentia
Для универсальности и удобства установим рабочую директорию и переменную среды с путем к проету.
ENV PROJECT_DIR=/root/catkin_ws
ENV ROS_MASTER_URI=${ros_master_uri}
WORKDIR /root/catkin_ws
На этом шаге нам и пригодятся переменные среды выше, появляется задачка немного сложнее, чтобы подтянулись все утилиты ROS'а нужно сурснуть файлик /opt/ros/$ROS_DISTRO/setup.bash
. Предлагаю в проекте создать файлик .bashrc
с таким содержимым:
source /opt/ros/$ROS_DISTRO/setup.bash
source $PROJECT_DIR/devel/setup.bash
export ROS_IP=$(hostname -i)
export PS1="\[\e]0;\u@\h: \w\a\]${debian_chroot:+($debian_chroot)}\[\033[0;33m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ "
Выставление последних двух переменных не обязательно, если при создании контейнера будет использоваться подсеть хоста (--net host
). PS1
нужна лишь для украшения нашего терминала, подчистую украдена с убунты, за исключением цвета.
Его требуется скопировать в домашнюю директорию внутри образа, отсюда появляется такая строчка
COPY .bashrc /root/
Цыганские фокусы
А теперь об организации проекта с докером, которую я вывел для себя как оптимальную путем экспериментов. Темплейт проекта организовать стоит как-то так
catkin_ws/
├── .catkin_workspace
└── src
├── CMakeLists.txt
└── ros-docker-template
├── CMakeLists.txt
├── docker
│ ├── .bashrc
│ └── Dockerfile
├── launch
│ └── default.launch
├── package.xml
├── scripts
│ ├── attach.sh
│ ├── build_docker.sh
│ ├── run_prog.sh
│ └── start.sh
└── src
└── node.py
Я хотел чтобы и хост система и докер видели это как ROS пакет. Запуск контейнера предполагается с помощью скрипта start.sh
, а build_docker.sh
будет собирать образ проекта соответственно, и все это через rosrun
на хост системе. Нужно всего-то смонтировать catkin_ws/src/ros-docker-template
в контейнер без привязки к абсолютным путям, поэтому будем использовать пути относительно скриптов, он и будет создавать контейнер и запускать его, при условии если контейнер не существует и не запущен. Штош, задача звучит уже так, что не хочется слышать, но в итоге вдоволь намучавшись она была решена.
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" > /dev/null && pwd )"
HOST_IP=($(hostname -I))
img_name=$(dirname $(readlink -m $DIR))
img_name=${img_name##*/}-img
docker build \
--build-arg hostname=$(hostname) \
--build-arg ros_master_uri="http://${HOST_IP[0]}:11311" \
--build-arg host_ip=${HOST_IP[0]} \
--tag $img_name \
$DIR/../docker
Основная фишка в том, что он называет контейнер по имени директории с проектом + '-img', эта фишка абузится и во всех остальных скриптах.
Собственно через аргумент --build-arg
и передаются переменные в Dockerfile
, если их не передать, докер выдаст ворнинг, но все равно соберет образ. Дальше пойдет совсем жеcть, просьба убрать людей, беременных детей и женщин с тонкой душевной организацией от экрана.start.sh
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" > /dev/null && pwd )"
container_name=$(dirname $(readlink -m $DIR))
container_name=${container_name##*/}
img_name=$container_name-img
if [ ! "$(docker ps -a | grep $container_name)" ]; then
docker run -di \
--name $container_name \
--add-host $(hostname):$(hostname -i) \
--mount type=bind,src=$DIR/../..,dst=/root/catkin_ws/src \
--hostname ros-0 \
-P \
$img_name bash
fi
if ! [ "$( docker container inspect -f '{{.State.Status}}' $container_name )" == "running" ]; then
docker start $container_name
fi
docker exec $container_name bash -c "source /root/.bashrc; catkin_make"
Этот скрипт сразу поднимает/создает контейнер и монтирует директорию catkin_ws/src
сразу в образ, что позволит запускать внутри контейнера и все остальные пакеты в этом воркспейсе, ну не чудно ли? Более того он на опережение его собирает.
Для быстрого старта любого пакета из этого воркспейса была собрана из велосипедов и костылей целая консольная утилита run_prog.sh
.
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" > /dev/null && pwd )"
container_name=$(dirname $(readlink -m $DIR))
container_name=${container_name##*/}
if [ $1 = "--help" ] || [ $1 = "-h" ];
then
echo "usage
no args: rosrun $container_name node.py
1 arg : rosrun $container_name <ARG>
2 arg : rosrun <arg1> <arg2>
"
fi
if [[ $1 == "" ]] && [[ $2 == "" ]]
then
PKG=$container_name
EXEC=node.py
echo 1
elif [[ $2 == "" ]] && [[ $1 != "" ]]
then
PKG=$container_name
EXEC=$1
echo 2
else
PKG=$1
EXEC=$2
echo 3
fi
echo "launching pkg:$PKG exec:$EXEC"
docker exec $container_name bash -c "source /root/.bashrc; catkin_make; rosrun $PKG $EXEC"
Все про нее написано в общем-то в help, оно при запуске перекомпилирует весь воркспейс, ну прямо чудеса bash скриптов.
Итого
Получилась вот такая репа
# Cборка образа
rosrun ros-docker-template build_docker.sh
# Запуск/создание контейнера
rosrun ros-docker-template start.sh
# Запуск любого пакета внутри контейнера
rosrun ros-docker-template run_prog.sh <pkg> <exec>