データポート (基礎編)

データポートとは

データポートは主に連続的なデータをRTC間でやりとるするためのポートです。 データを他のRTCへ送信するためのデータポートをOutPort、他のRTCからデータ を受信するためのデータポートをInPortと呼びます。「InPort」、「OutPort」 をまとめて「データポート (DataPort)」と呼ぶことがあります。

dataport_ja.png
データポート (InPortとOutPort)

RTCはいろいろなプログラミング言語で記述することができます。また、RTコン ポーネントはネットワーク上に分散させることも、同じノード上に配置するこ とも、あるいは同じプロセス上に置くこともできます。そして、両端のRTCがど んな言語で記述されているか、ネットワーク的に分散しているかに関わらず、 データポート間のデータの受け渡しは透過的に行われます。

RTCは必要に応じて任意の数のデータポートを持たせることができます。例えば、 センサからデータを取得するコンポーネントを作るとします。このコンポーネ ントは少なくとも一つのセンサデータを出力するためのOutPortが必要になるで しょう。

あるいは、指定されたトルク値に従って、モータを駆動するコンポーネントを作 るとします。このコンポーネントは、少なくとも一つの一つのトルク値指令を受 け取るInPortが必要になります。これらのコンポーネントを利用して、フィー ドバック制御を行うための制御器 (コントローラ) コンポーネントを作るとす れば、センサデータを受け取るInPort、指令値 (例えば速度指令) を受け取る InPort、トルク値を出力するOutPortのそれぞれが必要になります。

dataport_example_ja.png
センサ、コントローラ、モータとデータポートの例

プログラムとして実際に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型という方法のみ利用できるようになっており、 通常はこれを利用すれば特に問題ありません。ただし、システムの構成によっ ては、別のインターフェース型を利用するように、拡張することも可能です。

cneter
インターフェース型

データフロー型

データの送受信の方法には、OutPort が InPort にデータを送る push 型のも のと、逆にInPort から OutPort に問い合わせてデータを取ってくる pull 型 のものがあります。

push 型では、OutPort側のコンポーネントの主にアクティビティ (通常は on_execute() コールバック関数) が主体となりデータを受信側に送ります。送 るタイミングは次のサブスクリプション型で指定します。一方、pull 型では、 InPort側のコンポーネントの主にアクティビティ (通常は on_execute() コー ルバック関数) が主体となりデータを受信側に送ります。データを受信するタ イミングは、InPort側がread()を読んだ時点となります。

cneter
データフロー型

サブスクリプション型

サブスクリプション型は、データフロー型が push のときにだけ有効なプロパ ティです。デフォルトでは、同期型送信方式の flush, および非同期型送信方 式の new, periodic の3種類が提供されています。

flush 型はOutPortからInPortへデータをpushするとき、OutPortのwrite関数内 で直接データの送信を行います。つまり、write()関数から戻った時には、 InPortにデータが届いていることが保証されます。一方で、相手先のInPortが ネットワーク的に遠い場所にあり、通信に時間がかかる場合には、write() で 長い時間待たされる可能性があります。したがって、例えばアクティビティの ロジックをリアルタイム実行したい場合には flush 型では問題が生じる場合が あります。

new 型と periodic 型には、publisher という送信のためのスレッドが接続毎 に用意されます。これらのタイプでは、OutPort の write() 関数を呼ぶと、デー タは一旦バッファに書きこまれ write() 関数はすぐに終了します。データの実 際の送信は、publisherの別スレッドが行います。

cneter
サブスクリプション型

new 型は書き込みと同時に送信待ちしている publisher に対してシグナルを送 り、起こされた publisher スレッドが実際のデータ送信を行います。バッファ へのデータの書き込み周期に対して、データ送信時間が十分に短ければ、 flush とほぼ同じですが、データ送信に時間がかかる場合には、必ずしもすべ てのデータが受信側に届くわけではないことに注意してください。そういった 意味で new 型はベストエフォート的なデータ送信方法です。

一方 periodic 型は、publisher が一定周期でバッファからデータを取り出し データ送信を行います。送信周期は、接続時に外部から与えることができます。 データ送信周期に比べて、データ送信時間が長い場合、送信周期が守られない 可能性があります。また、データをバッファに書き込む周期 (アクティビティ の周期) と、バッファからデータを取り出して送信する周期 (publisher の周 期)、および後述するデータ送信ポリシーの整合性を考慮しなければ、定常的に バッファフル状態またはバッファエンプティ状態を引き起こす可能性がありま す。いわゆる、生産者・消費者問題を考慮する必要がある接続タイプになりま す。

サブスクリプション型まとめ

Subscription Type 同期・非同期 概要
New 非同期通信 データポートにデータがwriteされた後、非同期でできるだけ速く送る。
基本的には到達保証はないが、インターフェース型が corba_cdr の場合TCP通信であるため、トランスポート層レベルでは到達が保証されている。他のインターフェース型については、その伝送方式による。
[ユースケース]: データ送信側がリアルタイム実行、データ受信型が外部ノードの場合はNewかPeriodicを利用する。
Periodic 非同期通信 データポートにデータがwriteされた後、非同期で周期的に送る。
基本的には到達保証はなく、間引きも可能であるため、すべてのデータが送信される保証もない。ただし、送られたデータについてはインターフェース型が corba_cdr の場合TCP通信であるため、トランスポート層レベルでは到達が保証されている。他のインターフェース型については、その伝送方式による。
[ユースケース]: データ送信側のデータ生成周期と受信側の消費周期が異なる場合にここれを利用する。
Flush 同期通信 データポートにwriteされた後、データを同期転送する。write関数から戻ると受信側にデータが届いたことが保証される。(受信側が消滅しているばあいを除く。)
[ユースケース] 複数のRTCを複合化しリアルタイム実行しており、それらのRTC間の通信は通常Flushで実行する。リモートノードに対するデータ通信でも、到達を保証したい場合はFlushを使用する。

データ送信ポリシー

サブスクリプション型が、new または periodic の場合、OutPort はバッファ を持ちます。データを送信するタイミングで、バッファに溜まっているデータ をどのような方針で送信するかをデータ送信ポリシーと呼びます。

データ送信ポリシーには、バッファに保持されているデータをすべて送信す る all、先入れ先だし方式で一つずつ送信する fifo、バッファに保持され ているデータをいくつかおきに送信する skip、そして最新値のみ送信し、そ の他のデータはすべて捨ててしまう new の四種類があります。

ポリシー名 意味
all バッファに残っているデータをすべて送信
fifo 先入れ先だし方式で、データを一つずつ送信
skip n 個おきにデータを送信し、それ以外は捨てる
new 最新値のみ送信し、古い値は捨てる

サブスクリプション型を new や periodic 等の非同期型にした場合、データの 生成速度、消費速度、さらに通信路の帯域幅を事前に見積もったうえで、これ らのポリシーを適切に設定する必要があります。

InPortプログラミング

ここからは実際のプログラムでデータポートがどのように使われるのかを見て いきます。

InPortを使う際には、以下のルールを念頭に置いたうえでプログラミングする ことを推奨します。

  • データは来ていないかもしれないとして処理する
  • データは正しくないかもしれないとして処理する
  • 配列の長さは常に変化するかもしれないとして処理する
  • データは途中から来なくなるかもしれないとして処理する

InPort に接続される OutPort は他のノードのRTCの OutPortかもしれません。 ポートは接続されていないかもしれないし、データを送ってないかもしれませ ん。配列が含まれるデータ型の場合、配列の長さは次のデータでは変化するか もしれません。また、ネットワーク接続が切れたり、相手のRTCが停止してしまっ た場合、途中からデータを送らなくなるかもしれません。

モジュール化する上で、仮定や前提条件を少なくし、他の要素に依存しないよ うに作るということは非常に重要で、これによって再利用性が高く使いやすい モジュールになるかどうかが変わってきてしまいます。

さて、InPortの実際の使い方を見ていく前に、InPortの構造を説明します。

cneter
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と比べると、単に自分がデータを送りだすだけですので、少し 簡単になります。

cneter
OutPortの構造

構造は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() 関数だけ覚えておけば十分でしょう。

最新バージョン

初めての方へ

Windows msi(インストーラ) パッケージ (サンプルの実行ができます。)

C++,Python,Java,
Toolsを含む
1.1.2-RELEASE

RTコンポーネントを開発するためには開発環境のインストールが必要です。詳細はダウンロードページ

統計

Webサイト統計
ユーザ数:1629
プロジェクト統計
RTコンポーネント286
RTミドルウエア21
ツール20
文書・仕様書1

Join our slack

Enter email address for slack invite.

旧Webサイト

OpenRTM.org旧Webサイト

OpenHRP3

動力学シミュレータ

Choreonoid

モーションエディタ/シミュレータ

OpenHRI

対話制御コンポーネント群

OpenRTP

統合開発プラットフォーム

産総研RTC集

産総研が提供するRTC集

TORK

東京オープンソースロボティクス協会

DAQ-Middleware

ネットワーク分散環境でデータ収集用ソフトウェアを容易に構築するためのソフトウェア・フレームワーク

VirCA

遠隔空間同士を接続し、実験を行うことが可能な仮想空間プラットホーム