Bakulog

獏の夢日記的な何か。

Python/qi FrameworkでPepperに自作機能を登録する手順

タイトル通りです。内容としては公式ドキュメントを見つつ公式Githubのサンプルを動かしてみた紹介です。

    もくじ 1. はじめに 2. とりあえずサンプルが動くところまで 3. サンプルの中を見てみる 4. もう少し詳しい話: デコレータの意味について 5. まとめ

   

はじめに

本記事ではPepperやNaoのための新しいフレームワークであるqi Frameworkをガッツリ使うことを目的とし、特に自作の機能をPepperに載せるための方法に触れます。古いフレームワークであるNAOqiについてはそこそこ情報が転がってますが、qi Frameworkについては(需要が無いせいもあるのでしょうが)手を付けてる人が少ない印象を持っているため本記事を書いています。

対象とするプログラミング言語はPepperにとっていちばんスタンダードなPython 2.7です。qi Framework自体に馴染みがない場合は先に「【和訳記事】NAOqiのプログラムをqi Frameworkに移行しよう!」等をご覧ください。

 

具体的な内容ですが、下記の公式リソースを踏まえて自作機能(以下では「サービス」と呼びます)を作る手順が分かる所までサンプルを追ってみます。

ドキュメンテーションでは特にPython - How to write a qimessaging serviceが本記事の話題と関連の強い部分です。GithubについてはC++の実装ではなくexamplesフォルダ内のスクリプトサンプルを見れば十分で、特にその中でもサービス自作についてはqiservice.pyqiclient.pyが「サービスを作る」「作ったサービスを呼び出してみる」という二つで一組のサンプルになっています。

 

英語に抵抗ない場合は上記のリソースを自分で見つつサンプルを動かしていただければ大丈夫だと思うのですが、日本語訳があった方が手を出しやすい人も少なからずいらっしゃると思うので、以下ではサンプルの動かし方とかを順を追って紹介します。

 

なお実機のPepper/Naoがすぐ手元にない場合を想定し、本記事の途中でWindows版のChoregrapheに付属したPepper/Naoのシミュレータを用いますが、他OS用のChoregrapheでもシミュレータが普通に使えるかは確認していません。あらかじめご注意下さい。本記事の検証は全てWindows 10上で行っています。

   

とりあえずサンプルが動くところまで

まずAldebaranの公式からChoregrapheおよびPython NAOqi SDKを入手してください。これらの入手方法ついてはググれば出来ると思うので詳細は割愛します。

次に、上で触れたGithubに公開されてるスクリプトであるqiservice.pyqiclient.pyをダウンロードします。レポジトリごとgit cloneして持ってきても、ブラウザ上でスクリプトだけコピーしてローカルのテキストファイルとしてペーストするのでも構いません。

スクリプトを入手したらターミナルかコマンドプロンプトを二つ開き、サービスとクライアントのスクリプトを順に実行します。まずはサービス側。

python qiservice.py

実行結果です。

Usage : qiservice.py qi-service.py directory-address
Assuming service directory address is tcp://127.0.0.1:9559
[W] 10324 qimessaging.transportsocket: connect: 対象のコンピューターによって拒否されたため、接続できませんでした。
Traceback (most recent call last):
  File "qiservice.py", line 98, in <module>
    main()
  File "qiservice.py", line 79, in main
    s.connect(sd_addr)
RuntimeError: System error: 対象のコンピューターによって拒否されたため、接続できませんでした。

…失敗しました。本来このサンプルはローカル回線内にPepperを配置し、接続先を指定して使うものであるため、普通は下記のようにアドレス指定もしながら使います。

python qiservice.py tcp://192.168.xx.xx:9559

実機を用意して接続するのは面倒なため、今回はシミュレータで済ませます。Choregrapheのインストールディレクトリあたりにnaoqi-bin.exeという実行ファイルがあるので起動します。naoqi-bin.exeのフルパスは標準的なインストールを行った場合次のような感じになるはずです。

C:\Program Files (x86)\Aldebaran\Choregraphe Suite 2.4\bin\naoqi-bin.exe

Choregrapheのバージョン番号は適宜読み替えてください。

 

naoqi-bin.exeをダブルクリックで実行するとコンソールが出てきてバババッとログが流れ、数秒経つとログが落ち着きます。この画面を開いたまま放置しておくことで、PepperやNaoの独立したシミュレータが動作している状態になります。

 

機動したシミュレータは通常ローカルマシンの9559番ポートからアクセスできるように設定されるので、この状態で次のように実行すればサンプルが動作します。

python qiservice.py tcp://127.0.0.1:9559

正しく動作すると、サービス側のコンソールに1秒おきにprint処理が行われるようになります。このコンソールを放置して別のターミナルに移動し、今度はクライアント側のサンプルを動かします。

python qiclient.py

ローカルマシン上でシミュレータが走っている場合はこれでうまく行きます。ローカルマシン以外の場所の実機なりシミュレータなりに接続を行いたい場合、qiclient.pyソースコードのうちこの辺session.connect関数を呼び出している箇所があるので、ここの引数を接続したいアドレスに書き換えて下さい。

 

クライアント側がうまく動くと2, 3秒ほどサービス側と通信して切断するという動きが行われます。クライアント側のサンプルを走らせるとサービス/クライアントの両方のコンソールに表示が出るので、クライアント側のサンプルを数回走らせて動作を確認するのが良いと思います。

   

サンプルの中を見てみる

とりあえずサンプルが動くようになった(ハズな)ので、今度はソースコードを見てみます。最低限の確認だけしたいのでサービス側であるqiservice.pyを覗いてみると、メイン関数は次のように定義されています。

def main():
    """ Entry point of qiservice
    """
    #1 Check if user give us service directory address.
    sd_addr = get_servicedirectory_address()

    s = qi.Session()
    s.connect(sd_addr)
    m = ServiceTest()
    s.registerService("serviceTest", m)

    #5 Call Application.run() to join event loop.
    i = 0
    while True:
      mystr = "bim" + str(i)
      print("posting:", mystr)
      myplouf = [ "bim", 42 ]
      m.testEvent(mystr)
      m.testEventGeneric(myplouf)
      time.sleep(1);
      i += 1

    #6 Clean
    s.close()
    #main : Done.

細かい所も気になりますが、取り分けて重要なのは次の二行です。

m = ServiceTest()
s.registerService("serviceTest", m)

変数sはメイン関数内で生成される接続済みセッションですが、ServiceTestとは何でしょうか?これはメイン関数のすぐ上で定義されているクラスです。

class ServiceTest:
    def __init__(self):
        self.onFoo = qi.Signal("(i)")
        self.testEvent = qi.Signal("(s)")
        self.testEventGeneric = qi.Signal()

    def reply(self, plaf):
        print("v:", plaf)
        return plaf + "bim"

    def error(self):
        d= dict()
        print("I Will throw")
        r = d['pleaseraise']

    def fut(self):
        p = qi.Promise()
        #p.setValue(42)
        threading.Thread(target=makeIt, args=[p]).start()
        return p.future()

    @qi.nobind
    def nothing(self):
        print("nothing")
        pass

    @qi.bind(qi.String, (qi.String, qi.Int32), "plik")
    def plok(self, name, index):
        print("ploK")
        return name[index]

    @qi.bind(qi.Dynamic, qi.AnyArguments)
    def special(self, *args):
        print("args:", args)

    def special2(self, *args):
        print("args2:", args)

デコレータによるカスタマイズが入ってますが、それ以外は特に何の変哲もないクラス定義です。つまりqi FrameworkPythonから使ってサービスを登録する手順は

  1. 自作のサービスを一般的なクラスとして定義してインスタンス
  2. qi.SessionregisterService関数で名前を付けて登録
  3. (プログラムがすぐ終わってしまわないよう無限ループなどで待機)

というとても簡単な手順で構成されています。結論だけ見たら拍子抜けな感じですが、qi Frameworkが使いやすいという風に受け取ればいいのでしょう。NAOqiの場合にはALModuleクラスを継承する必要があったことと比べてもqi Frameworkはシンプルにまとまっていると言えそうです。

基本的には上記の「普通のクラス定義が使える」という事実だけ覚えておき、具体的な書き方はサンプルコードのコピーをベースにしてしまえば大概なんとかなると思います。

 

なおサンプルの例では関数の中身がシンプルすぎるのでPepperやNaoと連携している風景が想像できませんが、実際にはクラスの関数内で別のサービスと組み合わせてそれらしい処理を作る事になるかと思います。

class Foo:
    def __init__(self, address):
        self.ses = qi.Session()
        self.ses.connect(address)
        self.tts = self.ses.service("ALTextToSpeech")

    def talkAboutHomeElectronics(self, query):
        #例: なんか家電デバイスの情報とかそういうのをPC越しに収集し、テキストとして要約した結果を得る
        summary = getSomeHomeElectronicsStateSummary(query)
        #要約文をPepper/Naoに喋らせる
        tts.say(summary)

ただし重要な注意として大概の処理はこんなことをせずChoregraphe上で普通に作る方が簡単なので、WindowsMac専用のデバイスとの連携といった特殊な状況に対してのみ上記の方法に頼るのが良いんじゃないかなあと思います。  

基本的な説明は以上になります。サンプルの方ではSignalを用いるもっと高級な処理も紹介されていますが、これについては細かい話題になること、およびALMemoryモジュールをうまく使ってれば代用できそうな機構であることから思い切って省略します。気になる方はドキュメンテーションの、特にqi.Signal APIの箇所を参照下さい。

   

もう少し詳しい話: デコレータの意味について

最低限の使い方は上に述べた通りですが、上の説明だけでは「クラス定義の中に@qi.bindってあるけどこのデコレータは一体何やってんの??」といった疑問が解消されないため、デコレータについてもう少し詳しく確認しておきます。

デコレータの使い方についてはドキュメンテーションの中でもBind APIのページに記載されています。このAPIの目的ですが、設計目的を言う代わりにBind APIが無い場合とどういう問題があるかを簡単に示します。

 

前節ではサンプルコードを通じて「サービスを作るには普通にクラスを定義すればいい」という主旨のことを書きました。しかしPythonは動的型付け言語ですから、普通にクラスと関数を定義した場合、関数の情報として分かるのは「関数の名前」「引数の想定個数」くらいで、関数の引数が文字列なのか整数なのか、戻り値はあるのかないのか、といった詳しい情報は拾えません。

qi Frameworkは動的な型もサポートしているので型付けがフワッとした関数も登録出来ることには出来るのですが、関数の使い方を分かりやすくして想定外の入力値を避けるという一般的設計のためには、「この関数は文字列("str"的な)を引数にとって論理値("bool")を返す関数です」というように、入出力の型が伴った形で関数を登録する方がより健全です。一言で言うなら「静的型付けしたい!」と。

 

このようなモチベーションに基づいて整備されているのがBind APIで、これを使うことで関数の引数や戻り値の型、またサービスへの登録名なども含めたシグネチャ情報を定義できます。公式ドキュメンテーションにはシンプルな具体例が4つ掲載されているので順に見てみましょう。

 

例1: 関数のオーバーロード

第一の例ではいきなりトリッキーな事をやっています。この例では同名の関数を複数の型の引数に対応させる、いわゆるオーバーロード処理を行います。

class MyFoo:

  @qi.bind(paramsType=(qi.Int32,) , methodName="bar")
  def bar1(self, arg):
    pass

  @qi.bind(paramsType=(qi.String,) , methodName="bar")
  def bar2(self, arg):
    pass

このように書くと関数bar1は「32ビット型の整数を引数にとる"bar"という名前の関数」として登録され、bar2は「文字列を引数にとる"bar"という名前の関数」として登録されます。Pythonの流儀ではこのような名前の被ったメソッドは基本的にご法度ですが、qi Frameworkは引数の型を区別して正しい方を呼び出せるため問題は起きません。

 

例2: 戻り値の型を明示する

第二の例では関数の戻り値を明記しています。

class MyFoo:

  #※このままだと動かない!
  @qi.bind(returnType=qi.String)
  def bar(self, arg):
    pass

これでbarという関数は戻り値が文字列であることを明示しています…が、上の例だと実装はそうなっておらずNoneを返すので実際に呼び出すとエラーが起きます。宣言に合わせた戻り値を返すようにしましょう。

class MyFoo:
  #こっちならOK
  @qi.bind(returnType=qi.String)
  def bar(self, arg):
    return "this is bar method"

上の例では戻り値がある場合の典型例を紹介していますが、戻り値が無い事はqi.Voidを使って同様に表現できます。

 

例3: 引数の型を明記する

第一の例でも出てきましたが、引数の型を明記することが出来ます。複数の引数がある場合はタプルとして指定します。

class MyFoo:

  #this function take a string and an int. All others arguments types
  #will be rejected even before calling the method.
  @qi.bind(paramsType=(qi.String, qi.Int32))
  def bar(self, arg1, arg2):
    pass

 

例4: 関数を登録したくない

クラスの関数の中でもqiのサービスとしては触って欲しくない関数がある場合にそれを隠すことが出来ます。C++等で言う所のprivateに相当する処理と考えればすんなり納得できると思います。

class MyFoo:

  def bar(self, arg1, arg2):
    pass

  @qi.nobind
  def _privateOfHellBar(self, arg1, arg2):
    pass

このように書くと_privateOfHellBar関数はセッション越しでは呼び出せません。

   

まとめ

色々書きましたが、結論としては「自作機能を作りたければPythonの通常のクラスを定義すればOK」という簡単な結論が出ました。そんなに難しいわけじゃない、という事が伝わっていれば幸いです。

記事読んでもよく分からない点や間違ってる箇所の指摘等ありましたらコメントにてお知らせください。