ALMemory

本ページではAldebaranのロボットが標準的に採用しているALMemoryモジュールにアクセスし、メモリデータの読み書きやメモリイベントの監視を行う方法を見ていきます。ALMemory自体のしっかりした説明については公式ドキュメンテーションも参照下さい。


厳密に言うならlibqiBaku.LibqiDotNetALMemoryには直接の関係はありませんが、ALMemoryを使いこなすことはロボットアプリケーションの実用上重要であるため、チュートリアルの一環として紹介します。


本ページのチュートリアルは3セクションに分かれています。第一にALMemoryを用いてメモリデータの読み書きを行うプログラムを作成します。第二にメモリのイベント機構とその活用方法をサンプルコードで学びます。第三に、メモリのイベントを活用するプログラムとしてロボットが発した言葉やロボットが周囲から聞き取った音声認識結果のテキストログを取得するプログラムを作成します。


本節でも前までのチュートリアルと同様にコンソールアプリケーションを作成します。アプリケーションの準備手順についてはHello Worldを確認してください。



1. メモリデータの読み書き


もっとも簡単なデータ処理として、ロボット上のパブリックな領域へデータを書き込んだり、逆に読み込んだりする方法を見ていきましょう。



1-1. データの書き込みと読み込み


ALMemoryモジュールはinsertDataという関数があるので、これを使ってメモリ上へデータを配置します。またデータの取得はgetData関数で行えます。


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

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 memory = session.GetService("ALMemory");

    memory["insertData"].Call("MyApplication/MyData", 12345);

    int data = (int)memory["getData"].Call("MyApplication/MyData");

    Console.WriteLine(data);
}


これを実行すると、いったんロボット上に"MyApplication/MyData"というキーへ整数値12345を割り当てたのち、それをすぐダウンロードして結果を表示するという作業が行われます。その結果としてコンソールには12345が表示されます。



1-2. 既存のデータ一覧を取得する


これはドキュメンテーションから対応する関数を見つけられるかどうかの勝負です。ALMemoryにはgetDataListNameという関数があるので、これを使うとほぼ直ちに結果が得られます。

using System;

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



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

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

    var memory = session.GetService("ALMemory");

    var keys = (string[])memory["getDataListName"].Call();

    Console.WriteLine(keys.Length);
}


筆者が仮想ロボットに対してこのプログラムを試したところ1034個のデータがあるという結果を得ました。メモリデータの数は状況に応じて大きく変化するので、実機のロボットでこれよりはるかに大きい数字が出てもびっくりしないでください。



2. メモリイベントという仕組み


前節ではメモリ上のデータをシンプルに読んだり書いたりする機能を紹介しました。ALMemoryにはこれに加えてC#とほぼ同様のイベント機構がサポートされています。つまり、データの入力と同時にイベントを発生させたり、イベントハンドラ(コールバック)を登録することが出来ます。この機能はPepperがタブレットと本体を連携させるアプリケーション等では中心的な役割を担っており、C#でのアプリケーション作成でもイベント駆動処理が行いたい場合とくに重要です。



2-1. イベントの購読


ロボット上に既に定義されているイベントにアクセスする方法はロボットの会話ログを取得するサンプルで紹介するとして、ここではユーザが定義したイベントを購読する方法から始めます。本節で紹介するプログラムはPythonの場合であればqi Frameworkのドキュメンテーションに非常に似た例が掲載されています。


イベントの購読処理には次のような定型のプログラムを記述します。

using System;
using System.Threading;
using System.Threading.Tasks;

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 memory = session.GetService("ALMemory");

    var subscriber = memory["subscriber"].CallObject("MyApplication/MyEvent");

    Action<QiValue> callback = v =>
    {
        Console.WriteLine("Received Event");
        Console.WriteLine(v.Dump());
    };
    subscriber.ConnectSignal("signal", callback);

    Console.WriteLine("Press ENTER to end..");
    Console.ReadLine();

    subscriber.DisconnectSignal(callback).Wait();
}


このプログラムが接続に成功した場合、ENTERキーを押すとプログラムが終了します。それだけです。このプログラムは何もしません。


いや、何もしないというのは流石に言い過ぎでした。より正確には、このプログラムは発生し得ないイベントを購読、監視していました。購読処理は次のようにして行われます。

var subscriber = memory["subscriber"].CallObject("MyApplication/MyEvent");

Action<QiValue> callback = v =>
{
    Console.WriteLine("Received Event");
    Console.WriteLine(v.Dump());
};
subscriber.ConnectSignal("signal", callback);

//何かしらの処理

subscriber.DisconnectSignal(callback).Wait();


subscriber関数のCallObject(Call関数ではない事に注意!)を呼び出すと、通常のプログラムと異なり、この呼び出し結果はQiObject型の変数となります。QiObjectなんて初耳だと思われるかもしれませんが、実はQiObjectというのは今までのチュートリアルで用いてきたモジュールを表す型です。

QiObject tts = session.GetService("ALTextToSpeech");
QiObject motion = session.GetService("ALMotion");
QiObject memory = session.GetService("ALMotion");

QiObject subscriber 
    = memory["subscriber"].CallObject("MyApplication/MyEvent");


変数の型は同じですが、subscriber変数では今まで利用してこなかったConnectSignal関数とDisconnectSignal関数を用いています。ConnectSignalがイベントハンドラへの追加処置、DisconnectSignalがハンドラの削除に対応しています。第一引数である"signal"というのは監視するイベントのキーのような文字列なのですが、この文字列はある種のマジックナンバーみたいなもので、ALMemoryをこのように使う範囲では"signal"以外の文字列を渡す必要はありません。定数文字列をわざわざ書かされるなんて、と思うのももっともなことですが、この一見ちぐはぐなAPIはqi Frameworkがロボットの実装詳細と独立なライブラリであるために必要なすれ違いなので、まあ許容してください。


とにかく、上記のように第一引数へ"signal"という定数文字列、第二引数へイベントハンドラ的なコールバック関数を登録する、という手続きだけ覚えてください。


また購読解除では購読処理に使ったのと同じAction<QiValue>変数を指定することで解除処理が行えますが、実はこの購読解除は整数値のIDベースな方法で置き換える事も可能です。


var subscriber = memory["subscriber"].CallObject("MyApplication/MyEvent");

Action<QiValue> callback = v =>
{
    Console.WriteLine("Received Event");
    Console.WriteLine(v.Dump());
};
ulong id = subscriber.ConnectSignal("signal", callback);

//何かしらの処理

subscriber.DisconnectSignal(id).Wait();


個人的には登録時に使ったActionを直接渡す方法の方が直感的で気に入っていますが、好みで使い分けてください。

また上記のプログラムではQiValueDump関数を用いていますが、この関数は中身が分からないQiValueのオブジェクトについて内容を表す文字列を得るためのものです。今回のような純粋なデバッグでは有効に活用することが出来ます。



2-2. イベントを発生させる


前節ではイベントを購読するコードを紹介しましたが、あくまで購読しただけなのでコールバックの動作も確認できず、何が起きているのか分かりにくかったと思います。そこでイベントを発生(発火)させるコードを追加し、全体の動作を見てみましょう。

using System;
using System.Threading;
using System.Threading.Tasks;

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 memory = session.GetService("ALMemory");

    var subscriber = memory["subscriber"].CallObject("MyApplication/MyEvent");

    Action<QiValue> callback = v =>
    {
        Console.WriteLine("Received Event");
        Console.WriteLine(v.Dump());
    };
    subscriber.ConnectSignal("signal", callback);

    var cts = new CancellationTokenSource();
    var token = cts.Token;
    var t = Task.Run(async () =>
    {
        while (!token.IsCancellationRequested)
        {
            await Task.Delay(1000);
            memory["raiseEvent"].Call("MyApplication/MyEvent", 12345);
        }
    }, token);

    Console.WriteLine("Press ENTER to end..");
    Console.ReadLine();

    cts.Cancel();
    t.Wait();

    subscriber.DisconnectSignal(callback).Wait();
}


このプログラムでは、自分が購読したイベントの発生処理をraiseEvent関数の呼び出しによって自力でこなしています。うまく動作した場合、1秒おきにコールバック関数が呼ばれます。出力例はこのようになります。

[W] 1457789913.867607 10072 qi.path.sdklayout: No Application was created, trying to deduce paths
Press ENTER to end..
[I] 1457789914.966895 11620 qimessaging.object: Signal Callback()
Received Event
Tuple[1]:
  Dynamic
    QiInt:12345

[I] 1457789916.024120 11620 qimessaging.object: Signal Callback()
Received Event
Tuple[1]:
  Dynamic
    QiInt:12345


"Received Event"に続く3行はQiValue.Dump関数によってQiValue値の中身を表示した出力ですが、ここでTupleDynamicといった見慣れないものが再び現れました。


Tupleというのは名前の通りC#のタプル型みたいなものです。タプルから値を取り出す処理は配列とほぼ同様にインデックス指定で出来ますが、配列との違いとして要素ごとに型が異なってもよいことに注意が必要です。今回の場合データが単一の整数であるため、わざわざタプルの形で渡されるのは釈然としないかもしれませんが、これは「そういうものだ」と割り切ってください。


Dynamicというのは、あまり意識しないでも構わないのですが、単一のQiValueを保持するコンテナです。保持できるQiValueの内容に制約が無いため、見かけ上動的に型が変わっているように見えるのがDynamic型と呼ばれている理由です。上の例ではDynamic型は実際には整数値を保持しており、その値が12345であるという構造になっています。


Dynamic型からデータを取り出そうとすると暗黙にDynamicが実際に保持するデータを得られるので、Dynamicの存在を意識せずにキャスト処理をして構いません。コールバック関数を修正し、整数値を取得できるようにしてみましょう。具体的には次のようにします。

Action<QiValue> callback = v =>
{
    Console.WriteLine("Received Event");
    int data = (int)v[0];
    Console.WriteLine(data);
};


タプルの唯一の要素を取り出して整数値にキャストしています。実行例は次の通りです。

[W] 1457794565.019111 12216 qi.path.sdklayout: No Application was created, trying to deduce paths
Press ENTER to end..
[I] 1457794566.142094 12216 qimessaging.object: Signal Callback()
Received Event
12345
[I] 1457794567.168465 12216 qimessaging.object: Signal Callback()
Received Event
12345


整数値が正しく抽出できた事が分かります。



2-3. 既存のイベント一覧を取得する


データ一覧を取得する時とほとんど同じ手順が利用可能です。先ほどとほぼ同じコードですが、今度はgetEventListという関数を使います。

using System;

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



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

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

    var memory = session.GetService("ALMemory");

    var keys = (string[])memory["getDataListName"].Call();

    Console.WriteLine(keys.Length);
}


仮想ロボットでこれを試したところ230個のイベントが登録されている、という結果が得られました。メモリデータの個数と同様、この数値も実機のロボットでは各段に大きくなることに注意してください。



3. ロボットの会話ログを取得するサンプル


前節まではイベントを購読したり発生させたりする方法を自作のイベントによって見てきました。自作イベントの取り扱いも重要ですが、ここでは具体的なプログラムの例としてロボットの発話やロボットが聞き取った音声認識結果のテキストをロギングするプログラムを書いてみます。

using System;

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



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

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

    var memory = session.GetService("ALMemory");


    //人の会話
    var subscriberHumanSpeech = memory["subscriber"]
        .CallObject("Dialog/LastInput");
    ulong idHumanSpeech = subscriberHumanSpeech.ConnectSignal("signal", qv =>
    {
        if (qv.Count > 0 && qv[0].ContentValueKind == QiValueKind.QiString)
        {
            Console.WriteLine($"Human: {qv[0].ToString()}");
        }
        else
        {
            Console.WriteLine("Human: Received unexpected data");
            Console.WriteLine(qv.Dump());
        }
    });

    //ロボットの発話
    var subscriberRobotSpeech = memory["subscriber"]
        .CallObject("ALTextToSpeech/CurrentSentence");
    ulong idRobotSpeech = subscriberRobotSpeech.ConnectSignal("signal", qv =>
    {
        if (qv.Count > 0 && qv[0].ContentValueKind == QiValueKind.QiString)
        {
            string sentence = qv[0].ToString();
            if (!string.IsNullOrWhiteSpace(sentence))
            {
                Console.WriteLine($"Robot: {sentence}");
            }
        }
        else
        {
            Console.WriteLine("Robot: Received unexpected data");
            Console.WriteLine(qv.Dump());
        }
    });

    Console.WriteLine("Press ENTER to quit logging results.");
    Console.ReadLine();

    subscriberHumanSpeech.DisconnectSignal(idHumanSpeech).Wait();
    subscriberRobotSpeech.DisconnectSignal(idRobotSpeech).Wait();
}


上記のコードを実行して待機状態に入ったら、ログ出力を確認するために次のような方法を取ってください。

  1. ロボットに発話させる:
    • Choregraphe上でsayボックスを使い、簡単な発話をするプログラムを実行する
    • C#の新しいプロジェクトを作ってHello Worldコードを実行する
    • Pythonの対話環境でPyNaoqi SDKなどを使って発話させる
  2. 人の発話を認識させる:
    • (実機ロボット)Autonomous Lifeがオンの状態で話しかける
    • (仮想/実機ロボット)Choregrapheダイアログウィンドウでテキストを入力する


うまく動作している場合、ロボットの発話や人の発話情報がコンソールに表示されます。