python

Сага о том, как я клеил ROS и Docker

  • воскресенье, 11 декабря 2022 г. в 00:47:01
https://habr.com/ru/post/704674/
  • Python
  • Разработка под Linux
  • Робототехника


Это в общем-то первая статья на хабре, пробная и экспериментальная. Цель статьи изложить процесс создания темплейта под разработку для ROS (Robot Operating System) внутри контейнера и сделать это в шутливой манере.


Началось с того, что мне потребовалось установить контейнер с cuda. Все готовые контейнеры с ROS и cuda на докерхабе имели либо проблемы со стартом, либо имели битый пакетный менеджер. Я бы хотел сделать его несколько универсальным, чтобы адаптировать к любым своим проеrтам.

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>