ROSの基本操作
第142回 ロボット工学セミナー

ロボットの作り方
~移動ロボットの制御とROSによる動作計画実習~


ROSの基本操作

基本的なROS上で動くプログラムの書き方とコンパイル方法について説明します。

下記の実習を行うためには、リモートPCのROS_MASTER_URIROS_HOSTNAMElocalhostに戻す必要があります。
エディタで~/.bashrcファイルを開きます。下記のコマンドをターミナルに入力してください。

【リモートPCで実施】

$ cd
$ gedit ~/.bashrc

下図の赤枠で囲まれた部分が変更箇所となります。

修正後、画面右上のSaveにて保存してエディタを閉じます。

次に、変更内容をシステムに反映させます。

【リモートPCで実施】

$ source ~/.bashrc

基本用語

パッケージ
ノードや設定ファイル、コンパイル方法などをまとめたもの
ノード
ROSの枠組みを利用して動作する実行ファイル
メッセージ
ノード間でやりとりするデータ
トピック
ノード間でメッセージをやりとりする際にメッセージを格納する場所

ノード、メッセージ、トピックの関係は以下の図のように表せます。

ROS topics

基本的に、ソフトウェアとしてのROSはノード間のデータのやりとりをサポートするための枠組みです。
加えて、使い回しがきく汎用的なノードを世界中のROS利用者で共有するコミュニティも大きな意味でのROSの一部となっています。


ワークスペース

ROSでは、プログラムをビルドする際にcatkinというソフトウェアパッケージを使用しています。
また、catkin は、 cmake というソフトウェアを使っており、ROS用のプログラムのパッケージ毎にcmakeの設定ファイルを作成することで、ビルドに必要な設定を行います。

今回はROSをインストールするためにinstall_ros_melodic.shを利用したのでワークスペースが自動的に作成されています。

install_ros_melodic.shを利用せず新しいワークスペースを作る場合、以下の手順を行う必要があります(今回は以下のコマンド入力は不要です)

【リモートPCで実施】

$ mkdir -p ~/catkin_ws/src
$ cd ~/catkin_ws/src
$ catkin_init_workspace
Creating symlink "/home/[ユーザ名]/catkin/src/CMakeLists.txt" pointing to "/opt/ros/melodic/share/catkin/cmake/toplevel.cmake"
$ ls
CMakeLists.txt
$ cd ..
$ ls
src

catkin_wsディレクトリ内にある、builddevelは、catkinシステムがプログラムをビルドする際に使用するものなので、 ユーザが触る必要はありません。
catkin_ws/srcディレクトリは、ROSパッケージのソースコードを置く場所で、中にあるCMakeLists.txt は、 ワークスペース全体をビルドするためのルールが書かれているファイルです。

このディレクトリに、本作業用のパッケージをダウンロードします。

以下のコマンドを実行してください。

【リモートPCで実施】

$ cd ~/catkin_ws/src
$ git clone https://github.com/kogakuin-mobility-system-lab/rsj_seminar_no142_ros_basics.git
$ ls
CMakeLists.txt rsj_seminar_no142_ros_basics
$

gitは、ソースコードなどの変更履歴を記録して管理する分散型バージョン管理システムと呼ばれるものです。
今回のセミナーでは詳細は触れませんが、研究開発を行う上では非常に有用なシステムですので、利用をお勧めします。
公式の解説書、Pro Gitなどを参考にして下さい。

GitHubは、ソースコードなどを格納、保管、管理するためのWebリポジトリサービスです。
オープンソースソフトウェアの開発、共同作業および配布のためによく利用されており、ROSではソースコードの保存と配布する場所としてもっとも人気なサービスとなっています。
バイナリパッケージとして配布されているROSパッケージ以外の利用をする場合、GitHubを利用します。
URLが分かれば上の手順だけで簡単にROSのパッケージが自分のワークスペースにインポートし利用することができます。

では、次にパッケージのディレクトリ構成を確認します。
ダウンロードしているパッケージがバージョンアップされている場合などには、下記の実行例とファイル名が異なったり、ファイルが追加・削除されている場合があります。

以下のコマンドを実行してください。

【リモートPCで実施】

$ cd ~/catkin_ws/src/rsj_seminar_no142_ros_basics/
$ ls
CMakeLists.txt LICENSE launch msg package.xml src
$ ls launch/
test.launch
$ ls msg/
Text.msg
$ ls src/
Publish.cpp Show.cpp
$

CMakeLists.txtpackage.xmlには、使っているライブラリの一覧や生成する実行ファイルとC++のソースコードの対応など、このパッケージをビルドするために必要な情報が書かれています。
launchディレクトリには、複数のノードでできたシステムの定義が、msgディレクトリには、このパッケージ独自のデータ形式の定義が、srcディレクトリには、このパッケージに含まれるプログラム(ノード)のソースコードが含まれています。

catkin_makeコマンドで、ダウンロードしたrsj_seminar_no142_ros_basicsパッケージを含む、ワークスペース全体をビルドします。catkin_makeは、ワークスペースの最上位ディレクトリ(~/catkin_ws/)で行います。


ROSノードの理解とビルド・実行

先ほど作成したワークスペースを利用します。
ターミナルを開き、パッケージが正しく存在していることを確認します。

【リモートPCで実施】

$ cd ~/catkin_ws/src/
$ ls
CMakeLists.txt rsj_seminar_no142_ros_basics
$ cd ..
$

ソースファイルの編集にはお好みのテキストエディタが利用可能です。
Linuxでのプログラム開発がはじめての方には、Ubuntuにデフォルトでインストールされているgeditがおすすめです。プログラミング作業が多い方にはVisual Studio Codeがおすすめです。

お好みのテキストエディタで ~/catkin_ws/src/rsj_seminar_no142_ros_basics/src/Publish.cpp を開きます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <ros/ros.h>
#include <rsj_seminar_no142_ros_basics/Text.h>

#include <string>

int main(int argc, char **argv) {
  ros::init(argc, argv, "Publish");
  ros::NodeHandle node;

  std::string message;
  std::string date;
  ros::param::param<std::string>("~message", message, "test seminar 2022");
  ros::param::param<std::string>("~date", date, "September 10");

  ros::Publisher pub = node.advertise<rsj_seminar_no142_ros_basics::Text>("Publish", 1);

  ros::Rate rate(1);

  while (ros::ok()) {
    ros::spinOnce();

    ROS_INFO("Publishing message '%s %s'", message.c_str(), date.c_str());
    rsj_seminar_no142_ros_basics::Text sample;
    sample.message = message;
    sample.date = date;
    pub.publish(sample);

    rate.sleep();
  }

  return 0;
}

送信ノードの作成(基本的なコードを読み解く)

このコードが実行されたときの流れを確認しましょう。
まず、先頭部分では、必要なヘッダファイルをインクルードしています。

#include <ros/ros.h>

続いて、本ノードが利用するメッセージのヘッダファイルをインクルードしています。

#include <rsj_seminar_no142_ros_basics/Text.h>

std::stringが利用されるので、ヘッダファイルをインクルードします。

#include <string>

続いて、C++のmain関数が定義されています。
本ノードは非常に簡単な構成としているため、すべての機能をmain関数に入れています。
複雑な機能や色々なデータを持つノードには、クラスとしての実装することをおすすめします。

int main(int argc, char **argv) {
  ros::init(argc, argv, "Publish");
  ros::NodeHandle node;

  std::string message;
  std::string date;
  ros::param::param<std::string>("~message", message, "test seminar 2022");
  ros::param::param<std::string>("~date", date, "September 10");

  ros::Publisher pub = node.advertise<rsj_seminar_no142_ros_basics::Text>("Publish", 1);

  ros::Rate rate(1);

  while (ros::ok()) {
    ros::spinOnce();

    ROS_INFO("Publishing message '%s %s'", message.c_str(), date.c_str());
    rsj_seminar_no142_ros_basics::Text sample;
    sample.message = message;
    sample.date = date;
    pub.publish(sample);

    rate.sleep();
  }

  return 0;
}

main関数は、まずノードのセットアップを行います。

ros::initはROSのインフラストラクチャの初期設定を行いノードを初期化します。
1、2番目の引数には、main関数の引数をそのまま渡し、3番目の引数には、このノードの名前(この例では”Publish”)を与えています。

その次にあるros::NodeHandle nodeは、ノードを操るための変数を初期化します。

次の4行はパラメータの初期化です。ROSではコマンドライン引数によるパラメータ指定より、ROSのパラメータ設定機能の利用が一般的です。
ROSのパラメータ設定機能の利用により、roslaunch(複数のノードを起動するためのツール)やGUIツールからもパラメータ設定を容易に行えます。

パラメータの初期化が終わったら、データ送信のためのパブリッシャーを初期化します。
この変数の作成によりトピックが作成され、このノードからデータの送信が可能になります。
以下の引数を与えています。

"Publish"
トピック名:データをこのトピックに送信する
1
メッセージのバッファリング量を指定 (大きくすると、処理が一時的に重くなったときなどに受け取り側の読み飛ばしを減らせる)

advertise関数についている<rsj_seminar_no142_ros_basics::Text>の部分はメッセージの型を指定しています。
(この指定方法は、C++のテンプレートという機能を利用していますが、ここでは「advertiseのときはメッセージの型指定を<>の中に書く」とだけ覚えておけば問題ありません)

セットアップの最後として、ros::Rate rate(1)で周期実行のためのクラスを初期化しています。
初期化時の引数で実行周波数(この例では1 Hz)を指定します。

while(ros::ok())で、メインの無限ループを回します(すなわちこのノードのメーンプロセッシングループです)。
ros::ok()whileの条件にすることで、ノードの終了指示が与えられたとき(Ctrl+c が押された場合も含む)には、ループを抜けて終了処理などが行えるようになっています。

ループ中では、まず、ros::spinOnce()を呼び出して、ROSのメッセージを受け取る といった処理を行います。
spinOnce()は、その時点で届いているメッセージの受け取り処理を済ませた後、すぐに処理を返します。 spinOnce()はこのシンプルなプログラムでは意味をなしません。というのは、トピック受信時に処理を行うコールバックを、ここでは定義していないためです。仮にこのアプリケーションにトピック受信処理機能を加えた場合、spinOnce()を行わなければコールバックが呼ばれないため、ここで加えておいた方がよいでしょう。
rate.sleep()は、先ほど初期化した実行周波数を維持するようにsleepします。

ros::spinOnce()rate.sleep()の間に本ノードの処理を入れています。

spinOnce()後は、ROSでログ情報を画面などに出力する際に用いるROS_INFO()関数を呼び出してメッセージを表示しています。
他にも、ROS_DEBUG()、ROS_WARN()、ROS_ERROR()、ROS_FATAL()などのデバッグログ関数が用意されています。

その後、データを送信します。
まずは送信するデータ型(rsj_seminar_no142_ros_basics::Text)を初期化し、値を設定します。
さきほどセットアップで作成したパラメータの値を利用します。
こうすることで、送信データの内容を、実行するときに自由に変更できます。

そして、pub.publish(sample)によってデータを送信します。
この行でデータはバッファに入れられ、別スレッドが自動的にサブスクライバに送信します(ROSのデフォルトでは非同期送信となります)。

main ループが終了すると作成した変数は自動的にクリーンアップを実行し、ノードのシャットダウンを行います。


ビルド&実行

ROS パッケージをビルドするためには、catkin_makeコマンドを用います。

下記コマンドをターミナルで実行してみましょう。

【リモートPCで実施】

$ cd ~/catkin_ws/
$ catkin_make

ROSシステムの実行の際、ROSを通してノード同士がデータをやりとりするために用いる「roscore」を起動しておく必要があります。
2つ目のターミナルを開き、それぞれで以下を実行して下さい。

1つ目のターミナルで下記を実行します。

【リモートPCで実施】

$ roscore

ROSでワークスペースを利用するとき、ターミナルでそのワークスペースに環境変数などを設定することが必要です。
このためにワークスペースの最上位のディレクトリでsource devel/setup.bashを実行します。
このコマンドはワークスペースの環境編情報などを利用中のターミナルに読み込みます。
しかし、 ターミナルごとに環境変数はリセットされますので、新しいターミナルでワークスペースを利用しはじめるときには、まずsource devel/setup.bashを実行しなければなりません。
一つのターミナルで一回だけ実行すれば十分です。そのターミナルを閉じるまで有効となります。
この作業を省略するため、~/.bashrcsource devel/setup.bashを追加することをおすすめします。(install_ros_melodic.shを利用した場合は.bashrcsource devel/setup.bashが既に追加されています。)

2つ目のターミナルで下記を実行します。

【リモートPCで実施】

$ cd ~/catkin_ws/
$ rosrun rsj_seminar_no142_ros_basics publish
[INFO] [1640146129.900580884]: Publishing message 'test seminar 2022 September 10'

上記のようなログが表示されれば成功です。

ソースコードにパラメータを利用したので、コマンドラインからパラメータ設定を試してみましょう。

ノードを実行した2つ目のターミナル(注意:roscoreのターミナルではなくて)に Ctrl+c を入力してノードを終了します。

そして以下を実行してください。

【リモートPCで実施】

$ rosrun rsj_seminar_no142_ros_basics publish _message:=test-2 _date:=today
[INFO] [1640146529.644756809]: Publishing message 'test-2 today'

上記のようなログが表示されれば成功です。

実行後は両方のターミナルで Ctrl+c でノードとroscoreを終了します。


受信ノードの作成

前節で作成したノードはメッセージを送信するノードでした。
次にメッセージを受信するノードを作成してみましょう。

以下のソースはrsj_seminar_no142_ros_basics/src/Show.cppファイルにあります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <ros/ros.h>
#include <rsj_seminar_no142_ros_basics/Text.h>

#include <iostream>

void callback(const rsj_seminar_no142_ros_basics::Text::ConstPtr &msg) {
  std::cout << msg->message << " " << msg->date << '\n';
}

int main(int argc, char **argv) {
  ros::init(argc, argv, "Show");
  ros::NodeHandle node;

  ros::Subscriber sub = node.subscribe("Publish", 10, callback);

  ros::spin();

  return 0;
}

本ノードはPublishというトピックから取得したデータをターミナルに表示します。
Publish.cppからの差は以下のようです。

まずはcallback関数です。
この関数はトピックのデータ型に合っているポインタを引数として受け取ります。
トピックからデータを受け取ったら、callback関数は呼ばれます。
そのデータをstd::coutに出力し終了します。

const rsj_seminar_no142_ros_basics::Text::ConstPtr は、const型(内容を書き換えられない)、rsj_seminar_no142_ros_basicsパッケージに含まれる、Text型のメッセージの、const型ポインタを表しています。
&msg&は、参照型(内容を書き換えられるように変数を渡すことができる)という意味ですが、(const型なので)ここでは特に気にする必要はありません。
msgはクラスへのポインタなので「->」を用い、以降はクラスのメンバ変数へのアクセスなので「.」を用いてアクセスしています。

main関数内にパラメータの初期化はなくなりました。本ノードはパラメータを利用しません。

ros::Publisherの初期化もなくなり、代わりにros::Subscriberを初期化します。
この行はトピックへのアクセスを初期化し、データ取得時の対応を設定します。
引数は以下です。

"Publish"
トピック名
10
バッファーサイズ
callback
メッセージを受け取ったときに呼び出す関数を指定 (callback関数)

最後に、main ループでの処理は不要となります。
本ノードはトピック受信時以外何もしないので、無限ループになるros::spin()を呼びます。
ros::spin()は、Publish.cppにおけるwhile(...)およびros::spinOnce()と類似処理を実施し、ノードがシャットダウンされるまでに戻りません。


実行

作成したノードを実行してみましょう。

1つ目のターミナルで以下を実行します。

【リモートPCで実施】

$ roscore

そして2つ目のターミナルで以下を実行します。

$ cd ~/catkin_ws/
$ rosrun rsj_seminar_no142_ros_basics publish
[INFO] [1494840089.900580884]: Publishing message 'test seminar 2022 September 10']

最後に、3番目のターミナルを開いて、下記を実行します。

$ cd ~/catkin_ws/
$ rosrun rsj_seminar_no142_ros_basics show

「test seminar 2022 September 10」と表示されれば成功です。

以上の手順で、ROSパッケージに含まれるノードのソースコードを編集し、ビルドして、実行できるようになりました。

正常動作を確認後、各ターミナルにてCtrl+c にて、roscoreおよび、publishノード、showノードを停止してください。


システムとして実行する

roslaunchを利用して、複数のノードでできたシステムを開始、終了することができます。

手動でノードを1ノードずつ起動することは手間がかかりますし、ミスを誘発する可能性も高まります。
そのためにROSにroslaunchというツールがあります。
roslaunchを利用すると、システム全体をまとめて起動し、状況をモニターし、そしてまとめて終了することができます。

launchファイルを読み解く

launchファイルは、ノードやパラメータの組み合わせを定義するためのファイルです。
フォーマットはXMLです。
システムに含まれるノードとそのノードの起動方法を一つずつ定義します。

rsj_seminar_no142_ros_basicsパッケージに以下のファイルがlaunch/test.launchとして存在します。
このファイルはさきほど手動で起動したシステムを定義します。

<launch>
    <node name="publisher" pkg="rsj_seminar_no142_ros_basics" type="publish">
        <param name="message" value="Test seminar"/>
        <param name="date" value="September 10"/>
    </node>

    <node name="show" pkg="rsj_seminar_no142_ros_basics" type="show" output="screen"/>
</launch>
          

nodeタグは2つあります。
属性は下記の通りです。

name
ノードインスタンスの名
pkg
ノードを定義するパッケージ名
type
ノードの実行ファイル名
output
stdoutに対する出力先:定義しない場合、stdoutROS_INFOstd::coutへの出力等)はターミナルで表示されず、~/.ros/log/に保存されるログファイルだけに出力される。

1番目の<node>publishノードの定義です。
本要素の中でパラメータの設定も行っています。
なお、パラメータの設定を行わない場合はノードのソースに定義したデフォルト値が利用されるので、記述は必須ではありません。

2番目の<node>showノードの定義です。
パラメータはありませんが、出力されることをターミナルで表示するようにします。


roslaunchでシステムを起動

roscoreや起動中のノードをすべて Ctrl+c で停止します。

その後、いずれか1つのターミナルで以下を実行します。

【リモートPCで実施】

$ cd ~/catkin_ws
$ roslaunch rsj_seminar_no142_ros_basics test.launch
... logging to /home/user_name/.ros/log/40887b56-395c-11e7-b8
68-d8cb8ae35bff/roslaunch-user_name-11087.log
Checking log directory for disk usage. This may take awhile.
Press Ctrl-C to interrupt
Done checking log file disk usage. Usage is <1GB.

started roslaunch server http://localhost:11311/

SUMMARY
========

PARAMETERS
 * /publisher/date: September 10
 * /publisher/message: Test seminar
 * /rosdistro: melodic
 * /rosversion: 1.14.9

NODES
  /
    publisher (rsj_seminar_no142_ros_basics/publish)
    show (rsj_seminar_no142_ros_basics/show)

ROS_MASTER_URI=http://localhost:11311

process[rosout-1]: started with pid [18496]
started core service [/rosout]
process[publisher-2]: started with pid [18499]
process[show-3]: started with pid [18501]
Test seminar September 10
Test seminar September 10
          

「Test seminar September 10」が繰り返して表示されたら成功です。

Ctrl+c でノードを停止します。

Test seminar September 10
Test seminar September 10
Test seminar September 10
  (Ctrl+c)
^C[show-2] killing on exit
[publisher-1] killing on exit
shutting down processing monitor...
... shutting down processing monitor complete
done
$

これでシステムの起動、停止が簡単にできるようになりました。

roslaunchを利用する場合は、別のターミナルでのroscoreの実行は不要です。 roslaunchが自動的にroscoreの起動や停止を行うためです。


メインページに戻る