20
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Choregraphe プロジェクトの中にカスタムサービスを組み込む

Last updated at Posted at 2016-04-17

背景

NAOqi SDK の中に qipkg というツールがあります。これは コマンドラインで Pepper 用のアプリのパッケージを構築し、本体にインストールまでしてくれるという便利なツールなのですが、この qipkg が構築してくれるパッケージの形態が 「ロボアプリ」 ではなく 「サービス」 なのです。

qipkg がサービスをパッケージできるのであれば、Choregraphe からもできるのではということで試してみたところうまくいったのでその方法の紹介しようと思います。

qipkg については次のドキュメントを参考(英語)、ローカルならば、Choregraphe ヘルプドキュメントから qipkg で検索しても同じドキュメントをみれると思います。

誰に役に立つの?

今回の手法は次のような方に参考になると思います

  • Pepper アプリのコードベースでの開発手法を模索している。特にバージョン管理システムなどと親和性の高い手法を模索している
  • C++ プログラムを Choregraphe アプリに組み込みたい
  • オートノマスライフ起動時も稼働している独自の常駐プロセスを設定したい

#注意!

ここで紹介する方法はオフィシャルドキュメントには紹介がない手法なので、現状はちょっとした裏技的なものと考えてください。 ここで紹介する手法を使うと C++ プログラムを Choregraphe アプリの中に追加することも可能です。 C++ サービスの組み込みは 個人で利用する分には全く問題ありませんが、For Biz パートナーアプリ は現状 C++ プログラムの審査をサポートしていないと思われますので、公開アプリを作ろうとお考えの場合、注意をしてください

前提(対象)

この記事は Pepper のアプリ開発の基本を理解されている方を対象にしています。

サービスとは

Pepper 上で動くロボット OS NAOqi は分散オブジェクトの思想に基づいて構築されています。
NAOqi が提供する各機能は、「サービス」という形で常駐しており、各アプリは、この「サービス」内の「メソッド」にリクエストを送ることで各機能にアクセスしています。「サービス」は Pepper ロボアプリ開発を始めるとやがて耳にする「ALMemory」がその代表的なものの一つ、これ以外に例えば Pepper を喋らせるためのサービス「ALTextToSpeech」などがあります。
この「サービス」、自分で作ることも可能です。自分で作った「サービス」も他の「サービス」と同様、いろいろなアプリに機能を提供することができます。一つのサービスは一つのプロセスで動作し、それぞれ独立しています。 C++ で作られたサービスを Python のプログラムから呼び出すということも簡単にできます。

以下、サンプルアプリを作りながら具体的な方法を説明します。

サンプルアプリ手順1 プロジェクトプロパティーの設定

今回の作業では manifest.xml ファイルを直接編集します。このファイルはアプリの名前や、日本語対応アプリかなど、プロジェクトから作られるパッケージの様々な基本情報を記述するためのファイルで、通常は Choregraphe 「プロジェクトファイル」ビューの [プロパティ] ボタンを押したときに開かれる「プロジェクトのプロパティ」 ウィンドウを通して編集します。
manifest.xml ファイルを直接編集した内容が、「プロジェクトのプロパティ」 の設定により上書きされる可能性が完全にないわけではないので、「プロジェクトのプロパティ」 の設定は今回の作業に入る前に可能な限り済ませておくことをお勧めします。

わたしは今回 「対応言語」を日本語と、英語にし、ロボアプリ名 "ServiceInChoregrapheSample"、 アプリケーションID を serviceinchoregraphesample としました。

スクリーンショット 2016-04-17 9.44.08.png

サンプルアプリ手順2 サービス用のフォルダーを追加

必須ではありませんが、サービスはボックスライブラリとは独立して動作しますので、プロジェクトのファイル構成上も他とは別の場所に保存しておいたほうがわかりやすいと思います。
services というフォルダーをプロジェクト内に作成し、サービスプログラムはこの配下に保存することとします。

スクリーンショット 2016-04-17 9.56.38.png

サンプルアプリ手順3 カスタムサービスを services ディレクトリ内に保存

今回は例として次のようなシンプルな Python で作られたサービスを services ディレクトリ内に保存します。 ファイル名は myservice.py とします。
(まず プロジェクトを任意の名前で保存([ファイル] -> [プロジェクトを保存])、次に services フォルダーを右クリック、メニューから[エクスプローラーで表示] を選び services フォルダーのシステム内での場所を特定、テキストエディターなどでここにファイル myservice.py を作り編集)

myservice.py
# -*- coding: utf-8 -*-

import qi
import sys

# クラス名は manifest.xml のサービス名と一定していること
class MyService:
    def __init__(self, session):
        self.session = session
        self.logEventName = "%s/log" % self.__class__.__name__
        self.memory = self.session.service("ALMemory")
        
        #イベント登録 
        self.middlehead_signal = self.memory.subscriber("HandLeftBackTouched").signal
        self.middlehead_signal.connect(self._leftTouchedCallback)
        
        self._logger("MyService Ready..")
                                
    def _leftTouchedCallback(self,value):
        self._logger("_leftTouchedCallback called.") 
        if int(value) > 0:
            tts = self.session.service("ALTextToSpeech")
            tts.say("左手")

    #ログ出力用 (Choregrahe クラス名/log でログメッセージを伝播)
    def _logger(self, msg):
        self.memory.raiseEvent(self.logEventName, msg)
        
    #サービスメソッド
    def myMethod(self,value):
        self._logger("myMethod called.")
        tts = self.session.service("ALTextToSpeech")
        tts.say(value)
    
if __name__ == "__main__":
    app = qi.Application(sys.argv)
    app.start()
    myservice = MyService(app.session)
    app.session.registerService(myservice.__class__.__name__, myservice) 
                
    app.run()   # will exit when the connection is over

中身について簡単に解説

(使い回し可能:サービスクラス定義) 今回 MyService というカスタムサービスを定義しています。以下のコードはそのまま使い回していただけるかと思います。 MyService のところはご自身で作られるサービスの名前に置き換えます。

# -*- coding: utf-8 -*-

import qi
import sys

# クラス名は manifest.xml のサービス名と一定していること
class MyService:
    def __init__(self, session):
        self.session = session
        self.logEventName = "%s/log" % self.__class__.__name__
        self.memory = self.session.service("ALMemory")

(サンプル:イベントハンドリング処理)一つのサンプルとして、このサービスでは左手が触られたことに反応するようにしています。 左手が触られた時「左手」としゃべるようにしています。

        #イベント登録 
        self.middlehead_signal = self.memory.subscriber("HandLeftBackTouched").signal
        self.middlehead_signal.connect(self._leftTouchedCallback)
        
        self._logger("MyService Ready..")
                                
    def _leftTouchedCallback(self,value):
        self._logger("_leftTouchedCallback called.") 
        if int(value) > 0:
            tts = self.session.service("ALTextToSpeech")
            tts.say("左手")

(使い回し可能:ログ出力関連)カスタムサービスを使っていて一つ個人的に困ったのが、簡単に利用できるログの吐き出しの方法がないということでした。一つの解決策として、 self._logger("ログメッセージ") とすると "サービス名/log" で ALMemory にログを書き込むようにしました。この箇所もそのまま他サービスでも使い回していただけるかと思います。

    #ログ出力用 (Choregrahe クラス名/log でログメッセージを伝播)
    def _logger(self, msg):
        self.memory.raiseEvent(self.logEventName, msg)

(サンプル:サービスメソッド)このサービスが提供するメソッドとして myMethod とうメソッドを用意、ここではパラメータで渡された文字列をしゃべるようにしました。 メソッドは Choregraphe ボックス内のスクリプトなどから自由に呼び出すことができます。 Python で記述したサービスにおいては、先頭に _ が付いていないサービスクラスのメソッドは基本的にサービスメソッドとして外部から呼び出すことができます。

    #サービスメソッド
    def myMethod(self,value):
        self._logger("myMethod called.")
        tts = self.session.service("ALTextToSpeech")
        tts.say(value)

(使い回し可能:サービス起動処理)一つのサービスは一つの独立したプロセス上で動作します。一つのプロセス上でサービスとしてここまでで定義したクラスを起動するための処理が以下になります。 MyService(app.session) のところを自身で定義した任意のクラス名に置き換えることでこの箇所も使い回し可能かと思います。

if __name__ == "__main__":
    app = qi.Application(sys.argv)
    app.start()
    myservice = MyService(app.session)
    app.session.registerService(myservice.__class__.__name__, myservice) 

    app.run()   # will exit when the connection is over

サンプルアプリ手順4 manifest.xml にサービス起動用の記述を追加

ここが今回の目玉です。
プロジェクトの manifest.xml を直接編集します。プロジェクト内の manifest.xml は Choregraphe 内でダブルクリックして開こうとすると 「プロジェクトのプロパティ」 ウィンドウが開かれますので、必要な変更を加えることができません。
manifest.xml ファイルを右クリック、メニューから[エクスプローラーで表示] を選びシステムのファイルエクスプローラーを開き、manifest.xml ファイルをテキストエディターなどから開いてください。
次に下記 <services> .. </services> を追加します。

manifest.xml
<?xml version='1.0' encoding='UTF-8'?>
<package version="0.0.0" uuid="serviceinchoregraphesample-8cdad2">
 <names>
  <name lang="en_US">ServiceInChoregrapheSample</name>
 </names>
 <supportedLanguages>
  <language>en_US</language>
  <language>ja_JP</language>
 </supportedLanguages>
 <descriptionLanguages>
  <language>en_US</language>
  <language>ja_JP</language>
 </descriptionLanguages>
 <contents>
  <behaviorContent path="behavior_1">
   <userRequestable/>
   <nature>interactive</nature>
   <permissions/>
  </behaviorContent>
 </contents>
  <services>
  <service autorun="false" name="MyService" execStart="/usr/bin/python2 services/myservice.py"/>
 </services>
</package>

中身について簡単に解説

以下が追加した箇所。

manifest.xml
  <services>
  <service autorun="false" name="MyService" execStart="/usr/bin/python2 services/myservice.py"/>
 </services>
  • autorun : サービスを自動起動させるかどうか。 true にすると Pepper 起動時にサービスは自動起動されます。 一度起動されたサービスは明示的に停止処理をしない限り常に動いています。 true にする主な用途はオートノマスライフのカスタマイズ。以前サービスを常駐させる方法として autoload.ini を編集する方法 を紹介しましたが、これに代替する手法になります。
  • name : ここでサービスの名前を指定します
  • execStart : サービスを起動するための処理を記述します。今回は Python のサービスを定義したので、python ランタイムからスクリプトを実行させています。 C++で構築されたサービスであれば、その実行ファイル名をここに直接記述すればいいわけです。

サンプルアプリ手順5 Choregraphe アプリ側の処理:サービスを起動

今回のサービスは自動起動ではないので、必要な時に明示的に起動する必要があります。今回これを Choregraphe のボックスライブラリから行うようにします。

Python Script ボックスを一つ作ります

img2.jpg

ボックスをダブルクリック、次のスクリプトを設定します

## autorun="false" で定義されたサービス用の起動スクリプト
## (autorun="true" で定義されたサービスは自動起動されているので、このスクリプトは必要ありません)

class MyClass(GeneratedClass):
    def __init__(self):
        GeneratedClass.__init__(self)

    def onLoad(self):
        self.serviceName = "MyService"  # ここに起動するサービスの名前

        self.memory = self.session().service("ALMemory")
        self.serviceLog_signal = self.memory.subscriber("%s/log" % self.serviceName).signal
        self.serviceLog_linkId = self.serviceLog_signal.connect(self._logMessageCallback)

    def _logMessageCallback(self, msg):
        self.logger.info("[%s]: %s" % (self.serviceName, msg))

    def onUnload(self):
        try:
            self.serviceLog_signal.disconnect(self.serviceLog_linkId)

            serviceManager = self.session().service('ALServiceManager')
            if serviceManager.isServiceRunning(self.serviceName):
                self.logger.info("Stop service: %s" % self.serviceName)
                serviceManager.stopService(self.serviceName)
        except RuntimeError:
            self.logger.warning('error occured when trying to stop the service: ' + self.serviceName)

    def onInput_onStart(self):
        import time
        try:
            serviceManager = self.session().service('ALServiceManager')
            if serviceManager.isServiceRunning(self.serviceName):
                self.logger.info("Service %s is already running." % self.serviceName)
                self.onStopped()
            elif serviceManager.startService(self.serviceName) == True:
                for i in range(5):
                    time.sleep(0.5)
                    if serviceManager.isServiceRunning(self.serviceName):
                        #self.logger.info("Service %s started." % self.serviceName)
                        self.onStopped()
                        break
                    else:
                        self.logger.info("Waiting for starting up service %s ..." % self.serviceName)
            else:
                raise RuntimeError('Could not find or start the service')
        except RuntimeError:
            self.logger.error('Could not start service: ' + self.serviceName)

    def onInput_onStop(self):
        self.onUnload() #it is recommended to reuse the clean-up as the box is stopped
        self.onStopped() #activate the output of the box

このボックスは、入力が呼ばれたタイミングでサービスを呼び出し、アプリが終了するタイミング(またはこのボックスがアンロードされるタイミング)でサービスを停止します。

基本使い回しできるように作っています。次の箇所を任意に書き換えることでオリジナルのサービスを起動、停止できるかと思います。

        self.serviceName = "MyService"  # ここに起動するサービスの名前

サンプルアプリ手順6 Choregraphe ボックスからサービスメソッドを呼ぶ

ボックスからサービスに定義したメッド myMethod を呼び出す一つの例を示します。

まず Python Script ボックスを追加します。

img3.jpg

次のスクリプトを設定します。このボックスでは入力が呼ばれた時、サービスの myMethod() メソッドを呼び出すようにしています。パラメータに "右手" を渡しています。今回のサービスはパラメータで与えられた文字列を発話するように作られているので、結果として、Pepper は「右手」と発話します。

class MyClass(GeneratedClass):
    def __init__(self):
        GeneratedClass.__init__(self)

    def onLoad(self):
        pass

    def onUnload(self):
        #put clean-up code here
        pass

    def onInput_onStart(self):
        self.ms = self.session().service("MyService")
        self.ms.myMethod("右手")

        self.onStopped() #activate the output of the box

    def onInput_onStop(self):
        self.onUnload() #it is recommended to reuse the clean-up as the box is stopped
        self.onStopped() #activate the output of the box

サンプルアプリ手順7 アプリを完成させる

次のように結線して Choregraphe アプリ側を完成させます

スクリーンショット 2016-04-17 12.36.01.png

アプリを試す

アプリを再生して試します。

左手甲を触ると「左手」と発話(サービスがイベントハンドル、発話)、右手甲を触ると「右手」(Choregrphe アプリがイベントをハンドル、サービスメソッドを呼び出し、サービスが発話)すると正常な動作です。

インストールすると

インストールしてみます。サービスは次のスクリーンショットにあるように通常のビヘービアとは異なるアイコンで表示されます。

img1.jpg

なお Choregraphe の再生ボタンを押してアプリを実行した時、アプリは本体に .lastUploadedChoregrapheBehavior という暫定のアプリ名でインストールされ実行されています。この場合、この暫定アプリにもやはりサービスがインストールされており、結果、アプリをインストールをした場合、プロジェクトのアプリパッケージと暫定のパッケージの両方に同じ名前のサービスが存在することになります。これは特にサービスを autorun="true" で常駐するように定義した場合、混乱する場合があるので注意してください。 暫定、インストールアプリ、どちらかを使う場合、使わない一方のパッケージは「ロボアプリ一覧」で該当アプリを選択、ゴミ箱ボタンを押してアンインストールしておくことをお勧めします。

サンプルソース

一連の作業を完成させたサンプルプログラムを以下に保存しておきました。

20
20
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?