Сага о том, как я клеил 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>


ссылка на оригинал статьи https://habr.com/ru/post/704674/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *