(!) Please ask about problems and questions regarding this tutorial on answers.ros.org. Don't forget to include in your question the link to this page, the versions of your OS & ROS, and also add appropriate tags.

Dicas de Roslaunch para projetos grandes

Description: Traz dicas para escrever arquivos roslaunch para grandes projetos. O foco é em como estruturar os arquivos para que eles possam ser reutilizados o máximo possível em situações diversas. Usaremos o pacote 2dnav_pr2 como caso de estudo.

Tutorial Level: INTERMEDIATE

Next Tutorial: Rodando ROS através de várias máquinas

Introdução

Grandes aplicativos em um robô geralmente envolvem vários nós interconectados, cada um com muitos parâmetros. Navegação 2D é um bom exemplo. O aplicativo 2dnav_pr2 consiste não só do nó da move_base, mas também de localização, filtragem do plano do chão, controlador de base e servidor de mapas. Coletivamente, também existem algumas centenas de parâmetros do ROS que afetam o comportamento desses nós. Finalmente, existem restrições, como o fato de que a filtragem do plano do chão deve ser executada na mesma máquina que o laser de inclinação por motivo de eficiência.

Um arquivo roslaunch nos permite determinar tudo isso. Dado um robô em execução, o lançamento do arquivo 2dnav_pr2.launch do pacote 2dnav_pr2 inicializará tudo o que é necessário para a navegação do robô. Neste tutorial, examinaremos esse arquivo e vários dos recursos usados.

Também é desejável que arquivos roslaunch sejam o mais reutilizáveis ​​possível. Por exemplo, usar o mesmo arquivo sem alterações para dois robôs fisicamente idênticos. Até uma mudança como passar do robô físico para o simulador pode ser feita com apenas algumas alterações. Veremos como o arquivo é estruturado para tornar isso possível.

Organização por cima

Aqui está o arquivo de launch mais ao topo (em "rospack find 2dnav_pr2/move_base/2dnav_pr2.launch").

<launch>
  <group name="wg">
    <include file="$(find pr2_alpha)/$(env ROBOT).machine" />
    <include file="$(find 2dnav_pr2)/config/new_amcl_node.xml" />
    <include file="$(find 2dnav_pr2)/config/base_odom_teleop.xml" />
    <include file="$(find 2dnav_pr2)/config/lasers_and_filters.xml" />
    <include file="$(find 2dnav_pr2)/config/map_server.xml" />
    <include file="$(find 2dnav_pr2)/config/ground_plane.xml" />

    <!-- The navigation stack and associated parameters -->
    <include file="$(find 2dnav_pr2)/move_base/move_base.xml" />
  </group>
</launch>

Este arquivo inclui (include) vários outros arquivos. Cada um desses arquivos incluídos contém nós e parâmetros (e possivelmente mais arquivos incluídos) pertencentes a uma parte do sistema, como localização, processamento do sensor e planejamento de caminho.

Dica: Arquivos de launch mais ao topo devem ser curtos e consistir em inclusões de outros arquivos correspondentes aos subcomponentes do aplicativo, assim como alguns parâmetros do ROS.

Isso facilita a troca de uma parte do sistema, como veremos mais adiante.

Para executar este arquivo no robô PR2, é necessário rodar um roscore, e então rodar um arquivo de inicialização específico do robô, como pre.launch no pacote pr2_alpha, e iniciar o 2dnav_pr2.launch. Poderíamos ter incluído um arquivo de inicialização do robô aqui, em vez de exigir que ele seja iniciado separadamente. Isso traria os seguintes prós e contras:

  • Pró: Teríamos que fazer um passo a menos de "abrir novo terminal, roslaunch".
  • Contra: O lançamento do arquivo de inicialização do robô inicia uma fase de calibração com duração de aproximadamente um minuto. Se o arquivo de inicialização 2dnav_pr2 incluísse o arquivo de inicialização do robô, toda vez que matássemos o roslaunch (com Ctrl-C) e o trouxéssemos de volta, a calibração ocorreria novamente.

  • Contra: Alguns dos nós de navegação 2D exigem que a calibração já tenha terminado antes de começar. O roslaunch não fornece nenhum controle sobre a ordem ou o tempo de inicialização do nó. A solução ideal seria fazer com que os nós funcionassem bem enquanto aguardam a conclusão da calibração, mas já que não funcionam assim, colocar as coisas em dois arquivos de inicialização nos permite iniciar o robô, aguardar a conclusão da calibração, e só então iniciar o 2dnav.

Portanto, não há uma resposta universal para se deve-se ou não separar os arquivos launch em vários. Aqui foi decidido usar dois arquivos de inicialização diferentes.

Dica: esteja ciente das vantagens e desvantagens ao decidir quantos arquivos launch de topo seu aplicativo exige.

Etiquetas de máquina e variáveis de ambiente

Gostaríamos de controlar quais nós são executados em quais máquinas, para balanceamento de carga e gerenciamento de largura de banda. Por exemplo, gostaríamos que o nó amcl fosse executado na mesma máquina que o laser base. Ao mesmo tempo, para reusabilidade, não queremos codificar os nomes das máquinas em arquivos roslaunch. Roslaunch lida com isso usando etiquetas de máquina.

O primeiro include é

<include file="$(find pr2_alpha)/$(env ROBOT).machine" />

A primeira coisa a observar sobre esse arquivo é o uso do argumento de substituição env para usar o valor da variável de ambiente ROBOT. Por exemplo, fazendo

export ROBOT=pre

antes do roslaunch faria com que o arquivo pre.machine fosse incluído.

Dica: use o argumento de substituição env para permitir que partes de um arquivo de inicialização dependam de variáveis ​​de ambiente.

A seguir, vejamos um exemplo de arquivo de máquina: pre.machine no pacote pr2_alpha.

<launch>
  <machine name="c1" address="pre1" ros-root="$(env ROS_ROOT)" ros-package-path="$(env ROS_PACKAGE_PATH)" default="true" />
  <machine name="c2" address="pre2" ros-root="$(env ROS_ROOT)" ros-package-path="$(env ROS_PACKAGE_PATH)" />
</launch>

Este arquivo configura um mapeamento entre nomes de máquinas lógicas, nesse caso "c1" e "c2", e nomes de host reais, como "pre2". Ele ainda permite controlar o usuário com o qual efetuar login (assumindo que você possui as credenciais ssh apropriadas).

Uma vez definido o mapeamento, ele pode ser usado ao iniciar nós. Por exemplo, o arquivo incluído config/new_amcl_node.xml do pacote 2dnav_pr2 contém a linha

<node pkg="amcl" type="amcl" name="amcl" machine="c1">

Isso faz com que o nó amcl seja executado na máquina com o nome lógico c1 (observando os outros arquivos de inicialização, você verá que a maior parte do processamento sensorial foi colocada nessa máquina).

Ao rodar em um novo robô, digamos um conhecido como prf, basta alterar a variável de ambiente ROBOT. O arquivo da máquina correspondente (prf.machine do pacote pr2_alpha) será carregado. Podemos até usar isso para rodar em um simulador, mudando ROBOT para sim. Observando o arquivo sim.machine do pacote pr2_alpha, vemos que ele apenas mapeia todos os nomes de máquinas lógicas para o localhost.

Dica: use as etiquetas da máquina para equilibrar a carga e controlar quais nós são executados na mesma máquina e considere fazer com que o nome do arquivo da máquina dependa de uma variável de ambiente para reutilização.

Parâmetros, namespaces, e arquivos yaml

Vejamos o arquivo incluído move_base.xml. Aqui está uma parte deste arquivo:

<node pkg="move_base" type="move_base" name="move_base" machine="c2">
  <remap from="odom" to="pr2_base_odometry/odom" />
  <param name="controller_frequency" value="10.0" />
  <param name="footprint_padding" value="0.015" />
  <param name="controller_patience" value="15.0" />
  <param name="clearing_radius" value="0.59" />
  <rosparam file="$(find 2dnav_pr2)/config/costmap_common_params.yaml" command="load" ns="global_costmap" />
  <rosparam file="$(find 2dnav_pr2)/config/costmap_common_params.yaml" command="load" ns="local_costmap" />
  <rosparam file="$(find 2dnav_pr2)/move_base/local_costmap_params.yaml" command="load" />
  <rosparam file="$(find 2dnav_pr2)/move_base/global_costmap_params.yaml" command="load" />
  <rosparam file="$(find 2dnav_pr2)/move_base/navfn_params.yaml" command="load" />
  <rosparam file="$(find 2dnav_pr2)/move_base/base_local_planner_params.yaml" command="load" />
</node>

Este fragmento inicia o nó move_base. O primeiro elemento incluído é um remapeamento (remapping). A move_base foi projetada para receber odometria no tópico "odom". No caso do robô PR2, a odometria é publicada no tópico pr2_base_odometry, portanto, é necessário remapear.

Dica: use o remapeamento de tópico quando um determinado tipo de informação for publicado em diferentes tópicos em diferentes situações.

Em seguida vemos vários <param>s. Observe que esses parâmetros estão dentro do elemento do nó (<node>) (já que estão antes do </node> no final), portanto serão parâmetros privados. Por exemplo, o primeiro define move_base/controller_frequency como 10.0.

Após os elementos <param>, existem alguns elementos <rosparam>. Eles lêem parâmetros no formato yaml, um formato legível por humanos que permite estruturas de dados complexas. Aqui está uma parte do arquivo costmap_common_params.yaml carregado pelo primeiro elemento <rosparam>:

raytrace_range: 3.0
footprint: [[-0.325, -0.325], [-0.325, 0.325], [0.325, 0.325], [0.46, 0.0], [0.325, -0.325]]
inflation_radius: 0.55

# BEGIN VOXEL STUFF
observation_sources: base_scan_marking base_scan tilt_scan ground_object_cloud

base_scan_marking: {sensor_frame: base_laser, topic: /base_scan_marking, data_type: PointCloud, expected_update_rate: 0.2,
  observation_persistence: 0.0, marking: true, clearing: false, min_obstacle_height: 0.08, max_obstacle_height: 2.0}

Vemos que o yaml permite coisas como vetores (para o parâmetro footprint). Também permite colocar alguns parâmetros em um namespace aninhado. Por exemplo, base_scan_marking/sensor_frame está definido como base_laser. Observe que esses namespaces são relativos ao próprio namespace do arquivo yaml, que foi declarado como global_costmap pelo atributo ns do elemento rosparam. Por sua vez, como esse rosparam foi incluído pelo elemento do nó, o nome completo do parâmetro é /move_base/global_costmap/base_scan_marking/sensor_frame.

A próxima linha em move_base.xml é:

<rosparam file="$(find 2dnav_pr2)/config/costmap_common_params.yaml" command="load" ns="local_costmap" />

Na verdade, isso inclui exatamente o mesmo arquivo yaml da linha anterior. Está apenas em um namespace diferente (o namespace local_costmap é para o controlador de trajetória, enquanto o namespace global_costmap afeta o planejador de navegação global). Assim é muito melhor que digitar todos os valores de novo.

A próxima linha é:

<rosparam file="$(find 2dnav_pr2)/move_base/local_costmap_params.yaml" command="load"/>

Diferentemente dos anteriores, esse elemento não possui um atributo ns. Portanto, o namespace do arquivo yaml é o namespace herdado /move_base. Mas dê uma olhada nas primeiras linhas do próprio arquivo yaml:

local_costmap:
  #Independent settings for the local costmap
  publish_voxel_map: true
  global_frame: odom_combined
  robot_base_frame: base_link

Portanto, vemos que os parâmetros estão no namespace /move_base/local_costmap.

Dica: os arquivos Yaml permitem parâmetros com tipos complexos, namespaces aninhados para parâmetros e reutilização dos mesmos valores de parâmetro em vários locais.

Reutilizando arquivos launch

A motivação para muitas das dicas acima é facilitar a reutilização de arquivos launch em diferentes situações. Já vimos um exemplo em que o uso do argumento de substituição env pode permitir modificar o comportamento sem alterar nenhum arquivo launch. Porém, existem algumas situações em que isso é inconveniente ou impossível. Vamos dar uma olhada no pacote pr2_2dnav_gazebo. Ele contém uma versão do aplicativo de navegação 2D, mas para uso no simulador Gazebo Para a navegação, a única coisa que muda é que o ambiente do Gazebo que usamos é baseado em um mapa estático diferente, portanto, o nó map_server deve ser carregado com um argumento diferente. Poderíamos ter usado outra substituição de env aqui. Mas isso exigiria que o usuário definisse um monte de variáveis ​​de ambiente apenas para poder executar novamente. Em vez disso, o gazebo 2dnav contém seu próprio arquivo de inicialização de nível superior chamado 2dnav-stack-amcl.launch, mostrado aqui (modificado levemente para maior clareza):

<launch>
  <include file="$(find pr2_alpha)/sim.machine" />
  <include file="$(find 2dnav_pr2)/config/new_amcl_node.xml" />
  <include file="$(find 2dnav_pr2)/config/base_odom_teleop.xml" />
  <include file="$(find 2dnav_pr2)/config/lasers_and_filters.xml" />
  <node name="map_server" pkg="map_server" type="map_server" args="$(find gazebo_worlds)/Media/materials/textures/map3.png 0.1" respawn="true" machine="c1" />
  <include file="$(find 2dnav_pr2)/config/ground_plane.xml" />
  <!-- The naviagtion stack and associated parameters -->
  <include file="$(find 2dnav_pr2)/move_base/move_base.xml" />
</launch>

A primeira diferença é que, como sabemos que estamos no simulador, usamos apenas o arquivo sim.machine em vez de usar um argumento de substituição. Segundo, a linha

<include file="$(find 2dnav_pr2)/config/map_server.xml" />

foi substituída por

<node name="map_server" pkg="map_server" type="map_server" args="$(find gazebo_worlds)/Media/materials/textures/map3.png 0.1" respawn="true" machine="c1" />

O arquivo incluído no primeiro caso apenas continha uma declaração de nó como no segundo caso, mas com um arquivo de mapa diferente.

Dica: para modificar um aspecto de "topo" de um aplicativo, copie o arquivo launch de topo e altere as partes necessárias.

Substituições de parâmetro

A técnica acima às vezes se torna inconveniente. Suponha que desejamos usar 2dnav_pr2, mas apenas alteramos o parâmetro do local costmap para 0,5. Poderíamos simplesmente alterar localmente o local_costmap_params.yaml. Essa é a forma mais simples para modificações temporárias, mas significa que não podemos salvar o arquivo modificado. Em vez disso, poderíamos fazer uma cópia do local_costmap_params.yaml e modificá-lo. Teríamos então que mudar o move_base.xml para incluir o arquivo yaml modificado. E então teríamos que mudar 2dnav_pr2.launch para incluir o move_base.xml modificado. Isso pode levar muito tempo e, se você estiver usando o controle de versão, não veremos mais alterações nos arquivos originais. Uma alternativa é reestruturar os arquivos de ativação para que o parâmetro 'move_base/local_costmap/resolution seja definido no arquivo de topo 2dnav_pr2.launch e fazer uma versão modificada apenas desse arquivo. Essa é uma boa opção se soubermos com antecedência quais parâmetros provavelmente serão alterados.

Outra opção é usar o comportamento de substituição do roslaunch: os parâmetros são definidos em ordem (depois que as inclusões são processadas). Assim, poderíamos criar outro arquivo de nível superior que substitui a resolução original:

<launch>
<include file="$(find 2dnav_pr2)/move_base/2dnav_pr2.launch" />
<param name="move_base/local_costmap/resolution" value="0.5"/>
</launch>

A principal desvantagem é que esse método pode tornar as coisas mais difíceis de entender: conhecer o valor real que o roslaunch define para um parâmetro requer o rastreamento dos arquivos incluídos do roslaunch. Mas evita ter que fazer cópias de vários arquivos.

Dica: para modificar um parâmetro profundamente aninhado em uma árvore de arquivos launch que você não pode alterar, use a semântica de substituição de parâmetro do roslaunch.

Argumentos do roslaunch

Desde a versão CTurtle, o roslaunch possui um recurso de substituição de argumentos junto com etiquetas que permitem condicionar partes do arquivo launch ao valor dos argumentos. Essa pode ser uma maneira mais geral e clara de estruturar coisas do que o mecanismo de substituição de parâmetro ou as técnicas de reutilização de arquivos de inicialização acima, com o custo de ter que modificar o arquivo de inicialização original para especificar quais são os argumentos alteráveis. Veja a documentação do roslaunch XML.

Dica: se você pode modificar o arquivo launch original, geralmente é preferível usar argumentos roslaunch em vez de substituir parâmetros ou copiar arquivos roslaunch.

Wiki: pt_BR/ROS/Tutorials/Roslaunch tips for larger projects (last edited 2020-04-19 01:09:25 by chapulina)