[初心者向け] RTミドルウェア・ROS利用におけるコーディングについて

こんにちは.ysugaです.

RTMコンテスト作品を見て,やっぱりダメだ,と思ってこの記事を書いている所存です.
ROSのノードのほうも辟易していたので,ROSノードやRTコンポーネントのコードの書き方についていくつかノウハウをここにまとめときます.
ROSノード,RTコンポーネント (RTC) 双方に言えることなので,これらを「RTモジュール」ないしは単にモジュールと呼ぶことにします.
モジュールのコードと言ったら、RTMであればRTCBuilderが生成するスケルトンコードです。

1. コードの品質を上げる最初の一歩

1.1 コアロジックをRTモジュールのコードに書かない

コアロジックをモジュールのコードに直接書いている人が多すぎる.
ROSノードだとmain関数にロジックを書き下したり,コールバック関数に直接ゴリゴリ書いている人がいる.
RTCだとonExecuteやonActivateにだ.
ものすごく読みにくい.
いや,僕も古いコードはそうなってるの多いけどね,でもそれは悪い例.
後述するけど,コアロジックと呼ばれる,要するにロボットを制御するためのコードはモジュールのコードと分けて置く方がいい.
この理由は以下の通り.
1. コードの読みやすさ
2. コードレベルでの再利用性の担保

コードを読むときに,いきなり

output = Kp * d + Ki * di + Kd + dd;

とかまあ書かれてても読むけど,

output = PIDCalculator.calculateOutput(d);

と書かれている方が読みやすい.ちょっとわかりにくい例ですが・・・
関数の名前が説明的であれば,main関数やonExecuteを読んで,ざっくりと全体でどういうことをしているのかがわかるから,本当に変更したい場所だけ深く掘り下げていけばよくなる.
これで,PIDCalculator.cppをまた読まないといけないから面倒だ,と思う人は規模の大きなコードを書くことに慣れていないんじゃないかな.
ロボット屋でもソフト大事になってるこの時代,複数のコードをまたいでどんどん読んでいくのは当たり前の事だから慣れましょう.

それから、RTCのコードをROSに、ROSのコードをRTCに、なんてことが起こります。ROSはROS2になりますし、その時ROSのもともとのコードのままでいいや、なんて思ってたら不憫なことになりますから、とにかく今から書くなら移植しやすいコードにしましょう。

コードの追加の仕方がわからないかもしれませんね、後述の具体例まで読んでいってください。

1.2 グローバル変数を使うな

これは特にRTMに言えることです。ROSのノードだとシングルプロセス前提ですから大きな問題にはなりませんが、RTCはマルチスレッドモデルで、複数のRTCが同じメモリ空間を共有します。つまりグローバル変数を使うと、あなたのRTCが複数同時に起動された場合はグローバルに置いた変数を複数のRTCが共有するので,場合によってはまずいことになるわけです.

幸い、RTCはオブジェクト指向なモデルで、RTCのクラスにメンバ変数を追加できますから、ここにデータを保管しておくのが上策です。
ROSもオブジェクト指向的に書くことができますから、同じ方法が取れます。

またグローバル変数はコードの可読性を下げます。有効なグローバル変数の利用法もあるにはあるのですが、はっきりいってそんなケースに当たるのはレアなので。

1.3 変数名にこだわろう

r = d / 2
とあるより、
radius = diameter / 2
とあるほうが分かりやすいというのはわかってもらえると思います。
無論、この程度の式ならばrやdのままで良いがもしれないのですが、長く連なってくると辛くなってきます。タイプ量が増えますが、変数名はこだわったほうがいいと考えます。

一方で、長すぎるのも面倒なこともあります。
robotWheelRadius = robotWheelDiameter / 2
なんてのも冗長かもしれません。他にradius(半径)がホイールの半径ありえない状況なら読んでる人は察することができると思います。

また、関数の引数リストなどでも、
double calcDiameterFromRadius(const double r)
のように、引数の名前が頭文字だけでも察することができる関数名にしておけば可読性は高いままです。若干、極端な例ですが、コードを読む側もバカではないので、変数名くらいは少しくらいサボっても構いません、ということです。

本や論文に書いてある数式をそのまま変数名にしてしまう例も多々あります。悪くはないんですが、コード内にどの本の何ページの式なんたらから引用と書くべきだし、書く変数の宣言部分でコメントを追加しておくべきです。
alphaやbetaは普通は何も表していません。
ただ、工業の世界ではμやθのように文字が通常使われる文脈があって、意味がわかってしまうことがあります。でも、コードを読む人は必ずしもその文脈に一致していないことを意識しておくべきです。

2. 具体例 [初級編]

2.1 グローバル変数のメンバ変数化

グローバル変数をメンバ変数にする,と聞いてピンとこないのは,プログラムのプの字までしかわかっていない証拠です.
世の中はオブジェクト指向的に書かれているものがほとんどですから,オブジェクト指向プログラミング,クラス・オブジェクトの考え方について触れておくべきです.
そういう人はこのまま読み進めてください.まあ,この文の指摘がドキッとする人は,やっぱりわかってない人だから,下の例は読んで見たほうがいいと思いますが.

2.1.1 そもそもクラスとは

そもそもクラスという概念がわからない人が多いかもしれません。クラスとはデータと機能の集まりであり、ある属性を持った集団の特徴を定義しているものです。定義なのですこし固い言い回しになってしまいましたね。例を挙げましょう。

例えば、台車型の移動ロボット「あゆむくん」を考えましょう.
二つのモータを使った二輪対向型ロボット台車を考えます.
二輪型のロボットだと,正回転方向を逆にすると便利です.右モーターと左モーターに同じ電圧を与えると,進行方向に移動速度を発生する方向に回転するモーターと,その逆に回転するモーターが出てくることになります.これを事前に設定できるようにしましょう.回転方向はsetRotDir関数で設定できるとします.
ロボットはモータドライバ回路に対してグローバルなsetPWM関数でPWMのデューティー比を与えます.
また,車輪にはタコジェネレータが付いており,回転速度がグローバルな関数getVoltageで取ってくることができ,VOLT2OMEGAという係数を掛けることでrad/secの値に出力できます.

これでロボットが二つのモーターを持った構造であることがわかります.図にすると下図のような感じです.

この左モーター・右モーターというのが「オブジェクト」にあたります.
オブジェクトを考えるからオブジェクト指向と呼ばれています.

ここで賢い人なら,左モーターと右モーターが同じ様な機能を持っていると気づくと思います.これらをまとめてグループ化しましょう.
グループ化するのに「クラス」を使います.右モーター・左モーターは「モーター」というグループに属しています.
同様に「あゆむくん」にも「二輪対向型移動ロボット」というクラスに入ってもらいましょう.これがクラスです.同じような特徴があるオブジェクトをグループ化します.

では,クラスの実際について考えていきましょう.
次にモーターというクラスのオブジェクトの特徴について考えます.
モーターには回転方向設定機能がある,とありますから,モータークラスは「逆回転」という属性値を持っていることにします.これは真偽型 (boolean) としましょう.真 [true] であれば,回転方向を逆とするとします.
また,モーターに対してPWM出力を設定することができます.この機能も追加しましょう.命令時は-1から1の値を受け取り,負の値を受け取ると回転方向が逆になるようにしましょう.
また,モーターの回転速度をrad/secで受け取ることができる様にしましょう.

これがモーターというクラスです.

これはUMLというオブジェクト指向のプログラムの設計図表記法のうち,クラス図というものです.
クラスを表す箱の中段の「逆回転」がクラスの属性になります.そして下段がクラスの機能 (ふるまい) にあたります.
(先ほどのオブジェクトを表している図は,コミュニケーション図という図です.一部の機能しか使っていませんが.)

ここで,最初の文章に戻ると,ロボットの仕様を決める文章のうち,名詞をクラスや属性として,動詞 (・・・する) をクラスのふるまいとしているのがわかります.
これが基本的な「オブジェクト指向モデリング」の戦略です.なんとなく覚えておいてください.

さて,クラス図は日本語で作成してしまったのですが,これをC++言語やJava言語に変更するのに,英語に直しておきましょう.VBで書く人はそのままでもいいですけど.

そしてこれを言語に落とし込みます.C++, Python, Javaでごらんください.





#include <float.h> // for fabs
#include <hardware_access.h> // This is for setRotateDir/setDuty/getVoltage/VOLT2OMEGA
 
class Motor {
  private:
    bool m_CCW;
  public:
    Motor(): m_CCW(false) {}
 
    ~Motor() {}
 
    void setCCW(const bool CCW) { m_CCW = CCW; }    
 
    void setRotateDirection(const bool CW) {
      if ((CW &amp;&amp; !m_CCW) || (!CW &amp;&amp; m_CCW)) {
        ::setRotateDir(1); // CW
      } else {
        ::setRotateDir(0); // CCW
    }
 
    void setPWMDuty(const double ratio) {
      if (ratio &lt; 0) { setRotateDirection(false); // CCW }
      else { setRotateDirection(true); // CW }
      ::setDuty( fabs(ratio) );
    }
 
    double getRotateVelocity() {
      double voltage = ::getVoltage(); // Voltage of PotentioMeter
      return voltage * VOLT2OMEGA;
    }
};
import math
import hardware_access # This is for setRotateDir/setDuty/getVoltage/VOLT2OMEGA
 
class Motor:
  def __init__(self):
    self._CCW = false
    pass
 
  def setCCW(self, ccw):
    self._CCW= ccw
    pass
 
  def setRotateDirection(self, cw):
    if (cw and (not self._CCW)) or ((not cw) and self._CCW): # EXOR
      hardware_access.setRotateDir(1) # CW
    else:
      hardware_access.setRotateDir(0) # CCW
    pass
 
  def setPWMDuty(self, ratio):
    if ratio &lt; 0: 
      self.setRotateDirection(false) # CCW
    else:
      self.setRotateDirection(true) # CW
    hardware_access.setDuty( math.abs(ratio) )
    pass
 
  def getRotateVelocity(self):
    voltage_ = hardware_access.getVoltage() # Voltage of PotentioMeter
    return voltage_ * hardware_access.VOLT2OMEGA
import Math;
import HardwareAccess;
 
class Motor {
  private boolean m_CCW;
 
  public Motor() {
    m_CCW = false;
  }
 
  public void setCCW(boolean CCW) { 
    m_CCW = CCW;
  }    
 
  public void setRotateDirection(boolean CW) {
    if ((CW &amp;&amp; !m_CCW) || (!CW &amp;&amp; m_CCW)) { // EXOR
      HardwareAccess.setRotateDir(1); // CW
    } else {
      HardwareAccess.setRotateDir(0); // CCW
  }
 
  public void setPWMDuty(const double ratio) {
    if (ratio &lt; 0) { 
      HardwareAccess.setRotateDirection(false); // CCW 
    }
    else { 
      setRotateDirection(true); // CW 
    }
    HardwareAccess.setDuty( Math.abs(ratio) );
  }
 
  public double getRotateVelocity() {
    double voltage = HardwareAccess.getVoltage(); // Voltage of PotentioMeter
    return voltage * HardwareAccess.VOLT2OMEGA;
  }
};

動作確認してないコードなのですが・・・まあこういうことです.全くわからない人は,どれかの言語を先に学んでもらうのがいいでしょう.
入門書の例題をなぞって見てからこの文を読み返して見てください.
(ちなみに,astahのprofessionalバージョンを僕は使ってこの図を書いているのですが,このソフトウェアには先ほどの図からソースコードを一部自動生成する機能があります.図的に考えて誰かと議論しながら完成度を上げ,その後ソースコードを吐き出し,関数の中身を書いていく,というのがこの世界の定石です)

では,二輪対向型ロボットのクラスは以下のようになります.


TwoWheelMobileRobotクラスは,MotorクラスとrightMotorおよびleftMotorという関連を持っています.これは属性を持っているのと同じことだと考えてください.だから,このように書いたほうがいいかもしれません.


ここで,TwoWheelMobileRobotは,moveForwardとrotateの機能を提供します.moveForwardは引数としてratioを受け取りますので,これをそのままMotorに渡せばどちらも正方向に回転してロボットは全身,rotateの場合はどちらか符号を反対にして,その場で回転することにしましょう.ではコードです.





#include "motor.h"
 
class TwoWheelMobileRobot {
  private:
    Motor m_RightMotor;
    Motor m_LeftMotor;
  public:
    TwoWheelMobileRobot() {
      m_LeftMotor.setCCW(true);
    }
 
    ~TwoWheelMobileRobot() {}
 
    void moveForwared(const double ratio) {
      m_RightMotor.setPWMDuty(ratio);
      m_LeftMotor.setPWMDuty(ratio);
    }
 
    void rotate(const double ratio) {
      m_RightMotor.setPWMDuty(ratio);
      m_LeftMotor.setPWMDuty(-ratio);
    }
};
import motor
 
class TwoWheelMobileRobot:
  def __init__(self):
    self._rightMotor = motor.Motor()
    self._leftMotor = motor.Motor()
    self._leftMotor.setCCW(True)
    pass
 
  def moveForward(self, ratio):
    self._rigthMotor.setPWMDuty(ratio)
    self._leftMotor.setPWMDuty(ratio)
 
  def rotate(self, ratio):
    self._rightMotor.setPWMDuty(ratio)
    self._leftMotor.setPWMDuty(-ratio)
import Motor;
 
class TwoWheelMobileRobot {
  private Motor m_RightMotor;
  private Motor m_LeftMotor;
  public TwoWheelMobileRobot() {
    m_LeftMotor.setCCW(true);
  }
 
  public void moveForwared(const double ratio) {
    m_RightMotor.setPWMDuty(ratio);
    m_LeftMotor.setPWMDuty(ratio);
  }
 
  public void rotate(const double ratio) {
    m_RightMotor.setPWMDuty(ratio);
    m_LeftMotor.setPWMDuty(-ratio);
  }
}

これ,左右のモーターの識別などがおろそかになっていますので,このままでは論理的におかしいのですが,たとえばMotorクラスにchannelなどの属性を加えて,クラス内のsetPWMDuty関数などで使っているhardware_accessパッケージのsetDuty関数に,そのchannelの値を渡すようにすれば良さそうですね.channelの値はコンストラクタで与えるのが良さそうですが,setChannelなどの関数を作ってもいいでしょう.ここから先はもう少し深い議論になりますが,まずはモデリングと,コードへの落とし込みについて体感しておいてください.

main関数からはこのように使いましょう.





#include "twowheelrobot.h"
#include <windows.h> // For Sleep
int main(void) {
  auto robot = new TwoWheelMobileRobot();
 
  robot->moveForward(1.0);
  Sleep(1000); // [ms]
  robot->rotate(1.0);
  Sleep(1000); // [ms]
  robot->moveForward(0.0); // Stop
  return 0;
}
import twowheelmobilerobot
import time
def main():
  robot = TwoWheelMobileRobot()
  robot.moveForward(1.0)
  time.sleep(1.0)
  robot.rotate(1.0)
  time.sleep(1.0)
  robot.moveForward(0.0)
  pass
 
if __name__ == '__main__':
  main()
import TwoWheelMobileRobot;
import Thread;
 
class Main {
 
  public static void main(String[] args) {
    try {
      TwoWheelMobileRobot robot = new TwoWheelMobileRobot();
      robot.moveForward(1.0);
      Thread.sleep(1000); // [ms]
      robot.rotate(1.0);
      Thread.sleep(1000); // [ms]
      robot.moveForward(0.0);
    } catch (InterruptedException ex) {
      System.out.println("Exception: " + ex);
    }
  }
}

すると,main関数を見て,ロボットを1.0秒前進させて,1.0秒回転させるプログラムだ,ということがわかるわけです.
こういうプログラムは全体として見通しをよく作ることができ,また,TwoWheelMobileRobotクラスやMotorクラスの機能を再利用して,
プログラムの流れをRobotの回転を1.5秒にしよう,とか改造できたり,モーターを増やす,などができるようになるわけです.

いかがでしょう.ここから先はまた別の機会に.

2.2 まずグローバル変数を使わない

さて,クラスのことがわかっていれば,RTCもクラスですから怖くありません.また,ここではROSのノードをオブジェクト指向的に書くことについても紹介しましょう.

2.2.1 RTミドルウェアの場合

では,TimedDouble型の入力データポート「in」を1つ,TimedDouble型の出力データポート「out」を一つ持つRTCを考えましょう.このコンポーネントは,連続して受け取るデータの差分を計算して出力するRTCとします.この時,RTCは,onExecuteでinデータポートから受け取ったデータを,「前回のonExecuteでinデータポートから受け取ったデータ」との差分を計算してoutデータポートに出力します.
そうです.「前回のonExecuteでinデータポートから受け取ったデータ」を保存しておかなければいけません.

さあ,こういうRTCを書くときに皆さんならどうするでしょう.
この記事で「まずい」と言っている例はこれです.





#include <rtm/basicdatatypeskel.h>
#include <rtm/manager.h>
#include <rtm/dataflowcomponentbase.h>
#include <rtm/corbaport.h>
#include <rtm/datainport.h>
#include <rtm/dataoutport.h>
 
using namespace RTC;
 
boolean initialized; // Global Variable
double old_data; // Global Variable
 
class TestRTC
  : public RTC::DataFlowComponentBase
{
public:
  TestRTC(RTC::Manager* manager): 
    m_inIn("in", m_in), m_outOut("out", m_out) {}
  virtual ~TestRTC() {}
  virtual RTC::ReturnCode_t onInitialize() {省略}
  virtual RTC::ReturnCode_t onActivated(RTC::UniqueId ec_id) {
    initialized = false;
    return RTC::RTC_OK;
  }
 
  virtual RTC::ReturnCode_t onExecute(RTC::UniqueId ec_id) {
    if (m_inIn.isNew()) { // データが来ていれば
      m_inIn.read();      // 読み込んで
      if (initialized) {  // もし最初のデータが来ていればold_dataが使えるので
        m_out.data = m_in.data - old_data; // 差分を計算する
        setTimestamp<rtc::timeddouble>(m_out);
        m_outOut.write();   // 出力する
      }
      old_data = m_in.data; // 受信したデータをold_dataに保存しておく
      initialized = true;   // 初期化されましたよーというフラグを立てる
    }
    return RTC::RTC_OK;
  }
private:
  RTC::TimedDouble m_in;
  RTC::InPort<rtc::timeddouble> m_inIn;
  RTC::TimedDouble m_out;
  RTC::InPort<rtc::timeddouble> m_inIn;
};


Pythonの場合,わざわざグローバル宣言までしなくちゃなりませんが・・・

import sys
import time
sys.path.append(".")
 
import RTC
import OpenRTM_aist
 
・・・省略・・・
 
initialized = False # グローバル変数
old_data = 0 # グローバル変数
 
class TestRTC(OpenRTM_aist.DataFlowComponentBase):
 
  def __init__(self, manager):
    OpenRTM_aist.DataFlowComponentBase.__init__(self, manager)
 
    self._d_in = RTC.TimedLong(RTC.Time(0, 0), 0)
    self._inIn = OpenRTM_aist.InPort("in", self._d_in)
    self._d_out = RTC.TimedLong(RTC.Time(0, 0), 0)
    self._outOut = OpenRTM_aist.OutPort("out", self._d_out)
 
  def onInitialize(self):
    self.addInPort("in", self._inIn)
    self.addOutPort("out", self._outOut)
    return RTC.RTC_OK
 
  def onActivated(self, ec_id):
    initialized = False
    return RTC.RTC_OK
 
  def onExecute(self, ec_id):
    global old_data, initialized # これからグローバル変数つかうよー宣言
    if m_inIn.isNew():           # データが来ていれば
      m_in = m_inIn.read()       # 受信して
      if initialized:            # もし初期化フラグが立っていれば
        m_out.data = m_in.data - old_data # 差分を計算して
        m_out.write()            # 出力
      old_data = m_in.data       # 現在受信したデータをold_dataに格納して
      initialized = True         # 初期化されましたよーフラグを立てる
      return RTC.RTC_OK


Javaはグローバル変数を置けません.
プログラムをよく知らない人はむしろJavaで書いたほうがいいと思っているんですが・・・
まあJavaで書けばいいというわけでもありませんがね.


ざっと眺めてもらうと,C++だとさっくりとグローバル変数を作って使えるんですけど,そのせいでうまく動かない時がありますし,コードが見にくくなります.

これをさっくりと直します.old_dataとinitializedをRTCのクラスのメンバ変数に加えるのです.





#include <basicdatatypeskel.h>
#include <rtm/manager.h>
#include <rtm/dataflowcomponentbase.h>
#include <rtm/corbaport.h>
#include <rtm/datainport.h>
#include <rtm/dataoutport.h>
 
using namespace RTC;
 
 
class TestRTC
  : public RTC::DataFlowComponentBase
{
public:
  TestRTC(RTC::Manager* manager): 
    m_inIn("in", m_in), m_outOut("out", m_out) {}
  virtual ~TestRTC() {}
  virtual RTC::ReturnCode_t onInitialize() {省略}
  virtual RTC::ReturnCode_t onActivated(RTC::UniqueId ec_id) {
    m_initialized = false;
    return RTC::RTC_OK;
  }
 
  virtual RTC::ReturnCode_t onExecute(RTC::UniqueId ec_id) {
    if (m_inIn.isNew()) { // データが来ていれば
      m_inIn.read();      // 読み込んで
      if (m_initialized) {  // もし最初のデータが来ていればold_dataが使えるので
        m_out.data = m_in.data - m_oldData; // 差分を計算する
        setTimestamp<rtc::timeddouble>(m_out);
        m_outOut.write();   // 出力する
      }
      m_oldData = m_in.data; // 受信したデータをold_dataに保存しておく
      m_initialized = true;   // 初期化されましたよーというフラグを立てる
    }
    return RTC::RTC_OK;
  }
private:
  RTC::TimedDouble m_in;
  RTC::InPort<rtc::timeddouble> m_inIn;
  RTC::TimedDouble m_out;
  RTC::InPort<rtc::timeddouble> m_inIn;
 
  boolean m_initialized; // Member Variable
  double m_oldData; // Member Variable
};
import sys
import time
sys.path.append(".")
 
import RTC
import OpenRTM_aist
 
・・・省略・・・
 
 
class TestRTC_py(OpenRTM_aist.DataFlowComponentBase):
 
  def __init__(self, manager):
    OpenRTM_aist.DataFlowComponentBase.__init__(self, manager)
 
    self._d_in = RTC.TimedLong(RTC.Time(0, 0), 0)
    self._inIn = OpenRTM_aist.InPort("in", self._d_in)
    self._d_out = RTC.TimedLong(RTC.Time(0, 0), 0)
    self._outOut = OpenRTM_aist.OutPort("out", self._d_out)
 
 
    self._initialized = False # メンバ変数
    self._oldData = 0 # メンバ変数
 
  def onInitialize(self):
    self.addInPort("in", self._inIn)
    self.addOutPort("out", self._outOut)
    return RTC.RTC_OK
 
  def onActivated(self, ec_id):
    self._initialized = False
    return RTC.RTC_OK
 
  def onExecute(self, ec_id):
    if m_inIn.isNew():           # データが来ていれば
      m_in = m_inIn.read()       # 受信して
      if self._initialized:            # もし初期化フラグが立っていれば
        m_out.data = m_in.data - self._oldData # 差分を計算して
        m_out.write()            # 出力
      self._oldData = m_in.data       # 現在受信したデータをold_dataに格納して
      self._initialized = True         # 初期化されましたよーフラグを立てる
      return RTC.RTC_OK

import 省略

public class TestRTC_javaImpl extends DataFlowComponentBase {

public TestRTC_javaImpl(Manager manager) {
super(manager);
m_out_val = new TimedLong();
m_out = new DataRef(m_out_val);
m_outOut = new OutPort(“out”, m_out);
m_in_val = new TimedLong();
m_in = new DataRef(m_in_val);
m_inIn = new InPort(“in”, m_in);
}

@Override
protected ReturnCode_t onInitialize() {
addInPort(“in”, m_inIn);
addOutPort(“out”, m_outOut);
return super.onInitialize();
}

@Override
protected ReturnCode_t onActivated(int ec_id) {
m_initialized = false;
}

@Override
protected ReturnCode_t onExecute(int ec_id) {
if (m_inIn.isNew()) {
m_inIn.read();
if (m_initialized) {
m_out.v.data = m_in.v.data – m_oldData;
m_outOut.write();
}
m_oldData = m_in.v.data;
m_initialized = true;
}
return super.onExecute(ec_id);
}

protected TimedLong m_out_val;
protected DataRef m_out;
protected OutPort m_outOut;

protected TimedLong m_in_val;
protected DataRef m_in;
protected InPort m_inIn;

protected m_initialized;
protected double m_oldData;
}


こんな感じでデータをクラス内に格納します.難しく無いですし,そんなことか,と思うでしょうが,そんなことでもやって置いてほしいのです.基本中の基本.
どうしてもグローバル変数を使いたい時だけ使うようにして置いてください.

2.2.2 ROSの場合

執筆中

A. ROSノードのオブジェクト指向的書き方

執筆中

2.3 コアロジックをクラス化

さて,グローバル変数は消えましたよね?でも50行,100行あるonExecuteを書いていませんか?
まあ,いいですけどね.でも整理するための基本テクニックをここで紹介しましょう.

2.3.1 RTミドルウェアの場合

特にC++のRTCのコードが問題なんだと思います.CMakeをいじらないといけませんから.

まず、例として、先ほどの二輪対向型ロボット台車を例にあげましょう。ちょっと言語ごとに分岐しますね。





通常は、クラスの宣言と、関数の実装を、.hファイルと.cppファイルに分けますので、ここでもmobilerobot.hと、mobilerobot.cppファイルが手元にあるとしましょう。

RTCBuilderが生成したフォルダにはincludeというフォルダがあり、その中にRTCの名前と同じ名前のフォルダがあるので、そこにファイルを配置するのが基本です。別フォルダにするのがより理想的ですが、ここではそこまで説明しません。
cppファイルはsrcフォルダの中に置きます。

取り敢えず、ファイルを置きましょう。でも、このままcmakeして、Visual Studioで開いても追加したファイルが見当たりません。

ここで焦ってVisual Studioを操作してファイルを追加するのは間違いです。本来はこうします。

includeの下のRTC名のフォルダの中、つまり.hファイルの中にCMakeLists.txtというファイルがあります。これをテキストエディタで開くと、最初の行に.hファイルを並べてある記述がありますよね?ここ!ここに足します。スペード区切りなので気をつけましょう。

同様にsrcフォルダの中のCMakeLists.txtも編集します。

すると、再度ビルドするとあら不思議。ファイルをリンクできるようになりました。

さて、あとはこれを使うコード編集ですが、通常はこうします。

まず、RTC名.hファイルを開き、最初に自分のヘッダーをincludeします。
次に、RTCのクラス宣言の最後に、自分の作ったクラスのポインタを宣言して置きます。

次にRTCのcppファイルを開きます。
僕の場合は、onActivatedでnewし、onDeactivatedでdeleteします。onInitializeの方がいいという意見もあり、同意したいのですが、独自のリソースを確保しないでインターフェースを公開している状態を作りたいので、僕のRTCはほぼ全てonActivatedでリソースを確保しに行きます。リソース確保に失敗した場合にerrorになれるのもこのやり方です。

あとは、onExecuteで使うだけです。


通常通り、importが使えます。


通常通り、importが使えます。


少なくともこのようになっていると、ロボットを動かす部分だけライブラリとして再利用できるじゃないですか。だから、これくらいはやっときましょう。

通常の手順だと、まず単体でロボットを動かすコードを作成し、テストしておきます。次にRTCを用意して、生成したプロジェクトの中にコードを流し込んでCMakeListsを書き換えるという手順になります。

さらに発展すれば、RTCを活用しながらビジネスロジックを開発することもできるでしょうが、ここでは触れません。

2.3.2 ROSの場合

執筆中

3. 具体例 [中級編]

インターフェースを定義して実装とインターフェースを分ける

インターフェースを定義して、オブジェクト生成にファクトリーを使う方法がオススメです。
ビルドが早くなりますし、パターン化できます。

状態マシンを置く

作成したビジネスロジックをライブラリ化する