ここではサービスポートを使ったRTCの開発方法について解説します.
サービスポートはデータポートとは違い,データの授受に限定されない柔軟なインターフェースを提供します.
たとえばファイルに対する「Open」や「Close」,「Reset」などの,データ授受ではないコンポーネント操作を定義することが出来ます.
ただしサービスポートの使用には注意が必要です.サービスポートはそのインターフェースが柔軟な故に,モジュールごとの接続性を著しく低下させます.これまでのデータポートでは,NumberGeneratorのような数値生成コンポーネントを使って,モータの制御テストなどが簡単に実現できますが,サービスポートでは柔軟性があり過ぎて汎用なテスト用コンポーネントを定義しておくのが難しくなります.
最近,考えを変えました.データポートで無理なシステム設計をするよりは,サービスポートをきちんと定義したほうが,よっぽど再利用性が高く柔軟かつ堅牢なシステムを作ることができます.
サービスポートの問題点といえば,連続的にやりとりするデータにはあまり向いて居ないことでしょうか.データポートのバッファリング機能などが使えません.
サービスポートの利点は,入力と結果をついにすることができる点や,インターフェースの指定の仕方で,多くのデータを同期してやり取りできること,また処理の失敗も受け取ることができることです.
最後にももう一度述べます.
IDLの準備
サービスポートの設計は,IDLというオブジェクト指向の文法で記述します.設計者はまずIDLを使ってやりとりするデータ型や操作の名前,例外などを記述します.
次にIDLコンパイラを使って,IDLから実装用の言語のプログラムコードに変換します.今回はC++について説明しますが,C,C#,Java, rubyやPythonなどへの変換が可能になっています.
IDLの文法
IDLの文法はC++やJavaなどのオブジェクト指向言語が分かる方なら理解が早いと思われますが,このサイトで扱うには大きな内容なので,書籍やウェブサイトに任せます.
IDL文法参考になるサイト
IDL文法参考になる本 (Amazon.co.jpに飛びます.アフィリエイトになってるんで直接買わないでいいです.)
はじめてのコンポーネント指向ロボットアプリケーション開発 ~RTミドルウェア超入門~
ここではまず「操作」について学びます.独自の構造体を使ったデータ型定義に関しては別の機会にします.
例として以下のIDLを定義しました.
こちらからダウンロードできます
SimpleService
/** * Filename = SimpleService.idl * author: ysuga **/ module ysuga { interface SimpleService { long read (out long data); long write (in long data); long reset (); }; };
Posixシステムコールを参考に定義しています.IDLの文法はほとんどC++言語の宣言と同じなので皆さんならわかるはずだと思います.ポイントは・・・
- moduleでパッケージ名(名前空間)を定義
- interface文はinterface名を定義します.Javaならばクラスの名前に当たる定義
- writeやreadはオペレーション名です.メソッド名に当たります.
インターフェース名
インターフェース名には独自の名前が付けられますが,だれが見てもわかりやすい定義を心がけるのが基本です.
オペレーション
オペレーションの定義には,返り値の属性,引数の属性,引数の順序が重要ですが,C++やJavaと異なるのはディレクション属性です.
ディレクション属性はin, out, inoutの3種類があり,それぞれサーバー側から見てデータがin(つまりサーバの読み込みのみ),out(つまりサーバー側で書き込みが行われる),inout(サーバー側で読み込み書き込みの双方が行われる)の3種類となるわけです.
C++で考えればinはクライアント側からの呼び出しで「値渡し」,outおよびinoutは「参照渡し」となります.
この他にオペレーションでは,例外やコンテキスト定義が可能です.また,Javaのパッケージにあたるmodule定義も可能です.本来は同期に関しても定義可能ですが試していません.
データ型
データ型として,CORBAの組み込みデータ型がいくつか使えます.longは整数型,doubleが実数型…などです.上記のリンクが参考になります.ここではシンプルにlong型だけ使っています。
RTC Builder
RTC Builderを使ってスケルトンコードを生成します.
SimpleServiceProvier
サービスポートのタブをクリックして,サービスポートを追加します.「Add Port」ボタンを押せばサービスポートが追加されます.
「Name」の欄ではサービスポート名が定義されます.これはRT System Editorなどで表示されるポート名になります.Positionは表示位置です.これはどっちでもイイ…
次に「Add Interface」ボタンでインターフェースを追加します.追加されたインターフェースを選択すると,右側のメニューが変わります.
設定項目は以下の通り.
- Interface Name … インターフェース名.SimpleServiceとします.
- Direction … プロバイダ側は「Provided」とします.
- Instance Name … プログラムコード内のインスタンス名です.simpleServiceProviderとします.
- Var Name … サービス自体の実装の変数名です.こちらもsimpleServiceProvider
- IDL file … IDLファイルの位置.「Browse」ボタンでダウンロードしたidlを選択します.
- Interface Type … IDLファイルの選択を行うとプルダウンからIDLで定義されるInterface名を選択できます.
- IDL path … 複数のIDLを使う場合に必要になると思いますが,今は定義しなくても動きます.
生成されるファイルのうち,大事なものをざっと挙げると,
- SimpleServiceProvider.h … RTCの宣言
- SimpleServiceProvider.cpp … RTCの実装
- SimpleServiceSVC_impl.h … サービスポートの宣言
- SimpleServiceSVC_impl.cpp … サービスポートの実装
の4つです.上の3つはいつも生成されるRTCの本体ですが,これ以外にサービスポートの実装用のクラスが生成されます.
生成されるファイルのうち,大事なものをざっと挙げると,
- SimpleServiceProvider.java … RTCの宣言
- SimpleServiceProviderComp.java … RTC実行用のmain関数
- SimpleServiceProviderimpl.java … RTCの実装
- SimpleServiceSVC_impl.java … サービスポートの実装
の4つです.上の2つはいつも生成されるRTCの本体ですが,これ以外にサービスポートの実装用のクラスが生成されます.
生成されるファイルのうち,大事なものをざっと挙げると,
- SimpleServiceProvider.py … RTC
- SimpleService_idl_example.py … サービスポートの実装
- idlcompile.bat … IDLコンパイルを行うためのスクリプト
の4つです.SimpleServiceProvider.pyはいつも生成されるRTCの本体ですが,これ以外にサービスポートの実装用のクラスが生成されます.
サービスポート実装用クラスにwriteやread,resetなどのメソッドが組み込まれているので,これをカスタマイズします.Consumer側からサービスポートのreadなどの関数を呼び出すと,リモートでこのサービスポートのクラスのreadなどが呼びだされる,というわけです.
このサービスポートのクラスのオブジェクトは自動的にRTCに追加されていますので,もとのRTCからonExecuteなどで,サービスポートのクラスにアクセスして,データを周期的に確認することもできるでしょう.
SimpleServiceConsumer
サービスを利用する側はコンシューマと呼ばれます.RTCBuilderでSimpleServiceConsumerプロジェクトを作成します.
サービスポートのタブをクリックして,サービスポートを追加します.「Add Port」ボタンを押せばサービスポートが追加されます.
さらに「Add Interface」ボタンでインターフェースを追加します.追加されたインターフェースを選択すると,右側のメニューが変わります.
設定項目は以下の通り.
- Interface Name … インターフェース名.SimpleServiceとします.Providerと同じにしなくてはなりません
- Direction … コンシューマ側は「Required」とします.
- Instance Name … プログラムコード内のインスタンス名です.simpleServiceConsumerとします.
- Var Name … サービス自体の実装の変数名です.こちらもsimpleServiceConsumer
- IDL file … IDLファイルの位置.「Browse」ボタンでダウンロードしたidlを選択します.
- Interface Type … IDLファイルの選択を行うとプルダウンからIDLで定義されるInterface名を選択できます.
- IDL path … 複数のIDLを使う場合に必要になると思いますが,今は定義しなくても動きます.
さて,コンシューマ側では生成されたファイルはいつものRTCとほとんど変わりません.
その代わり,コンパイルする過程で,Stubと呼ばれるクラスが自動的に生成されます.これはIDLファイルからIDLコンパイラというツールを利用して自動的に生成されます.Stubクラスは,IDLで定義したインターフェースの関数を持っていますので,これを呼び出すことで,接続先のサービスポートの関数をリモートから呼び出すことができます.
プロバイダー側コーディング
さて,ゴリゴリとコーディングします.
サービスポートクラスの編集
“data”というメンバを追加して,ここにデータを書き込んだり,読み込んだりしてみましょう.
SimpleServiceProvider_impl.hを編集して,m_dataメンバを追加します.ついでにアクセサも追加しておきます.
public: /*! * @brief standard constructor */ SimpleServiceSVC_impl(); /*! * @brief destructor */ virtual ~SimpleServiceSVC_impl(); // attributes and operations CORBA::Long read(CORBA::Long& data); CORBA::Long write(CORBA::Long data); CORBA::Long reset(); /** Add From Here **/ private: long m_data; public: long getData() { return m_data; } void setData(long data) {m_data = data;} }; |
Java版では,生成されたファイルの中にbuild_SimpleServiceProvider.xmlというantビルド用のxml設定ファイルがあります.これを右クリック->実行->ANTビルドで実行します.
そして,srcフォルダを「リフレッシュ」するとビルドが可能になるはずです.
うまくいかない場合はビルドパスが無効かもしれません.プロパティを開き,無効なビルドパスを除去して,プログラムフォルダ内のOpenRTM-aist/1.*/jar/にある,OpenRTM-aist.***.jarおよびcommon-cli.***.jarを追加してビルドしてください.
SimpleServiceSVC_impl.javaがサービスポート本体です.ここにdataメンバを加えて,アクセサを追加します.
public class SimpleServiceSVC_impl extends SimpleServicePOA{ private int data; final public int getData() {return data;} final public void setData(int d) {data = d;} public SimpleServiceSVC_impl() { // Please add extra constructor code here. } |
Python版では,生成されたファイルの中に,idlcompile.batというファイルがあります.これを実行するのが最初なんですが,現時点ではIDLファイルのコンパイルがうまくいかない場合があります.
idlcompile.batを実行してもすぐにDOS窓が閉じ,何も変化が起きない場合は,以下のようにbatファイルを編集します.
C:\Python26\omniidl.exe -bpython SimpleService.idl
omniidlがPythonに対応したバージョンを呼べるように,無理やり絶対パスを指定するわけです.
んで,編集.
SimpleService_idl_example.pyというファイルが,サービスポートの実装になります.
このクラスのオブジェクトをSimpleService.pyに記述されているRTCが保持しているので,
SimpleService_iクラスにdataというメンバを追加して,それにデータを保持させ,
RTCからはそのデータメンバにアクセスする形をとることにします.
class SimpleService_i (ysuga__POA.SimpleService): """ \class SimpleService_i Example class implementing IDL interface ysuga.SimpleService """ def __init__(self): """ \brief standard constructor Initialise member variables here """ self._data = 0 pass def getData(self): return self._data def setData(self, data): self._data = data |
readメソッド
readメソッドでは,dataメンバのデータを,引数のdataに対して書き込みます.
CORBA::Long SimpleServiceSVC_impl::read(CORBA::Long& data) { // Please insert your code here and remove the following warning pragma #ifndef WIN32 #warning "Code missing in function <CORBA::Long SimpleServiceSVC_impl::read(CORBA::Long& data)>" #endif data = getData(); return 0; } |
out方向のオペレーションでは,**Holderというヘルパークラスを使ってデータをやり取りします.Java言語で参照渡しをする時の定石です.valueメンバがデータ本体なので,ここにdataを書き込みます.
public int read(org.omg.CORBA.IntHolder data) { // Please insert your code here and remove the following warning pragma // TODO "Code missing in function <int read(org.omg.CORBA.IntHolder data)>" data.value = this.data; return 0; } |
SimpleService_idl_example.pyを変更します.デフォルトでCORBA.NO_IMPLEMENTエラーを送出するようになっていますが,これはコメントアウトします.
# long read(out long data) def read(self): # raise CORBA.NO_IMPLEMENT(0, CORBA.COMPLETED_NO) # *** Implement me # Must return: result, data return (0, self._data) |
Pythonでは,out方向の引数は省略され,データをリストの形で返却するようにします.
リストの一番目が返却値,2番目以降はout方向のデータになります.
writeメソッド
writeではdataメンバにデータを書き込みます.
CORBA::Long SimpleServiceSVC_impl::write(CORBA::Long data) { // Please insert your code here and remove the following warning pragma #ifndef WIN32 #warning "Code missing in function <CORBA::Long SimpleServiceSVC_impl::write(CORBA::Long data)>" #endif m_data = data; return 0; } |
writeオペレーションは極めてシンプルです.
public int write(int data) { // Please insert your code here and remove the following warning pragma // TODO "Code missing in function <int write(int data)>" this.data = data; return 0; } |
SimpleService_idl_example.pyを変更します.
# long write(in long data) def write(self, data): # raise CORBA.NO_IMPLEMENT(0, CORBA.COMPLETED_NO) # *** Implement me # Must return: result self._data = data return 0 |
in方向の引数をとる場合は,かなりシンプルにかけますね.説明は必要ないと思います.
resetメソッド
resetではdataをゼロにします.ついでに古いデータを返すことにしましょう.
CORBA::Long SimpleServiceSVC_impl::reset() { // Please insert your code here and remove the following warning pragma #ifndef WIN32 #warning "Code missing in function <CORBA::Long SimpleServiceSVC_impl::reset()>" #endif long old_data = m_data; m_data = 0; return old_data; } |
resetメソッドもシンプルです.引数がありませんしね.
public int reset() { // Please insert your code here and remove the following warning pragma // TODO "Code missing in function <int reset()>" int buf = data; data = 0; return buf; } |
SimpleService_idl_example.pyを変更します.
dataの値をゼロにして,もともとのdataの値を返却します.これも簡単ですね.
# long reset() def reset(self): # raise CORBA.NO_IMPLEMENT(0, CORBA.COMPLETED_NO) # *** Implement me # Must return: result buffer = self._data self._data = 0 return buffer |
プロバイダ側RTC
先ほどのSimpleService_iクラスは,RTCBで定義したsimpleServiceProviderという変数でRTCに保持されていますので,これにアクセスします.
getData関数を定義したので,これでdataの値を読み出し,出力することとしましょう.
C++バージョンのコードです.SimpleServiceProvider_Impl.cppを編集します.
RTC::ReturnCode_t SimpleServiceProvider::onExecute(RTC::UniqueId ec_id) { std::cout << "data is " << m_simpleServiceProvider.getData() << std::endl; return RTC::RTC_OK; } |
SimpleServiceProviderImpl.javaを編集します.
@Override protected ReturnCode_t onExecute(int ec_id) { System.out.println("data is " + m_simpleServiceProvider.getData()); return super.onExecute(ec_id); } |
def onExecute(self, ec_id): """ The execution action that is invoked periodically former rtc_active_do() \param ec_id target ExecutionContext Id \return RTC::ReturnCode_t """ buf = self._simpleServiceProvider.getData() print ("data is %d" , buf) return RTC.RTC_OK |
コンシューマ側
onExecute内で,キーボード入力に従ってデータをやり取りするようにしましょう.
サービスポートはRTCBで定義したsimpleServiceConsumerで宣言され,RTCに保持されています.
RTC::ReturnCode_t SimpleServiceConsumer::onExecute(RTC::UniqueId ec_id) { std::cout << "Input Command (q:reset, r:read, w:write)" << std::endl; char c; std::cin >> c; CORBA::Long data; switch(c) { case 'r': m_simpleServiceConsumer->read(data); std::cout << "read(): Data is " << (long)data << std::endl; break; case 'w': data = (long)rand(); std::cout << "write(" << (long)data << ")" << std::endl; m_simpleServiceConsumer->write(data); break; case'q': data = m_simpleServiceConsumer->reset(); std::cout << "reset(): return value is " << data << std::endl; break; default: break; } return RTC::RTC_OK; } |
@Override protected ReturnCode_t onExecute(int ec_id) { System.out.println("Input Command: r:read, w:write, q:reset"); try { int c = System.in.read(); switch (c) { case 'r': IntHolder intHolder = new IntHolder(); this.m_simpleServiceConsumerBase._ptr().read(intHolder); System.out.println("read(): data is " + intHolder.value); break; case 'w': int val = new Random().nextInt(); this.m_simpleServiceConsumerBase._ptr().write(val); System.out.println("write(" + val + ")"); break; case 'q': int ret = this.m_simpleServiceConsumerBase._ptr().reset(); System.out.println("reset():returns " + ret); break; default: break; } } catch (Exception e) { System.out.println("Exception occurred:" + e); } return super.onExecute(ec_id); } |
def onExecute(self, ec_id): """ The execution action that is invoked periodically former rtc_active_do() \param ec_id target ExecutionContext Id \return RTC::ReturnCode_t """ data = random.randint(1, 10) # need to import random print "Input data" c = raw_input() if c == 'q': retval = self._simpleServiceConsumer._ptr().reset() print ("reset() returns " , retval) if c == 'r': retval = self._simpleServiceConsumer._ptr().read() print ("read() returns " , retval[0]) print (" data is " , retval[1]) if c == 'w': retval = self._simpleServiceConsumer._ptr().write(data) print ("write(", data, ")" ) return RTC.RTC_OK |
rtc.confの設定
実行前にrtc.confファイルの変更が必要です.今回もnamingサービスの設定が必要になります.
実行
ネームサーバーを起動してから,ProviderとConsumerを実行します.
RT System Editorで四角いサービスポートを繋げば準備完了.アクティブ化して処理を開始します.
Consumer側のウィンドウをアクティブにして,キーボードから「w」や「r」などを送れば,write,readを実行します.
まとめ
いかがでしたか?
最初に述べましたがサービスポートはもろ刃の剣です.
私の場合,基本的にデータの送受信はデータポートを使って行うようにします.resetなどはステートの変化で実現できますからサービスポートでは実装しません.制御パラメータはコンフィグにします.
あれ?サービスポートいらない?
繰り返しますけど,サービスポートの利点は,入力と結果をついにすることができる点や,インターフェースの指定の仕方で,多くのデータを同期してやり取りできること,また処理の失敗も受け取ることができることです.
たとえば,「まとまったタスクの実行」「複雑な認識処理」「作業計画」などに向いて居ます.たとえば,画像認識処理に必要なパラメータと画像を引数として,認識結果(たとえばオブジェクトのクラスと位置・姿勢)を参照渡しで受け取ることとし,計画の成功・失敗(エラーコード)を返り値とします.例外も投げることができますが,ここでは説明しません.
こうすれば,データポートで認識させるよりも,どの画像を渡した結果が認識成功だったのか,というのが一目瞭然にになります.
認識できなかった場合もエラーを返すことができます.これはデータポートではできないことです.