How to Call Method

このページでは、Baku.LibqiDotNetにおいてqi Frameworkのサービス上の関数を呼び出す一般的な手順を、実際のロボットに適用できるサンプルを用いて紹介します。


本サンプルではロボットのモーションに関するサービスであるALMotionを用いて、現在のロボットの状態を取得したり、逆に特定のポーズを指示するプログラムを作成します。本チュートリアルは実機のロボットだけでなく仮想ロボットでも検証可能です。



1. コンソールアプリケーションの準備


Hello Worldの手順に沿ってコンソールアプリケーションを準備します。



2. もっともシンプルな呼び出し: wakeUpとrest


本チュートリアルでは少しずつコードを書き足しながら動作を確認していきます。まず、本チュートリアルで利用するALMotionモジュールをロードし、ロボットのアクチュエータを起動し、また停止する処理を行ってみましょう。このような処理はwakeUp関数とrest関数を順に呼び出すことで実装できます。


以下ではProgram.csのみを含むコンソールアプリケーションを想定し、簡単のためusing文とMain関数のみを記述します。また接続先となるロボットのアドレスは適切に指定してください。

using System;
using Baku.LibqiDotNet;
using Baku.LibqiDotNet.Path;


static void Main(string[] args)
{
    PathModifier.AddEnvironmentPath(
        "dlls", PathModifyMode.RelativeToEntryAssembly);

    //HelloWorldの対象とするマシンのアドレスをIPとポート(ポートは通常9559)で指定
    string address = "tcp://xxx.xxx.xxx.xxx:9559";
    var session = QiSession.Create(address);

    var motion = session.GetService("ALMotion");

    motion["wakeUp"].Call();
    motion["rest"].Call();
}


このプログラムを動作させると、ロボットのアクチュエータが起動していない場合は起動を行い、その後再びアクチュエータを休止させます。



3. 関数の結果を受け取る


上の例では戻り値がない関数を呼び出しました。次は戻り値がある関数を利用してみましょう。たとえばALMotionモジュールでは、現在のロボットの姿勢情報を単一の要約文テキストとして取得するgetSummary関数があります。

string getSummary();


この関数を利用するコードは次のようになります。

using System;

using Baku.LibqiDotNet;
using Baku.LibqiDotNet.Path;


static void Main(string[] args)
{
    PathModifier.AddEnvironmentPath(
        "dlls", PathModifyMode.RelativeToEntryAssembly);

    string address = "tcp://xxx.xxx.xxx.xxx:9559";
    var session = QiSession.Create(address);

    var motion = session.GetService("ALMotion");

    string summary = (string)motion["getSummary"].Call();

    Console.WriteLine(summary);
}


出力例は次の通りです(最上部に警告を示す[W]から始まる出力が出ていますが無視して構いません)。

[W] 1457718290.352819 14000 qi.path.sdklayout: No Application was created, trying to deduce paths
---------------------- Model ---------------------------
        BodyName   Stiffness     Command      Sensor
         HeadYaw    0.000000   -0.000000   -0.000000
       HeadPitch    0.000000    0.637000    0.637000
  LShoulderPitch    0.000000    1.133252    1.133252
   LShoulderRoll    0.000000    0.059364    0.059364
       LElbowYaw    0.000000   -0.491643   -0.491643
      LElbowRoll    0.000000   -0.008727   -0.008727
       LWristYaw    0.000000   -0.801464   -0.801464
         HipRoll    0.000000    0.000000    0.000000
        HipPitch    0.000000   -1.038400   -1.038400
       KneePitch    0.000000    0.510000    0.510000
  RShoulderPitch    0.000000    1.133252    1.133252
   RShoulderRoll    0.000000   -0.059364   -0.059364
       RElbowYaw    0.000000    0.491643    0.491643
      RElbowRoll    0.000000    0.008727    0.008727
       RWristYaw    0.000000    0.801464    0.801464
           LHand    0.000000    0.600000    0.600000
           RHand    0.000000    0.600000    0.600000
         WheelFL    0.000000    0.000000    0.000000
         WheelFR    0.000000    0.000000    0.000000
          WheelB    0.000000    0.000000    0.000000
---------------------- Tasks  --------------------------
            Name         ID
----------------- Motion Cycle Time --------------------
              20 ms


改行を含むサマリー文字列が正しく取得出来ています。もう一度関数呼び出しの部分を再掲しますが、ここでは呼び出し結果に対してキャストを行っています。

string summary = (string)motion["getSummary"].Call();


関数の呼び出し結果はQiValue型の変数です。この変数はqi Frameworkから渡される全ての変数の派生元である(ように見える)クラスです。本質的にこの変数が保持するデータは整数値、文字列やリスト構造などC#でもお馴染みのデータですが、とくに組み込み型を含むいくつかの型への変換は頻繁に必要となるため、上述のようなキャスト演算によって値を取得できるようになっています。



4. キャストを用いず複雑な戻り値を得る: getAngles関数


前節の例ではstring型の戻り値を持つ関数をキャスト操作によって取得出来る事を紹介しました。以下の型の変数はQiValueからのキャストによって得られます。

  • 論理値(bool)
  • 整数(int, long, short, sbyte, uint, ulong, ushort, byte)
  • 小数(float, double)
  • 文字列(string)
  • 配列(int[], double[], string[])
  • バイナリ(byte[])


しかし上記のキャストが出来ないような、複雑な戻り値を持った関数を呼び出した場合はどうすればよいのでしょうか?例を示すため、ここではキャスト可能な戻り値をあえてキャスト以外の方法でパースしてみます。下記の例ではgetAngles関数を用い、ロボットの各関節の角度を小数の配列として取得しています。

using System;

using Baku.LibqiDotNet;
using Baku.LibqiDotNet.Path;



static void Main(string[] args)
{
    PathModifier.AddEnvironmentPath("dlls", PathModifyMode.RelativeToEntryAssembly);

    string address = "tcp://xxx.xxx.xxx.xxx:9559";
    var session = QiSession.Create(address);

    var motion = session.GetService("ALMotion");

    //キャストを用いた方法
    double[] angles = (double[])motion["getAngles"].Call("Body", true);

    //キャストを用いない方法
    QiValue result = motion["getAngles"].Call("Body", true);
    var angles2 = new double[result.Count];
    for (int i = 0; i < angles2.Length; i++)
    {
        angles2[i] = (double)result[i];
    }

    //同じ値が取得できているかチェックしてみよう!
    for (int i = 0;i < angles.Length; i++)
    {
        Console.WriteLine(angles[i]);
        Console.WriteLine(angles2[i]);
    }

}


キャストを用いない方法ではQiValue.Countプロパティにアクセスすることで配列の長さを取得し、インデックスを指定することで配列の各要素を取り出しています。CountプロパティはQiValueの実際の内容が配列などである場合は要素数を表し、それ以外の場合Countの値は単に0になります。

私がこの例を試した所、次のような出力が得られました。

[W] 1457719018.860709 11624 qi.path.sdklayout: No Application was created, trying to deduce paths
-5.6843418860808E-14
-5.6843418860808E-14
0.637000024318695
0.637000024318695
1.13325190544128
1.13325190544128
0.0593636594712734
0.0593636594712734
-0.491642862558365
-0.491642862558365
-0.00872664619237185
-0.00872664619237185
-0.801463842391968
-0.801463842391968
0.600000023841858
0.600000023841858
0
0
-1.03840005397797
-1.03840005397797
0.509999990463257
0.509999990463257
1.13325190544128
1.13325190544128
-0.0593636594712734
-0.0593636594712734
0.491642862558365
0.491642862558365
0.00872664619237185
0.00872664619237185
0.801463842391968
0.801463842391968
0.600000023841858
0.600000023841858
0
0
0
0
0
0

どちらの方法でも同じ結果が得られている事が分かります。今回の例ではQiValueをわざわざ操作する意義があまり感じられなかったと思いますが、配列の要素ごとに異なる型のデータが入ったようなデータを受け取る場合QiValueの直接操作が必要となります。この具体例に関してはGitHubのソースコードにカメラ画像を取得する例が含まれているので参考にしてください。



5. 複雑な引数を渡す: angleInterpolation関数


前節では関数の戻り値が複雑であるケースのための処理を紹介しました。今度は関数の引数が複雑な場合に対応することを考えてみましょう。今回は具体例として、ロボットの動きを自動で補間するangleInterpolation関数のサンプルをC#で動かしてみましょう。

この関数の仕様はドキュメンテーションに記載されていますが、それより大切なのは、この関数を使う場合に入れ子配列のデータ構造が必要ということです。たとえばPythonのサンプルコードを抜粋すると次のようなプログラムが書かれています。

names  = ["Head"]
angleLists  = [[50.0*almath.TO_RAD, 0.0],
               [-30.0*almath.TO_RAD, 30.0*almath.TO_RAD, 0.0]]
timeLists   = [[1.0, 2.0], [ 1.0, 2.0, 3.0]]

motionProxy.angleInterpolationBezier(names, timeLists, angleLists)


almath.TO_RADは度数法をラジアン法に変換している定数なので大した意味はありません。ポイントになるのはangleListstimeListsがリストのリストであるという点です。この処理をC#で行う場合、コードは次のようになります。

using System;

using Baku.LibqiDotNet;
using Baku.LibqiDotNet.Path;



static void Main(string[] args)
{
    PathModifier.AddEnvironmentPath(
        "dlls", PathModifyMode.RelativeToEntryAssembly);

    string address = "tcp://xxx.xxx.xxx.xxx:9559";
    var session = QiSession.Create(address);

    var motion = session.GetService("ALMotion");

    double toRad = Math.PI / 180.0;

    QiList<QiString> names 
        = QiList.Create(new string[] { "HeadYaw", "HeadPitch" });

    QiList<QiList<QiDouble>> angleLists
        = QiList.Create<QiList<QiDouble>>(new QiList<QiDouble>[]
    {
        QiList.Create(new double[] { 30.0 * toRad, 0.0 }),
        QiList.Create(new double[] { -30.0 * toRad, 30.0 * toRad, 0.0 })
    });

    QiList<QiList<QiDouble>> timeLists
        = QiList.Create<QiList<QiDouble>>(new QiList<QiDouble>[]
    {
        QiList.Create(new double[] { 1.0, 2.0 }),
        QiList.Create(new double[] { 1.0, 2.0, 3.0 })
    });

    QiBool isAbsolute = new QiBool(true);

    motion["angleInterpolation"].Call(names, angleLists, timeLists, isAbsolute);
}


これを実行するとロボットの首が左上、下、正面を順に向きます。


上記のプログラムでは今まで現れなかったクラス名がいくつか現れています。とはいえQiList, QiString, QiDouble, QiBoolなどの型名からこれらが何を意味するのかは想像がつくでしょうし、経験のあるプログラマであればQiList<QiList<QiDouble>>が小数の二次元配列データを表すこともすぐ納得できるでしょう(※上のコードではvarキーワードの使用を意図的に控えて冗長な表現を行っていますが、これは型が分かりやすいように示しているだけです。リファクタリングされたコードは後述します)。QiList<T>型の変数を生成しているのはQiList.Create関数です。この関数は実際のユースケースを想定して大量にオーバーロードが定義されていますが、これについてはVisual Studio上でインテリセンスとにらみ合って確認すれば済む話題ですから詳しくは触れません。


それよりも問題なのはCall関数の仕組みです。せっかくなので、今まで何となく使っていたCall関数の正式なシグネチャを確認しましょう。シグネチャは下記の通りです。

class QiMethod
{
    public QiValue Call(params QiAnyValue[] args) { /* 実装 */ }
}


Call関数は可変個のQiAnyValue型変数を引数に取ります。QiAnyValueクラスはQiValueクラスと類似したクラスですが、QiValueが関数の戻り値の基底を表していたのとは対照的に、QiAnyValueクラスは関数への入力に用いる値の基底を表します。


ご想像の通り、上記のコードで使っているQiString, QiBool, QiDoubleなどは全てQiAnyValueの派生型です。QiList<T>(TQiAnyValueの派生型)もQiAnyValueから派生しています。以上から、適切なデータを関数の引数に渡す手順は

  1. Qi**というQiAnyValueから派生した型の変数を作成
  2. 適切な関数名を指定し、Call関数に適切な順番で引数を渡す


というシンプルなものであることが分かりました。一件落着です



…いや、ちょっと待ってください。ひとつ引っかかる事があります。Hello Worldプログラムを思い出して欲しいのですが、このプログラムでは次のようなコードを書いていました。

//最も基本的なモジュールの一つとして合成音声のモジュールを取得
var tts = session.GetService("ALTextToSpeech");

//"say"関数に文字列引数を指定して実行
tts["say"].Call("this is test");


QiAnyValueの派生型ではなく単なる文字列を渡せています。なぜでしょうか?


これは仕掛けが分かってしまえば簡単な話なのですが、QiAnyValueクラスはいくつかの暗黙型キャストがimplicit operatorで定義されています。例をいくつか紹介します。

public abstract class QiAnyValue
{
    public static operator QiAnyValue(bool v) => new QiBool(v);
    public static operator QiAnyValue(int v) => new QiInt32(v);
    public static operator QiAnyValue(string v) => new QiString(v);
}


このような定義があるため、下記の呼び出し

tts["say"].Call("this is test");


これは実際には次のコードと同様に扱われます。

tts["say"].Call(new QiString("this is test"));


暗黙の型変換がサポートされている型は論理値、整数型、小数型とバイナリデータを表すbyte[]、また利用頻度がそれなりに高いと思われるint[], string[], double[]です。これを踏まえたうえで先程のangleInterpolationを用いたコードを書き直してみると、より簡潔にまとめる事が出来ます。


PathModifier.AddEnvironmentPath(
    "dlls", PathModifyMode.RelativeToEntryAssembly);

string address = "tcp://xxx.xxx.xxx.xxx:9559";
var session = QiSession.Create(address);

var motion = session.GetService("ALMotion");

double toRad = Math.PI / 180.0;

var names = new string[] { "HeadYaw", "HeadPitch" };

var angleLists = QiList.Create(new []
{
    QiList.Create(new double[] { 30.0 * toRad, 0.0 }),
    QiList.Create(new double[] { -30.0 * toRad, 30.0 * toRad, 0.0 })
});

var timeLists = QiList.Create(new []
{
    QiList.Create(new double[] { 1.0, 2.0 }),
    QiList.Create(new double[] { 1.0, 2.0, 3.0 })
});

motion["angleInterpolation"].Call(names, angleLists, timeLists, true);


truenew QiBool(true)へ変換され、namesQiList<QiString>に適切に変換されます。入れ子配列の難しいデータ構造以外では組み込み型をそのまま渡せるので、簡潔なコードを書く場合は意識的にこの機能を活用してください。