データポート(基本編)では、データポートの基本的な使い方について説明しました。応用編では、もう少し踏み込んだ使い方について解説します。
データポートでは、事前に定義されたデータ型 (例: TimedLong、TimedDouble 等) 以外に、自分で定義したデータ型を使用することもできます。 ただし、自分で新たなデータ型を作る前に、既に似たようなデータ型が定義されていないか確認して、その中に必要なデータ型がない場合にのみ新たなデータ型を定義することをお勧めします。
OpenRTM-aist では、以下の IDLファイルでデータポートに使用するデータ型が定義されています。
データ型を定義する IDLファイルを作成します。データ型は struct キーワードで定義します。以下の基本型や、文字列型、シーケンス型が利用できます。
ここでは、MyDataType.idl に MyData というデータ型を定義することにします。 表示のためにスペースを前方に入れていますが、これは実際にIDLファイルを使用する上で不要のため削除してください。
// @file MyDataType.idl #include "BasicDataType.idl" module MyModule { struct MyData { RTC::Time tm; short shortVariable; long longVariable; sequence<double> data; }; };
2行目に
#include "BasicDataType.idl"
RTCBuilder でプロジェクトの作成を行います。
「RTC Builder Project」のアイコンをクリックし、表示したウィンドウにプロジェクト名を入力し「終了」すると、左のウィンドウに生成したプロジェクトファイルが表示します。
そこに独自データ型を配置するidlフォルダーがあるので独自データ型のIDLファイル(MyDataType.idl)を、ドラッグ&ドロップや[右クリック]>[貼り付け]などで置いてください。
プロジェクトフォルダーは、RTCBuilder起動直後に表示する「ディレクトリをワークスペースとして選択」をデフォルトで進めた場合、[ c:\Users\ユーザ名\workspase ]フォルダー以下にあります。
RTCBuilder で独自データ型のidlを使ったコンポーネントの作成を行います。
データポート設定タブを開き、データポート(InPort/OutPort)を定義します。
作成したデータポートで独自データ型を使いたい場合、[Reload] をクリックするとidlフォルダーのMyDataType.idlが読み込まれ、 *データ型 のプルダウンで新たに定義した MyData を選択できます。
その他、コンポーネント作成に必要な項目の設定が終わったら、基本タブに戻り、[コード生成] ボタンをクリックし、コードの生成を行います。
InPort は isNew() でデータの到着の有無を確認して、read() で読みだす、あるいは OutPort は write() でデータを送り出す、ということについてはすでに述べました。
例えば、InPort はデータが来てから、onExecute() 等などの関数内で、isNew() を呼び read() を呼び出すまでデータを取得することはできません。 onExecute() の周期が非常に速かったとしても、データが InPort に到着するタイミングと、実際に処理が行われるタイミングは非同期に行われます。
データが到着してすぐに、すなわち同期的に処理を行いたい場合にはどうすればよいのでしょうか。これを実現する方法として、OpenRTM-aist ではデータポートやコネクタの種々の処理のタイミングで呼び出されるコールバックを定義しています。
コールバックには大きく分けて、1) InPort、2) OutPort、3) コネクタ、4) ポート の4種類のコールバックが用意されています。
InPort には、以下の2種類のコールバックが用意されています。 これらは rtm/PortCallback.h において定義されています。
OnRead コールバックは read() が呼び出されたときに、OnReadConvert は read() が呼び出されたとき、呼び出し元にある種の変換を施したデータを返すために使用するコールバックです。
それぞれのコールバックは、rtm/PortCallback.h で定義されているそれぞれのファンクタの基底クラスを継承することにより実装します。
以下にそれぞれの実装例を示します。
#include <rtm/Portcallback.h> template <class T> class MyOnRead : public RTC::OnRead<T> { public: MyOnRead(std::ostream& os) : m_os(os) {}; virtual void operator()() { m_os << "read() 関数が呼ばれました。" << std::endl; std::cout << "read() 関数が呼ばれました。" << std::endl; } private: std::ostream& m_os; }; template <class T> class MyOnReadConvert : public RTC::OnReadConvert<T> { public: virtual T operator()(const T& value) { T tmp; tmp.data = value.data * value.data; return tmp; } };
OnRead を継承した MyOnRead ファンクタでは、コンストラクタで出力ストリーム std::ostream を渡しています。どこかでオープンしたファイル出力ストリーム std::ofstream 等を渡すことを意図しています。 ファンクタの実体である operator()() では、出力ストリームと標準出力に対して、文字列を出力しています。このように、ファンクタでは、予めコンストラクタなどで状態変数を渡すことで、他のオブジェクトに対する呼び出しも実現することができます。
一方 OnReadConvert<T> を継承した MyOnReadConvert は operator()(constT&) のみを実装しています。この関数の引数には、read() が呼ばれたときにInPort 変数に読みだされる前のデータが渡されます。 この関数内で何らかの処理を行い return で返したデータは InPort 変数に書き込まれます。この例では、データ型に data というメンバがあり、かつ乗算演算子が定義されているという前提で自乗を計算して返しています。 適切なメンバがない変数型を使用すればコンパイルエラーになります。
さて、このファンクタを実際にコンポーネントに組み込んでみましょう。InPort を使用しているサンプルとして、ここでは OpenRTM-aist に含まれているサンプルである ConsoleOut を利用します。ConsoleOut は OpenRTM-aist のソースを展開すると、
OpenRTM-aist-<version>/examples/SimpleIO/
の下に、また Linux 等でパッケージ等からインストールすると、
/usr/share/OpenRTM-aist/examples/src/
の下にソースコードがあります。
まず、上記のクラス定義を、ConsoleOut.h に記述します。クラス定義は、本来別のソースに記述した方が良いのですが、ファンクタクラスは、このコンポーネント内でしか使用せず、内容も短いものですので、こういう場合はヘッダ内で実装も含めて定義しても構わないでしょう。
// ConsoleOut.h 中略 // Service Consumer stub headers // <rtc-template block="consumer_stub_h"> // </rtc-template> using namespace RTC; // ここから追加分 template <class T> class MyOnRead : public RTC::OnRead<T> { public: MyOnRead(std::ostream& os) : m_os(os) {}; virtual void operator()() { m_os << "read() 関数が呼ばれました。" << std::endl; std::cout << "read() 関数が呼ばれました。" << std::endl; } private: std::ostream& m_os; }; template <class T> class MyOnReadConvert : public RTC::OnReadConvert<T> { public: virtual T operator()(const T& value) { T tmp; tmp.data = value.data * value.data; return tmp; } }; // ここまで追加分 class ConsoleOut : public RTC::DataFlowComponentBase { 中略 protected: // DataInPort declaration // <rtc-template block="inport_declare"> TimedLong m_in; InPort<TimedLong> m_inIn; 中略 private: //ここから追加分 MyOnRead<TimedLong>* m_onread; MyOnReadConvert<TimedLong>* m_onreadconv; //ここまで追加分 };
まず、ConsoleOut クラスの宣言の前に、コールバックファンクタ MyOnRead とMyOnReadConvert を宣言します。 これらのクラスのポインタ変数をメンバとして持たせるために、private の部分に、それぞれのポインタ変数を宣言します。 このとき、MyOnRead/MyOnReadConvert ともに、クラステンプレートの型引数にこのコンポーネントの InPort の型と同じ、TimedLong を与えていることに注意してください。
OutPort には、以下の2種類のコールバックが用意されています。 これらは rtm/PortCallback.h において定義されています。
OnWrite コールバックは write() が呼び出された際に、OnWriteConvert はwrite() が呼び出された際に、ある種の変換を施したデータを送信するために使用するコールバックです。
それぞれのコールバックは、InPort と同様 rtm/PortCallback.h で定義されているそれぞれのファンクタの基底クラスを継承することにより実装します。
#include <rtm/Portcallback.h> template <class T> class MyOnWrite : public RTC::OnWrite<T> { public: MyOnWrite(std::ostream& os) : m_os(os) {}; virtual void operator()() { m_os << "write() 関数が呼ばれました。" << std::endl; std::cout << "write() 関数が呼ばれました。" << std::endl; } private: std::ostream& m_os; }; template <class T> class MyOnWriteConvert : public RTC::OnWriteConvert<T> { public: virtual T operator()(const T& value) { T tmp; tmp.data = 2 * value.data; return tmp; } };
コールバック用のファンクタの書き方は、InPort の OnRead/OnReadConvert とほぼ同じです。OnWrite を継承した MyOnWrite ファンクタでは、コンストラクタで出力ストリーム std::ostream を渡しています。 どこかでオープンしたファイル出力ストリーム std::ofstream 等を渡すことを意図しています。ファンクタの実体である operator() では、出力ストリームと標準出力に対して、文字列を出力しています。 このように、ファンクタでは、予めコンストラクタなどで状態変数を渡すことで、他のオブジェクトに対する呼び出しも実現することができます。
一方 OnReadConvert<T> を継承した MyOnReadConvert は operator()(constT&) のみを実装しています。この関数の引数には、read() を呼んだときに InPort 変数に読みだされる前のデータが渡されます。この関数内で何らかの処理を行い return で返したデータは InPort 変数に書き込まれます。 この例では、データ型に data というメンバがあり、かつ乗算演算子が定義されているという前提で自乗を計算して返しています。適切なメンバがない変数型を使用すればコンパイルエラーになります。
コネクタはバッファおよび通信路を抽象化したオブジェクトです。図に示すように、OutPort と InPort の間に存在し、OutPort からは write() 関数によりデータの書き込み、InPort からは read() 関数によりデータの読み出しが行われます。 コネクタは、データがどのような手段で OutPort から InPort へ伝送されるかを抽象化し隠蔽します。
OutPort は複数の InPort へ接続することができますが、一つの接続につき、一つのコネクタが生成されます。(実際には InPort も複数の接続を同時に持つこともできますが、データを区別する方法がないので、通常は用いません。) つまり、接続が3つあれば、コネクタが3つ存在し、それぞれに対して書き込みのステータスが存在することになります。
また、これらの機能のために、OutPort/InPort 一対に対して、それぞれ一つコネクタが存在する必要があることがわかります。 さらに、コネクタをサブスクリプション型に対応した実装レベルでモデル化するにあたり、パブリッシャと呼ばれる非同期通信のためのオブジェクトを導入しました。
データポートは接続が確立されると、1つの接続につき1つのコネクタオブジェクトを生成します。コネクタは、OutPort と InPort をつなぐデータストリームの抽象チャネルです。
データポートは、データの送受信を行った際に、ステータスを返します。 ステータスは、rtm/DataPortStatus.h で定義されています。
独自データポートインターフェースの作成例を示します。
以下の関数、クラスの定義が必要です。
データポートインターフェース(Push型)のプロバイダクラスです。 コンシューマ側で put関数を呼び出した際に、何らかの方法によりプロバイダ側にデータを転送する必要があります。 このサンプルではコンシューマ側の put関数呼び出し時にファイルにデータを書き込み、プロバイダ側でファイルからデータを読み込むことでデータの転送を行っています。
//InPortTestProvider.cpp #include "InPortTestProvider.h" #ifdef WIN32 #pragma warning( disable : 4290 ) #endif namespace RTC { InPortTestProvider::InPortTestProvider(void) : m_buffer(0), m_running(true), m_filename("data.dat") { setInterfaceType("test"); activate(); } InPortTestProvider::~InPortTestProvider(void) { m_running = false; wait(); } //プロバイダ生成時に呼び出される関数 //コネクタプロファイル、ポートのプロパティの情報を受け取る void InPortTestProvider::init(coil::Properties& prop) { } void InPortTestProvider:: setBuffer(BufferBase<cdrMemoryStream>* buffer) { m_buffer = buffer; } void InPortTestProvider::setListener(ConnectorInfo& info, ConnectorListeners* listeners) { m_profile = info; m_listeners = listeners; } void InPortTestProvider::setConnector(InPortConnector* connector) { m_connector = connector; } //別スレッドにより実行される関数 //周期的にファイルからデータを読み込んでバッファに書き込む //この関数はこのサンプルでは必要ですが、独自インターフェースを作成するうえで //必須ではありません int InPortTestProvider::svc() { coil::sleep(1); while (m_running) { std::ifstream fin; fin.open(m_filename, std::ios::in | std::ios::binary); if (fin) { while (!fin.eof()) { int data_size = 0; fin.read((char*)&data_size, sizeof(int)); if (data_size > 0) { CORBA::OctetSeq data; data.length(data_size); fin.read((char*)&data[0], data_size); //cdrMemoryStream型変数にデータを格納してバッファに書き込む //以下の記述方法はomniORB特有なため、TAOやORBexpressに対応する場合は //分ける必要がある cdrMemoryStream cdr; //エンディアンの設定を行う bool endian_type = m_connector->isLittleEndian(); cdr.setByteSwapFlag(endian_type); //データを書き込む cdr.put_octet_array(&(data[0]), data.length()); //バッファに書き込む m_buffer->write(cdr); } } fin.close(); } } return 0; } //コネクタ接続時に呼び出される関数 //この関数はコンシューマ側のsubscribeInterface関数よりも前に呼び出される //このため、publishInterface関数で設定した情報をコンシューマ側のsubscribeInterface関数で //取得することができる //何か問題があった時はfalseを返してコネクタを切断する bool InPortTestProvider:: publishInterface(SDOPackage::NVList& properties) { //データを書き込むファイル名の情報を格納する CORBA_SeqUtil:: push_back(properties, NVUtil::newNV("dataport.test.filename", m_filename.c_str())); return true; } }; extern "C" { //この関数をモジュールロード時に呼び出す必要がある void InPortTestProviderInit(void) { RTC::InPortProviderFactory& factory(RTC::InPortProviderFactory::instance()); factory.addFactory("test", ::coil::Creator< ::RTC::InPortProvider, ::RTC::InPortTestProvider>, ::coil::Destructor< ::RTC::InPortProvider, ::RTC::InPortTestProvider>); } };
データポートインターフェース(Push型)のコンシューマクラスです。InPortProvider を継承する必要があります。 Taskクラスの継承はこのサンプル独自のものなので必須ではありません。
//InPortTestProvider.h #ifndef RTC_INPORTTESTPROVIDER_H #define RTC_INPORTTESTPROVIDER_H #include <rtm/BufferBase.h> #include <rtm/InPortProvider.h> #include <rtm/CORBA_SeqUtil.h> #include <rtm/Manager.h> #include <rtm/ConnectorListener.h> #include <rtm/ConnectorBase.h> #include <fstream> #ifdef WIN32 #pragma warning( disable : 4290 ) #endif namespace RTC { class InPortTestProvider : public InPortProvider, public coil::Task { public: InPortTestProvider(void); virtual ~InPortTestProvider(void); virtual void init(coil::Properties& prop); virtual void setBuffer(BufferBase<cdrMemoryStream>* buffer); virtual void setListener(ConnectorInfo& info, ConnectorListeners* listeners); virtual void setConnector(InPortConnector* connector); virtual bool publishInterface(SDOPackage::NVList& properties); virtual int svc(); private: CdrBufferBase* m_buffer; ConnectorListeners* m_listeners; ConnectorInfo m_profile; InPortConnector* m_connector; bool m_running; std::string m_filename; }; }; extern "C" { DLL_EXPORT void InPortTestProviderInit(void); }; #ifdef WIN32 #pragma warning( default : 4290 ) #endif #endif
データポートインターフェース(Push型)のコンシューマクラスです。Push型の場合は InPort側にコンシューマ、OutPort側にプロバイダを生成します。 コンシューマ側で put関数を呼び出した際に、何らかの方法によりプロバイダ側にデータを転送する必要があります。 このサンプルではコンシューマ側の put関数呼び出し時にファイルにデータを書き込み、プロバイダ側でファイルからデータを読み込むことでデータの転送を行っています。
//InPortTestConsumer.cpp #include <rtm/NVUtil.h> #include "InPortTestConsumer.h" namespace RTC { InPortTestConsumer::InPortTestConsumer(void) : rtclog("InPortTestConsumer") { } InPortTestConsumer::~InPortTestConsumer(void) { RTC_PARANOID(("~InPortTestConsumer()")); } //コネクタプロファイル、ポートのプロパティの情報を受け取る void InPortTestConsumer::init(coil::Properties& prop) { m_properties = prop; } //データ転送時に呼び出される関数 //put関数内でプロバイダ側にデータを転送する処理を記述する InPortConsumer::ReturnCode InPortTestConsumer:: put(const cdrMemoryStream& data) { RTC_PARANOID(("put()")); //このサンプルでは、コンシュマー側でファイルにデータを書き込んで、プロバイダ側で //ファイル内のデータを読み込むことにしている //バイナリファイルを開く m_file.open(m_filename, std::ios::out | std::ios::binary | std::ios::trunc); //データサイズと生データをファイルに書き込む int data_size = data.bufSize(); m_file.write((char*)&data_size, sizeof(int)); m_file.write((char*)data.bufPtr(), data_size); //ファイルを閉じる m_file.close(); return PORT_OK; } //コネクタ接続時に呼び出される関数 //この関数はプロバイダ側のpublishInterface関数よりも後に呼び出される //このため、プロバイダ側のpublishInterface関数で設定した情報を取得することができる //return: 何か問題があった時はfalseを返してコネクタを切断する bool InPortTestConsumer:: subscribeInterface(const SDOPackage::NVList& properties) { //プロバイダ側で設定したファイル名を取得する CORBA::Long index = NVUtil::find_index(properties, "dataport.test.filename"); const char* filename(0); properties[index].value >>= filename; //取得したファイル名のファイルを開く m_filename = filename; m_file.open(m_filename, std::ios::out | std::ios::binary | std::ios::trunc); m_file.close(); return true; } //コネクタ切断時に呼び出される関数 void InPortTestConsumer:: unsubscribeInterface(const SDOPackage::NVList& properties) { } void InPortTestConsumer::publishInterfaceProfile(SDOPackage::NVList& properties) { } }; extern "C" { //コンシューマ登録関数 //この関数をモジュールロード時に呼び出す必要がある void InPortTestConsumerInit(void) { RTC::InPortConsumerFactory& factory(RTC::InPortConsumerFactory::instance()); factory.addFactory("test", ::coil::Creator< ::RTC::InPortConsumer, ::RTC::InPortTestConsumer>, ::coil::Destructor< ::RTC::InPortConsumer, ::RTC::InPortTestConsumer>); } };
データポートインターフェース(Push型)のコンシューマクラスです。InPortConsumer を継承する必要があります。
//InPortTestConsumer.h #ifndef RTC_INPORTTESTCONSUMER_H #define RTC_INPORTTESTCONSUMER_H #include <rtm/InPortConsumer.h> #include <rtm/Manager.h> #include <fstream> namespace RTC { class InPortTestConsumer : public InPortConsumer { public: DATAPORTSTATUS_ENUM InPortTestConsumer(void); virtual ~InPortTestConsumer(void); virtual void init(coil::Properties& prop); virtual ReturnCode put(const cdrMemoryStream& data); virtual void publishInterfaceProfile(SDOPackage::NVList& properties); virtual bool subscribeInterface(const SDOPackage::NVList& properties); virtual void unsubscribeInterface(const SDOPackage::NVList& properties); private: mutable Logger rtclog; coil::Properties m_properties; std::ofstream m_file; std::string m_filename; }; }; extern "C" { DLL_EXPORT void InPortTestConsumerInit(void); }; #endif
OpenRTM-aist のマネージャは「XXX.dll」というダイナミックリンクライブラリをロードした場合に「XXXInit」関数を呼び出します。 このサンプルの場合は「InPortTestInterface.dll」をロードして、以下の「InPortTestInterfaceInit」関数を呼び出します。
//InPortTestInterface.cpp #include "InPortTestConsumer.h" #include "InPortTestProvider.h" extern "C" { DLL_EXPORT void InPortTestInterfaceInit(RTC::Manager* manager) { InPortTestProviderInit(); InPortTestConsumerInit(); } };
1. OpenRTM-aistがインストールされている環境を用意します。 2. ビルドのために CMake設定ファイル (CMakeLists.txt) を作成します。 以下は、独自インターフェースのサンプルをビルドするための CMake設定ファイルです。
//OpenRTM-aistのライブラリを見つけるための記述 cmake_minimum_required (VERSION 2.6) find_package(OpenRTM HINTS /usr/lib64/openrtm-1.1/cmake) if(${OpenRTM_FOUND}) MESSAGE(STATUS "OpenRTM configuration Found") else(${OpenRTM_FOUND}) message(STATUS "Use cmake/Modules/FindOpenRTM.cmake in the project") list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake/Modules) find_package(OpenRTM REQUIRED) endif(${OpenRTM_FOUND}) if (DEFINED OPENRTM_INCLUDE_DIRS) string(REGEX REPLACE "-I" ";" OPENRTM_INCLUDE_DIRS "${OPENRTM_INCLUDE_DIRS}") string(REGEX REPLACE " ;" ";" OPENRTM_INCLUDE_DIRS "${OPENRTM_INCLUDE_DIRS}") endif (DEFINED OPENRTM_INCLUDE_DIRS) if (DEFINED OPENRTM_LIBRARY_DIRS) string(REGEX REPLACE "-L" ";" OPENRTM_LIBRARY_DIRS "${OPENRTM_LIBRARY_DIRS}") string(REGEX REPLACE " ;" ";" OPENRTM_LIBRARY_DIRS "${OPENRTM_LIBRARY_DIRS}") endif (DEFINED OPENRTM_LIBRARY_DIRS) if (DEFINED OPENRTM_LIBRARIES) string(REGEX REPLACE "-l" ";" OPENRTM_LIBRARIES "${OPENRTM_LIBRARIES}") string(REGEX REPLACE " ;" ";" OPENRTM_LIBRARIES "${OPENRTM_LIBRARIES}") endif (DEFINED OPENRTM_LIBRARIES) include_directories(${OPENRTM_INCLUDE_DIRS}) include_directories(${OMNIORB_INCLUDE_DIRS}) add_definitions(${OPENRTM_CFLAGS}) add_definitions(${OMNIORB_CFLAGS}) link_directories(${OPENRTM_LIBRARY_DIRS}) link_directories(${OMNIORB_LIBRARY_DIRS}) //プロジェクト名設定 project (InPortTestInterface) //動的ライブラリを作成する add_library(InPortTestInterface SHARED InPortTestProvider.cpp InPortTestConsumer.cpp InPortTestProvider.h InPortTestConsumer.h InPortTestInterface.cpp) //リンクするライブラリの設定 target_link_libraries(InPortTestInterface ${OPENRTM_LIBRARIES})
3. CMake により Visual Studio のプロジェクトファイルを生成します。 4. Visual Studio でビルドします。 ビルド後に、Debugフォルダーに InPortTestInterface.dll が生成されます。 5. rtc.conf を作成します rtc.conf を作成して、Manager 起動時にロードするモジュールで InPortTestInterface.dll を指定します。
manager.modules.preload: InPortTestInterface.dll
InPortTestInterface.dll が、RTC を実行するディレクトリーと異なる場合は、別途モジュール探索パスを設定します。
manager.modules.load_path: C:¥workspace¥InPortTestInterface¥build¥Debug
ConsoleInComp.exe -f ../rtc.conf
モーションエディタ/シミュレータ
動力学シミュレータ
統合開発プラットフォーム
産総研が提供するRTC集
東京オープンソースロボティクス協会
ネットワーク分散環境でデータ収集用ソフトウェアを容易に構築するためのソフトウェア・フレームワーク
データポート(基本編)では、データポートの基本的な使い方について説明しました。応用編では、もう少し踏み込んだ使い方について解説します。
データ型の利用
データポートでは、事前に定義されたデータ型 (例: TimedLong、TimedDouble 等) 以外に、自分で定義したデータ型を使用することもできます。 ただし、自分で新たなデータ型を作る前に、既に似たようなデータ型が定義されていないか確認して、その中に必要なデータ型がない場合にのみ新たなデータ型を定義することをお勧めします。
OpenRTM-aist では、以下の IDLファイルでデータポートに使用するデータ型が定義されています。
独自データ型IDLファイルの作成
データ型を定義する IDLファイルを作成します。データ型は struct キーワードで定義します。以下の基本型や、文字列型、シーケンス型が利用できます。
ここでは、MyDataType.idl に MyData というデータ型を定義することにします。 表示のためにスペースを前方に入れていますが、これは実際にIDLファイルを使用する上で不要のため削除してください。
2行目に
RTC Builderでプロジェクト作成
RTCBuilder でプロジェクトの作成を行います。
「RTC Builder Project」のアイコンをクリックし、表示したウィンドウにプロジェクト名を入力し「終了」すると、左のウィンドウに生成したプロジェクトファイルが表示します。
そこに独自データ型を配置するidlフォルダーがあるので独自データ型のIDLファイル(MyDataType.idl)を、ドラッグ&ドロップや[右クリック]>[貼り付け]などで置いてください。
プロジェクトフォルダーは、RTCBuilder起動直後に表示する「ディレクトリをワークスペースとして選択」をデフォルトで進めた場合、[ c:\Users\ユーザ名\workspase ]フォルダー以下にあります。
データポートの作成
RTCBuilder で独自データ型のidlを使ったコンポーネントの作成を行います。
データポート設定タブを開き、データポート(InPort/OutPort)を定義します。
作成したデータポートで独自データ型を使いたい場合、[Reload] をクリックするとidlフォルダーのMyDataType.idlが読み込まれ、 *データ型 のプルダウンで新たに定義した MyData を選択できます。
その他、コンポーネント作成に必要な項目の設定が終わったら、基本タブに戻り、[コード生成] ボタンをクリックし、コードの生成を行います。
データポート、コネクタのコールバックの利用
InPort は isNew() でデータの到着の有無を確認して、read() で読みだす、あるいは OutPort は write() でデータを送り出す、ということについてはすでに述べました。
例えば、InPort はデータが来てから、onExecute() 等などの関数内で、isNew() を呼び read() を呼び出すまでデータを取得することはできません。 onExecute() の周期が非常に速かったとしても、データが InPort に到着するタイミングと、実際に処理が行われるタイミングは非同期に行われます。
データが到着してすぐに、すなわち同期的に処理を行いたい場合にはどうすればよいのでしょうか。これを実現する方法として、OpenRTM-aist ではデータポートやコネクタの種々の処理のタイミングで呼び出されるコールバックを定義しています。
コールバックには大きく分けて、1) InPort、2) OutPort、3) コネクタ、4) ポート の4種類のコールバックが用意されています。
InPort のコールバック
InPort には、以下の2種類のコールバックが用意されています。 これらは rtm/PortCallback.h において定義されています。
OnRead コールバックは read() が呼び出されたときに、OnReadConvert は read() が呼び出されたとき、呼び出し元にある種の変換を施したデータを返すために使用するコールバックです。
それぞれのコールバックは、rtm/PortCallback.h で定義されているそれぞれのファンクタの基底クラスを継承することにより実装します。
以下にそれぞれの実装例を示します。
OnRead を継承した MyOnRead ファンクタでは、コンストラクタで出力ストリーム std::ostream を渡しています。どこかでオープンしたファイル出力ストリーム std::ofstream 等を渡すことを意図しています。 ファンクタの実体である operator()() では、出力ストリームと標準出力に対して、文字列を出力しています。このように、ファンクタでは、予めコンストラクタなどで状態変数を渡すことで、他のオブジェクトに対する呼び出しも実現することができます。
一方 OnReadConvert<T> を継承した MyOnReadConvert は operator()(constT&) のみを実装しています。この関数の引数には、read() が呼ばれたときにInPort 変数に読みだされる前のデータが渡されます。 この関数内で何らかの処理を行い return で返したデータは InPort 変数に書き込まれます。この例では、データ型に data というメンバがあり、かつ乗算演算子が定義されているという前提で自乗を計算して返しています。 適切なメンバがない変数型を使用すればコンパイルエラーになります。
さて、このファンクタを実際にコンポーネントに組み込んでみましょう。InPort を使用しているサンプルとして、ここでは OpenRTM-aist に含まれているサンプルである ConsoleOut を利用します。ConsoleOut は OpenRTM-aist のソースを展開すると、
の下に、また Linux 等でパッケージ等からインストールすると、
の下にソースコードがあります。
まず、上記のクラス定義を、ConsoleOut.h に記述します。クラス定義は、本来別のソースに記述した方が良いのですが、ファンクタクラスは、このコンポーネント内でしか使用せず、内容も短いものですので、こういう場合はヘッダ内で実装も含めて定義しても構わないでしょう。
まず、ConsoleOut クラスの宣言の前に、コールバックファンクタ MyOnRead とMyOnReadConvert を宣言します。 これらのクラスのポインタ変数をメンバとして持たせるために、private の部分に、それぞれのポインタ変数を宣言します。 このとき、MyOnRead/MyOnReadConvert ともに、クラステンプレートの型引数にこのコンポーネントの InPort の型と同じ、TimedLong を与えていることに注意してください。
OutPort のコールバック
OutPort には、以下の2種類のコールバックが用意されています。 これらは rtm/PortCallback.h において定義されています。
OnWrite コールバックは write() が呼び出された際に、OnWriteConvert はwrite() が呼び出された際に、ある種の変換を施したデータを送信するために使用するコールバックです。
それぞれのコールバックは、InPort と同様 rtm/PortCallback.h で定義されているそれぞれのファンクタの基底クラスを継承することにより実装します。
以下にそれぞれの実装例を示します。
コールバック用のファンクタの書き方は、InPort の OnRead/OnReadConvert とほぼ同じです。OnWrite を継承した MyOnWrite ファンクタでは、コンストラクタで出力ストリーム std::ostream を渡しています。 どこかでオープンしたファイル出力ストリーム std::ofstream 等を渡すことを意図しています。ファンクタの実体である operator() では、出力ストリームと標準出力に対して、文字列を出力しています。 このように、ファンクタでは、予めコンストラクタなどで状態変数を渡すことで、他のオブジェクトに対する呼び出しも実現することができます。
一方 OnReadConvert<T> を継承した MyOnReadConvert は operator()(constT&) のみを実装しています。この関数の引数には、read() を呼んだときに InPort 変数に読みだされる前のデータが渡されます。この関数内で何らかの処理を行い return で返したデータは InPort 変数に書き込まれます。 この例では、データ型に data というメンバがあり、かつ乗算演算子が定義されているという前提で自乗を計算して返しています。適切なメンバがない変数型を使用すればコンパイルエラーになります。
コネクタ・バッファのコールバック
コネクタ
コネクタはバッファおよび通信路を抽象化したオブジェクトです。図に示すように、OutPort と InPort の間に存在し、OutPort からは write() 関数によりデータの書き込み、InPort からは read() 関数によりデータの読み出しが行われます。 コネクタは、データがどのような手段で OutPort から InPort へ伝送されるかを抽象化し隠蔽します。
OutPort はコネクタ内のバッファに対して、OutPort は複数の InPort へ接続することができますが、一つの接続につき、一つのコネクタが生成されます。(実際には InPort も複数の接続を同時に持つこともできますが、データを区別する方法がないので、通常は用いません。) つまり、接続が3つあれば、コネクタが3つ存在し、それぞれに対して書き込みのステータスが存在することになります。
また、これらの機能のために、OutPort/InPort 一対に対して、それぞれ一つコネクタが存在する必要があることがわかります。 さらに、コネクタをサブスクリプション型に対応した実装レベルでモデル化するにあたり、パブリッシャと呼ばれる非同期通信のためのオブジェクトを導入しました。
データポートは接続が確立されると、1つの接続につき1つのコネクタオブジェクトを生成します。コネクタは、OutPort と InPort をつなぐデータストリームの抽象チャネルです。
ポートのコールバック
ステータス
データポートは、データの送受信を行った際に、ステータスを返します。 ステータスは、rtm/DataPortStatus.h で定義されています。
PORT_OK, PORT_ERROR, SEND_FULL, SEND_TIMEOUT, CONNECTION_LOST, UNKNOWN_ERROR
PORT_OK, PORT_ERROR, BUFFER_ERROR, BUFFER_FULL, BUFFER_TIMEOUT, UNKNOWN_ERROR
PORT_OK, PORT_ERROR, RECV_EMPTY, RECV_TIMEOUT, CONNETION_LOST, UNKNOWN_ERROR
独自データポートインターフェースの作成
独自データポートインターフェースの作成例を示します。
以下の関数、クラスの定義が必要です。
プロバイダクラス (InPortTestProvider)
データポートインターフェース(Push型)のプロバイダクラスです。 コンシューマ側で put関数を呼び出した際に、何らかの方法によりプロバイダ側にデータを転送する必要があります。 このサンプルではコンシューマ側の put関数呼び出し時にファイルにデータを書き込み、プロバイダ側でファイルからデータを読み込むことでデータの転送を行っています。
データポートインターフェース(Push型)のコンシューマクラスです。InPortProvider を継承する必要があります。 Taskクラスの継承はこのサンプル独自のものなので必須ではありません。
コンシューマクラス (InPortTestConsumer)
データポートインターフェース(Push型)のコンシューマクラスです。Push型の場合は InPort側にコンシューマ、OutPort側にプロバイダを生成します。 コンシューマ側で put関数を呼び出した際に、何らかの方法によりプロバイダ側にデータを転送する必要があります。 このサンプルではコンシューマ側の put関数呼び出し時にファイルにデータを書き込み、プロバイダ側でファイルからデータを読み込むことでデータの転送を行っています。
データポートインターフェース(Push型)のコンシューマクラスです。InPortConsumer を継承する必要があります。
プロバイダ、コンシューマ登録関数 (InPortTestInterfaceInit)
OpenRTM-aist のマネージャは「XXX.dll」というダイナミックリンクライブラリをロードした場合に「XXXInit」関数を呼び出します。 このサンプルの場合は「InPortTestInterface.dll」をロードして、以下の「InPortTestInterfaceInit」関数を呼び出します。
確認手順
1. OpenRTM-aistがインストールされている環境を用意します。
2. ビルドのために CMake設定ファイル (CMakeLists.txt) を作成します。
以下は、独自インターフェースのサンプルをビルドするための CMake設定ファイルです。
3. CMake により Visual Studio のプロジェクトファイルを生成します。
4. Visual Studio でビルドします。
ビルド後に、Debugフォルダーに InPortTestInterface.dll が生成されます。
5. rtc.conf を作成します
rtc.conf を作成して、Manager 起動時にロードするモジュールで InPortTestInterface.dll を指定します。
InPortTestInterface.dll が、RTC を実行するディレクトリーと異なる場合は、別途モジュール探索パスを設定します。
6. 上記の rtc.conf を読み込んで RTCを 起動します。
7. ポート接続時にInterface Typeに「test」を指定する。
RTSystemEditor上から Interface Type を指定できます。