eSEATでROSノードを作る(Pub/Sub)
Simple Wiki Based Contents Management System
ソフトウェア関連 >> RTコンポーネント関連 >> eSEAT_v2.5 >> SEATMLファイルの書き方 >> ROSノードの作成(eSEAT) >> eSEATでROSノードを作る(Pub/Sub)

eSEATでROSノードを作る(Pub/Sub)

ここでは、eSEATでROSノードを使うSEATMLファイルの作成について解説していきます。ROSのチュートリアルでは、チュートリアル用のパッケージ作成から説明されていますが、ここでは、catkin等のビルドコマンドは使いません。
そのため、eSEATに含まれるコマンドとROSのデフォルトインストールの状態のみを使います。SEATMLでは、ROSノードもROS2ノードもほぼ同じ記述で利用することができますが、必要パッケージや操作の違いが若干ありますので、まず、ROSノードでの説明を行った後に、ROS2での改変部分のみを説明します。

作業場所の作成

ROSのチュートリアルでは、catkin_wsという作業場所を作って独自パッケージの作成等を説明されていますが、あえて異なる作業場所でSEATMLを作成してみたいと思います。
最初に、
 # mkdir ~/ros_ws
 # cd ~/ros_ws
 # source /opt/ros/kinetic/setup.bash

実装するプログラム

ここでは、ROSチュートリアルとの対比がわかるように、シンプルなメッセージ(文字列)を送受信する talkerとlistenerを作成していきます。

ROSのPublisherを作成する

まずは、RTCの作成のときと同様にひな形をtalker.seatmlとしてコピーします。
 # cd ~/ros_ws
 # source /usr/local/eSEAT/setup.bash
 # gen_seatml talker
次に、適当なエディタでtalker.seatmlを開き、動作周波数を設定します。
 <?xml version="1.0" encoding="UTF-8" ?>
 <seatml>
  <general name="talker" anonymous="1" rate="10">
  </general>

  <state name="main_state">
 </state>
 </seatml>
この例では、ノード名は talkerで動作周波数は 10Hzです。anonymous属性は、rospy.init_node関数の引数になります。(ROS2ノードの場合には、anonymous属性は無視されます。)
次に、talkerのインターフェースであるPublisherを定義します。ROSチュートリアルでは、talkerのPublisherは、'chatter'というトピック名で std_msgs/String型のメッセージでメッセージ・キューの量が10になっていました。
eSEATでは、トピック名がadaptorの識別子になっているのですが、トピック名は、必ず'/'から始めるようにしてください。(これは、eSEATの実装仕様だと思ってください。)
eSEATにおけるROS Pub/Subのadaptorの定義は下記の通りです。
属性説明
type'ros_pub'(パブリッシャー)または 'ros_sub'(サブスクライバ)のどちらかの値
nameトピック名 (識別子として使用します)
datatypeパケージ名+'/'+メッセージ型
size'ros_pub'の場合に queue_sizeに設定する値 (通常 1)
callback'ros_sub'の場合に指定したメッセージを受け取った時に動作するコールバック関数。 省略時は、eSEAT_Core.onDataが呼ばれるようになています。
file'ros_pub'の場合に、インポートしたい外部Pythonプログラムファイル(file_exec関数で実行します)
したがって、talkerのインターフェース定義は、下のようになります
 <adaptor name="chatter" type="ros_pub" datatype="std_msgs/String" size="10" />
ここでsize属性を省略すると size="1" と同じになります。また、datatypeは、パッケージ名を必ずつけてください。eSEATの内部では、上記の場合には、
 import std_msgs.msg as std_msgs
が実行され、std_msgs.String としてメッセージ型が定義されます。
これで、talkerのノード定義とPublisherの生成、動作周波数(rospy.Rateの実行)まで終わりました。最後に、Publisherに周期的にメッセージを出力する部分を付け加えます。
eSEATでは、周期動作は RTCと同じように <onexec>要素で定義します。ROSチュートリアルのtalker.pyの
    while not rospy.is_shutdon():
        str = "hello world %s"%rospy.get_time()
        rospy.loginfo(str)
        pub.publish(str)
        r.sleep()
の部分です。SEATMLでは、下のようになります。
  <onexec>
     <script>
        str = "hello world %s" % seat.get_time()
        seat.loginfo(str)
        seat.sendto("chatter", str)
     </script>
  </onexec>
また、<script>要素の sendto属性を使うと
  <onexec>
     <script sendto="chatter">
        rtc_result = "hello world %s" % seat.get_time()
        seat.loginfo(rtc_result)
     </script>
  </onexec>
と書くこともできます。最終的には、
 <?xml version="1.0" encoding="UTF-8" ?>
 <seatml>
  <general name="talker" anonymous="1" rate="10">
   <adaptor name="chatter" type="ros_pub" datatype="std_msgs/String" size="10" />
  </general>

  <state name="main_state">
    <onexec>
      <script>
        str = "hello world %s" % seat.get_time()
        seat.loginfo(str)
        seat.sendto("chatter", str)
      </script>
    </onexec>
  </state>
 </seatml>
となります。つまり、eSEATでは、adaptorでインタフェースを定義して、コアロジックを書くというスタイルでSEATMLを作成していきす。
注意:
上記の例では、データを送信するためにseat.sendto('chatter', str)としています。通常は、Publisherで送信するデータは、対応するデータ型(上記の例では、std_msgs.String)にする必要があります。
この例では、単に文字列を送信していますが、eSEATが元々対話制御のために設計されていたために、文字列のみ特別扱いしているためです。
そのため、通常は、

s_data=seat.newData('chatter')

 で新規データを作成し、

s_data.data=str

とする必要があります。詳細は、添付のtalker.seatmlを参照して下さい。

ROSのSubscriberを作成する

次に、Subscriberのであるlistener.seatmlを実装していきます。まずは、Publisherの作成のときと同様にひな形をlistener.seatmlとしてコピーします。
 # cd ~/ros_ws
 # source /usr/local/eSEAT/setup.bash
 # gen_seatml listener
次に、適当なエディタでlistener.seatmlを開き、anonymous属性の設定を行います。Subscriberは、データの受信のタイミングで動作させますので、動作周波数は必要ありませんので、下のようになります。
 <?xml version="1.0" encoding="UTF-8" ?>
 <seatml>
  <general name="listener" anonymous="1">
  </general>

  <state name="main_state">
 </state>
 </seatml>
ROSチュートリアルのSubscriberの生成では、callback関数を定義したのちに、rospy.Subscriber関数でlistenerのインターフェースを生成していました。eSEATでも<general>要素に<script>要素を使って、関数定義を行えば、ほぼ同じコードになるのですが、ここでは <rule>要素で定義しますので、callback属性は使わずにインタフェースを定義すると下のようになります。
 <adaptor name="chatter" type="ros_sub" datatype="std_msgs/String" />
このようにSubscriberのadaptorでcallback関数を指定しない場合には、eSEATの内部では、seat.onDataにcallbackが割り当てられ
  rospy.Subscriber('chatter', std_msgs.String, seat.onData)
とほぼ同じ動作を行います。(厳密には少し異なるのですが)
最後に、データが到着したときの振る舞いを記述します。ここでは、<rule>要素を使い、source属性でadaptorを指定します。
    <rule source="chatter">
      <script>
        print(rospy.get_caller_id()+" I harad %s" % rtc_in_data)
      </script>
    </rule>
このコードは、ROSのSubscriberが トピック名'chatter'でデータを受信するとeSEATの内部では、onDataメソッドが呼び出され、<rule>要素でsource属性で'chatter'と指定された処理を実行します。<rule>要素の中は、<message>や<log>, <shell>等も使えるのですが、Pythonコードを実行させるために<script>要素で記述します。
<rule>要素の中の<script>要素の中では、受診したデータは rtc_in_dataという変数に格納さえれています。この時通常は、rtc_in_data.dataになるはずなのですが、現在のところ、eSEATはstd_msgs.Stringの場合のみ rtc_in_dataでアクセスするようになります。これは、RTCの時と同じような仕様なので、こんなものだと思ってください。ユーザからの要求によっては、仕様変更することもあります。
また、ここでは rospy.loginfoの代わりにprintで出力させています。loginfoを使う場合には、rospy.loginfoではなく、seat.loginfoを使ってください。
最終的に、listener.seatmlは下のようになります。
 <?xml version="1.0" encoding="UTF-8" ?>
 <seatml>
  <general name="listener" anonymous="1">
    <adaptor name="chatter" type="ros_sub" datatype="std_msgs/String" />
  </general>

  <state name="main_state">
    <rule source="chatter">
      <script>
        print(rospy.get_caller_id() + " I harad %s" % rtc_in_data)
      </script>
    </rule>
  </state>
 </seatml>
このSEATMLでは、<onexec>もありませんが、eSEAT内部では空のループが1Hzで回っています。ROSでいう rospy.spin() があるとほぼ同値です。

作成したtalkerとlistenerを動作させる

では、talker.seatmlとlistener.seatmlを動かして動作確認を行います。ターミナルを3つ開いてください。ターミナル1は、roscore, ターミナル2は talker, ターミナル3は listenerとします。
  • ターミナル1(roscore)
 # . /opt/ros/kinetic/setup.bash
 # roscore
  • ターミナル2(talker)
 # . /opt/ros/kinetic/setup.bash
 # cd ~/ros_ws
 # eSEAT_Node talker.seatml
  • ターミナル3(listener)
 # . /opt/ros/kinetic/setup.bash
 # cd ~/ros_ws
 # eSEAT_Node listener.seatml
これで、ターミナル2で出力されたメッセージが、ターミナル3で表示されればOKです。すべてのターミナルで Ctrl+C でプログラムを終了させてください。

ROS2で使うために

ROS2では、新たに通信ミドルウェア(RMW)の概念が導入され、DDS(Data Distribution Service)を使うようになりました。また、pythonでの実装もPython3に移行しており、rospyからrclpyモジュールに変更になっています。eSEATでは、ROSノードを作成する場合に、最初にrospyモジュールのインポートを試みて、失敗すれば rclpyモジュールをインポートし、ROS2ノードを生成するようになっています。
SEATML内でのadaptorの定義は、ROSもROS2も同じになっていますので、コアロジック以外を変更する必要はありません。
コアロジックにおいてもrospyモジュールの関数を使っていなければ、変更なしでROS2ノードで実行させれることができます。
上記の例では、listener.seatmlで rospy.get_caller_id() を使っていますので、この部分を削除(代替関数は見当たりませんでしたので未実装です)すればOKです。
したがって、listener.seatmlを下のように書き換えます。
 <?xml version="1.0" encoding="UTF-8" ?>
 <seatml>
  <general name="listener" anonymous="1">
    <adaptor name="chatter" type="ros_sub" datatype="std_msgs/String" />
  </general>

  <state name="main_state">
    <rule source="chatter">
      <script>
        print(" I harad %s" % rtc_in_data)
      </script>
    </rule>
  </state>
 </seatml>
ROS2の場合には、Python2.7ではなく、Python3.xが必要になります。eSEATもinstall.shスクリプトでは、Python2.7のみへインストールされますので、Python3.x用にセットアップをして下さい。
eSEATのソースディレクトリに移動して、下記のスクリプトを実行して下さい。
 # sudo apt-get -y install python3-pip python3-tk python3-yaml python3-lxml
 # sudo python3 setup_py3.py install
Python3用のeSEATのセットアップの終了後、動作確認を行います。
ROS2における動作確認は、2つのターミナルで行います。(ROS2では、roscoreが必要なくなりました)
  • ターミナル1(talker)
 # . /opt/ros/ardent/setup.bash
 # cd ~/ros_ws
 # eSEAT_Node3 talker.seatml
  • ターミナル2(listener)
 # . /opt/ros/ardent/setup.bash
 # cd ~/ros_ws
 # eSEAT_Node3 listener.seatml
これでROSの時と同様に、ターミナル1で生成されたメッセージが、ターミナル2に表示されると思います。

資料