データポートとは
データポートは主に連続的なデータをRTC間でやりとるするためのポートです。 データを他のRTCへ送信するためのデータポートをOutPort、他のRTCからデータ を受信するためのデータポートをInPortと呼びます。「InPort」、「OutPort」 をまとめて「データポート (DataPort)」と呼ぶことがあります。
RTCはいろいろなプログラミング言語で記述することができます。また、RTコン ポーネントはネットワーク上に分散させることも、同じノード上に配置するこ とも、あるいは同じプロセス上に置くこともできます。そして、両端のRTCがど んな言語で記述されているか、ネットワーク的に分散しているかに関わらず、 データポート間のデータの受け渡しは透過的に行われます。
RTCは必要に応じて任意の数のデータポートを持たせることができます。例えば、 センサからデータを取得するコンポーネントを作るとします。このコンポーネ ントは少なくとも一つのセンサデータを出力するためのOutPortが必要になるで しょう。
あるいは、指定されたトルク値に従って、モータを駆動するコンポーネントを作 るとします。このコンポーネントは、少なくとも一つの一つのトルク値指令を受 け取るInPortが必要になります。これらのコンポーネントを利用して、フィー ドバック制御を行うための制御器 (コントローラ) コンポーネントを作るとす れば、センサデータを受け取るInPort、指令値 (例えば速度指令) を受け取る InPort、トルク値を出力するOutPortのそれぞれが必要になります。
プログラムとして実際にInPortとOutPortを利用する簡単な例を見てみます。各 オブジェクトはそれぞれ以下の働きをします。
- encoderDevice: ハードウエア (例えばカウンタボード等) を制御してエンコーダから現在の角度を読み取るための機能が実装されたオブジェクト。ハードウエアベンダがそうしたライブラリ等を提供していなければ、自分で実装する必要がある。
- encoderData: OutPort 用にエンコーダのデータを保持する変数。ここでは、エンコーダの値をを保持する data というフィールド (構造体のメンバ) を持っているものとする。
- encoderDataOut: OutPort オブジェクト。encoderData オブジェクトに関連付けられている。
// エンコーダコンポーネントの例 encoderData.data = encoderDevice.read(); // カウンタから現在値を取得 encoderDataOut.write(); // OutPort からデータが出ていく
1行目では、encoderDevice オブジェクトの read() 関数を呼んで、エンコーダ の現在値を読み込んでいます。読み込まれたデータは、encoderData オブジェ クトの data メンバーに代入されます。OutPort のインスタンスである encoderDataOut オブジェクトは、write() が呼ばれると、encoderData オブジェ クトからデータを取り出し、接続されている InPort へデータを出力します。
一方、InPortを持つモータコンポーネントは、以下のように書けます。
- motorDevice: ハードウエア(例えばモータドライバに接続されたDAボード等)を制御してモータ制御をするためのオブジェクト。ベンダからそうしたライブラリが提供されていなければ、自分で実装する必要がある。
- motorData: InPort から入力された値を保持する変数。ここでは、エンコーダの値をを保持する data というフィールド(構造体のメンバ)を持っているものとする。
- motorDataIn: InPort オブジェクト。motorData オブジェクトに関連付けられている。
// モータコンポーネントの例
if (motorDataIn.isNew() {
motorData.data = motorDataIn.read(); // InPortからデータを読む
motorDevice.output(motorData.data); // モータドライバへ指令値を出力
}1行目ではまずInPortにデータが来ているかどうか確かめています。データが到 着していれば、motorDataIn の read() 関数を呼んで、InPortからデータを motorData の data メンバーに読み込んでいます。次に、実際にモータに指令 値を渡すため、motorDevice オブジェクトの output 関数を呼び出しています。
同様に、InPortとOutPortを持つ制御器コンポーネントでは以下のようになるで しょう。
// 制御器コンポーネントの例
if (positionDataIn.isNew() && referenceDataIn.isNew()) {
positionDataIn.read(); // 位置データをInPortから読み込む
referenceDataIn.read(); // 速度指令をInPortから読み込む
// 制御アルゴリズムに従ってモータに与えるトルク値を計算
torqueData.data = controller.calculate(positionData.data,
referenceaData.data);
torqueDataOut.write(); // モータトルク値をOutPortから出力
}行っていることは、それぞれInPort、OutPortだけの場合とそれほど変わりませ んので、詳しい説明は省略します。相手のRTCがどの言語で書かれているか、あ るいは、ネットワーク上の別のノード上にあるのかローカルにあるのか等の違 いについては、RTコンポーネントフレームワークにより隠蔽されているので、 このように簡単にデータの送受信を行うことができます。
変数の型
ここまでの例では、各オブジェクトの宣言が示されていないので、C++やJava等 型のある言語に慣れている方は、サンプルプログラムの各変数がどのような型 なのか気になったかもしれません。
基本型
上の例のデータ格納変数で想定していたのは、TimedDouble というデータ型 です。C/C++の構造体で書くと、ほぼ以下のような構造体と同等のものです。
struct Time
{
long int sec;
long int usec;
};
struct TimedDouble
{
Time tm;
double data;
};データポートの型に関しては、以下のような決まりや特徴があります。
- データポートにはそれぞれ特有の型がある。
- 型の定義はIDL(Interface Definition Language)という言語非依存のインターフェース定義言語によって定められている。
- 言語が異なっても、IDL定義の型が同じなら接続できる。
- 型の異なるデータポート同士は接続できず、データの送受信は行えない。
従って、上記の例で、エンコーダ、制御器、モータの各コンポーネントを接続 するためには、ポートのデータ型がそれぞれ TimedDouble 型でなければなりま せん。
なお、OpenRTM-aist では、デフォルトで以下のようなデータポート型を用意し ており、特に定義することなく利用することができます。これらのデフォルト 定義の基本型にはタイムスタンプ保持用に tm フィールドが用意されています。
| 型名 | 内容 |
| TimedShort | タイムスタンプと short int 型 |
| TimedUShort | タイムスタンプと unsigned short int 型 |
| TimedLong | タイムスタンプと long int 型 |
| TimedULong | タイムスタンプと unsigned long int 型 |
| TimedFloat | タイムスタンプと float 型 |
| TimedDouble | タイムスタンプと double 型 |
| TimedString | タイムスタンプと string 型 |
| TimedWString | タイムスタンプと wstring 型 |
| TimedChar | タイムスタンプと char 型 |
| TimedWChar | タイムスタンプと wchar 型 |
| TimedOctet | タイムスタンプと バイト 型 |
| TimedBool | タイムスタンプと bool 型 |
これらのうち、TimedChar, TimedWChar, TimedOctet はあまり使用する場面は ないかもしれません。
IDL型から各言語固有の方への対応関係をマッピングといいます。それぞれの型 から各言語上の型へのマッピングはCORBAの言語マッピング仕様書または「言語 マッピング」の章を参照してください。
少し複雑なデータ型
上記の基本型には、~Seq というシーケンス型と呼ばれる型が用意されています。 これは簡単にいえば配列を保持できる型です。
seqdata.length(10); // 配列を10個分確保する
for (int i(0); i < seqdata.length(); ++i) // 引数なし length は長さを返す
{
seqdata[i] = i; // 代入する
}C++ではこのように利用することができます。配列よりは便利で、STLのvector に似ていますが、vectorよりはだいぶ低機能です。Javaでは配列専用のホルダー クラスが自動的に生成されこれを利用することができます。また、Pythonでは、 Pythonの配列に直接マッピングされます。
先ほどの例では、エンコーダとモータは一つでしたが、実際のロボットでは多 くの自由度を扱う必要があります。その時に、各自由度ごとにポートを設ける のは、通信効率、同期の問題などから得策ではありません。そのような場合で は、こうしたシーケンス型を利用することで、複数のデータを効率的に扱うこ とができます。
独自のデータ型
さらに、もっと複雑なデータ構造を扱いたい場合もあります。その場合は、自 分でデータ型を定義して、データポートで利用することもできます。詳細は 「データポート(応用編)」を参照してください。
データポートの接続
コネクタ
RTCが持つInPortとOutPortを接続するには、RTSystemEditor や rtcshell など のツールを使用します。ポートを接続するとOutPortから送信されたデータは、 ネットワーク等を経由してInPortによって受信されます。接続は、システムの 構造やコンポーネントの特性に応じて、以下のようにいくつかの種類を選択す ることができます。
- データフロー型
- インターフェース型
- サブスクリプション型
- データ送信ポリシー
インターフェース型
インターフェース型では、データをどのプロトコルで送受信するかを指定しま す。デフォルトでは、corba_cdr型という方法のみ利用できるようになっており、 通常はこれを利用すれば特に問題ありません。ただし、システムの構成によっ ては、別のインターフェース型を利用するように、拡張することも可能です。
データフロー型
データの送受信の方法には、OutPort が InPort にデータを送る push 型のも のと、逆にInPort から OutPort に問い合わせてデータを取ってくる pull 型 のものがあります。
push 型では、OutPort側のコンポーネントの主にアクティビティ (通常は on_execute() コールバック関数) が主体となりデータを受信側に送ります。送 るタイミングは次のサブスクリプション型で指定します。一方、pull 型では、 InPort側のコンポーネントの主にアクティビティ (通常は on_execute() コー ルバック関数) が主体となりデータを受信側に送ります。データを受信するタ イミングは、InPort側がread()を読んだ時点となります。
サブスクリプション型
サブスクリプション型は、データフロー型が push のときにだけ有効なプロパ ティです。デフォルトでは、同期型送信方式の flush, および非同期型送信方 式の new, periodic の3種類が提供されています。
flush 型はOutPortからInPortへデータをpushするとき、OutPortのwrite関数内 で直接データの送信を行います。つまり、write()関数から戻った時には、 InPortにデータが届いていることが保証されます。一方で、相手先のInPortが ネットワーク的に遠い場所にあり、通信に時間がかかる場合には、write() で 長い時間待たされる可能性があります。したがって、例えばアクティビティの ロジックをリアルタイム実行したい場合には flush 型では問題が生じる場合が あります。
new 型と periodic 型には、publisher という送信のためのスレッドが接続毎 に用意されます。これらのタイプでは、OutPort の write() 関数を呼ぶと、デー タは一旦バッファに書きこまれ write() 関数はすぐに終了します。データの実 際の送信は、publisherの別スレッドが行います。
new 型は書き込みと同時に送信待ちしている publisher に対してシグナルを送 り、起こされた publisher スレッドが実際のデータ送信を行います。バッファ へのデータの書き込み周期に対して、データ送信時間が十分に短ければ、 flush とほぼ同じですが、データ送信に時間がかかる場合には、必ずしもすべ てのデータが受信側に届くわけではないことに注意してください。そういった 意味で new 型はベストエフォート的なデータ送信方法です。
一方 periodic 型は、publisher が一定周期でバッファからデータを取り出し データ送信を行います。送信周期は、接続時に外部から与えることができます。 データ送信周期に比べて、データ送信時間が長い場合、送信周期が守られない 可能性があります。また、データをバッファに書き込む周期 (アクティビティ の周期) と、バッファからデータを取り出して送信する周期 (publisher の周 期)、および後述するデータ送信ポリシーの整合性を考慮しなければ、定常的に バッファフル状態またはバッファエンプティ状態を引き起こす可能性がありま す。いわゆる、生産者・消費者問題を考慮する必要がある接続タイプになりま す。
データ送信ポリシー
サブスクリプション型が、new または periodic の場合、OutPort はバッファ を持ちます。データを送信するタイミングで、バッファに溜まっているデータ をどのような方針で送信するかをデータ送信ポリシーと呼びます。
データ送信ポリシーには、バッファに保持されているデータをすべて送信す る all、先入れ先だし方式で一つずつ送信する fifo、バッファに保持され ているデータをいくつかおきに送信する skip、そして最新値のみ送信し、そ の他のデータはすべて捨ててしまう new の四種類があります。
| ポリシー名 | 意味 |
| all | バッファに残っているデータをすべて送信 |
| fifo | 先入れ先だし方式で、データを一つずつ送信 |
| skip | n 個おきにデータを送信し、それ以外は捨てる |
| new | 最新値のみ送信し、古い値は捨てる |
サブスクリプション型を new や periodic 等の非同期型にした場合、データの 生成速度、消費速度、さらに通信路の帯域幅を事前に見積もったうえで、これ らのポリシーを適切に設定する必要があります。
InPortプログラミング
ここからは実際のプログラムでデータポートがどのように使われるのかを見て いきます。
InPortを使う際には、以下のルールを念頭に置いたうえでプログラミングする ことを推奨します。
- データは来ていないかもしれないとして処理する
- データは正しくないかもしれないとして処理する
- 配列の長さは常に変化するかもしれないとして処理する
- データは途中から来なくなるかもしれないとして処理する
InPort に接続される OutPort は他のノードのRTCの OutPortかもしれません。 ポートは接続されていないかもしれないし、データを送ってないかもしれませ ん。配列が含まれるデータ型の場合、配列の長さは次のデータでは変化するか もしれません。また、ネットワーク接続が切れたり、相手のRTCが停止してしまっ た場合、途中からデータを送らなくなるかもしれません。
モジュール化する上で、仮定や前提条件を少なくし、他の要素に依存しないよ うに作るということは非常に重要で、これによって再利用性が高く使いやすい モジュールになるかどうかが変わってきてしまいます。
さて、InPortの実際の使い方を見ていく前に、InPortの構造を説明します。
InPort の実体はオブジェクトです。C++では、クラステンプレート InPort<T> 型として定義されています。Tにはデータポートが使用するデータ型が入ります。 下の例は、サンプルに付属しているConsoleOutコンポーネントのInPort宣言の 例です。InPort が TimedLong 型で宣言されているのがわかります。
TimedLong m_in; InPort<TimedLong> m_inIn;
宣言や初期化は、RTCBuilderやrtc-templateを使っていれば自動的に記述して くれます。InPortを使用する際には、InPortオブジェクトに結び付けられた T 型の変数が一つ定義されます。先ほどの例で、TimedLong 型の m_in というも のがその変数です。これをInPort変数と呼びます。
InPort と InPort変数は初期化時に関連付けられ、InPortのデータ読み出し関 数 read() を呼ぶと、InPortが持つバッファからデータが一つ読みだされ InPort 変数にコピーされます。InPort にやってきたデータを使用する際には このように InPort 変数を介して利用します。
InPortオブジェクト
InPort クラステンプレートで定義されている関数を以下の表に示します。
これはC++のInPortクラスの関数ですが、他の言語においてもほぼ同一の名前で 各関数が提供されています。なお、これらの関数のリファレンスマニュアルは、 Windows では、「スタート」->「OpenRTM-aist」->「C++」->「documents」-> 「Class reference」から見ることができます。Linux 等ではドキュメントがイ ンストールされていれば、 ${prefix}/share/OpenRTM-aist/docs/ClassReference 等からアクセスすること ができます。マニュアルはdoxygen形式で記述されており、上部メニューの「ネー ムスペース」からクラス一覧を表示させ、InPortを参照してください。
| InPort (const char *name, DataType &value) | コンストラクタ |
| ~InPort (void) | デストラクタ |
| const char * name () | ポート名称を取得する。 |
| bool isNew () | 最新データが存在するか確認する |
| bool isEmpty () | バッファが空かどうか確認する |
| bool read () | DataPort から値を読み出す |
| void update () | バインドされた T 型の変数に InPort バッファの最新値を読み込む |
| void operator>> (DataType &rhs) | T 型のデータへ InPort の最新値データを読み込む |
| void setOnRead (OnRead< DataType > *on_read) | InPort バッファへデータ読み込み時のコールバックの設定 |
| void setOnReadConvert (OnReadConvert< DataType > *on_rconvert) | InPort バッファへデータ読み出し時のコールバックの設定 |
主に使用する関数は、isNew() および read() 関数となります。実際に使われ ている例を見てみます。
RTC::ReturnCode_t ConsoleOut::onExecute(RTC::UniqueId ec_id)
{
if (m_inIn.isNew())
{
m_inIn.read();
std::cout << "Received: " << m_in.data << std::endl;
std::cout << "TimeStamp: " << m_in.tm.sec << "[s] ";
std::cout << m_in.tm.nsec << "[ns]" << std::endl;
}
return RTC::RTC_OK;
}m_inIn.isNew() でデータが来ているかを確認し、m_inIn.read() でInPort変数 m_in にデータを読み込んでいます。その後、m_inの内容を cout で表示してい ます。
通常は、この例のように InPort のデータの処理は、onExecute() 関数内で行 い、InPortにやってくるデータを周期的に処理するようにプログラムします。
他の関数は説明からすぐにわかると思いますが、コールバックオブジェクトセッ トする関数 setOnRead と setOnRedConvert については、応用編で改めて説明 します。
OutPort
OutPortはInPortと比べると、単に自分がデータを送りだすだけですので、少し 簡単になります。
構造はInPortとほぼ同じで、C++であれば、OutPortは T型の型引数をとるクラ ステンプレートになっています。TはOutPortのデータ型で、このTが同じ InPortに対してしかデータを送ることはできません。OutPortもInPort同様、 OutPort変数と一緒に利用します。OutPort変数にデータを書き込んだ後、 OutPortの write() 関数を呼ぶとデータがOutPortから接続されているInPortへ 送り出されます。
OutPortオブジェクト
OutPort クラステンプレートで定義されている関数を以下の表に示します。
OutPort についても InPort 同様、他の言語においてもほぼ同一の名前で各関 数が提供されています。リファレンスマニュアルについても InPort 同様、 doxygen の「ネームスペース」からOutPortを見てください。
| OutPort (const char *name, DataType &value) | コンストラクタ |
| ~OutPort (void) | デストラクタ |
| bool write (DataType &value) | データ書き込み |
| bool write () | データ書き込み |
| bool operator<< (DataType &value) | データ書き込み |
| DataPortStatus::Enum getStatus (int index) | 特定のコネクタへの書き込みステータスを得る |
| DataPortStatusList getStatusList () | 特定のコネクタへの書き込みステータスリストを得る |
| void setOnWrite (OnWrite< DataType > *on_write) | OnWrite コールバックの設定 |
| void setOnWriteConvert (OnWriteConvert< DataType > *on_wconvert) | OnWriteConvert コールバックの設定 |
OutPortで主に使用する関数は write() と getStatusList() になります。
RTC::ReturnCode_t ConsoleIn::onExecute(RTC::UniqueId ec_id)
{
std::cout << "Please input number: ";
std::cin >> m_out.data;
if (!m_outOut.write())
{
DataPortStatusList stat = m_outOut.getStatusList();
for (size_t i(0), len(stat.size()); i < len; ++i)
{
if (stat[i] != PORT_OK)
{
std::cout << "Error in connector number " << i << std::endl;
}
}
}
return RTC::RTC_OK;
}まず、std::cin >> m_out.data で標準入力から OutPort へデータを代入しま す。その後、m_outOut.write() でデータをOutPortから送り出しています。戻 り値が false の場合、ポートのステータスを調べてどの接続でエラーが起きて いるのかを表示しています。
データポートのまとめ
ここでは、データポート (InPort, OutPort) の基本的な概念と使い方について 解説しました。データポートの宣言は、RTCBuilderやrtc-templateで行ってく れますが、実際にどのようにデータを与えるのか、あるいは利用するのかにつ いてはコンポーネント開発者が記述する必要があります。ただし、簡単に使用 するだけであれば、InPortでは、isNew() と read() 関数だけ、OutPort では、 write() と getStatusList() 関数だけ覚えておけば十分でしょう。









