クラウドアプリからPaPeRo iを制御する

 Node-RED編Tornado/Heroku編中継クライアント改良編go版中継クライアント編と続けて

 アプリ → クラウド(中継) ← 中継クライアント → PaPeRo i(WebSocket通信アドオンシナリオ)

という形でWebSocket制御通信レイヤでデータ中継してPaPeRo iを制御する方法を紹介しましたが、今回はアプリをクラウドで動かす方法です。つまり、

 クラウド(アプリ) ←中継クライアント→ PaPeRo i(WebSocket通信アドオンシナリオ)

という形で、クラウド上のアプリからWebSocket通信でPaPeRo iを制御します。
 クラウドは今回はIBM Cloudライト・アカウントでCloud Foundry(PaaS)を、中継クライアントはgo版、ホストはUbuntu16.04を使用しました。

サーバ型通信ライブラリ

 従来の、通信ライブラリpypaperoを使用したアプリは、WebSocketクライアントとして動作しWebSocketサーバのPaPeRo i(WebSocket通信アドオンシナリオ)に接続していましたが、今回は中継クライアントがクラウドに接続に来るので、WebSocketサーバとして動作する通信ライブラリが必要になります。基本部分は共通なのでpypapero.Paperoクラスの派生クラスを作り、必要な部分をオーバーライドする事でこの接続形態に対応する通信ライブラリを試作しました。
 WebSocketライブラリは今回もTornadoを使用します。Tornadoは単一スレッド、ノンブロッキング動作であり、アプリのコードもそれを意識する必要があります。具体的にはマルチスレッドやtime.sleepによる遅延などは単純には使用できません。
 とはいえ、当然必要になる場合があると思います。今回処理の遅延についてはライブラリにTornadoの機能を利用したタイマー機能を実装しました。マルチスレッドが必要な場合はTornadoのadd_callback()というAPIを使用すると別スレッドからTornadoスレッド上での関数実行を要求できるのでそれを利用してください。
 なお今回は状態遷移モデルを前提としたライブラリとしています。また終話検知機能もライブラリに実装しました。

ライブラリの使い方

(1) 初期化
 SsPyPaperoを継承したクラスでアプリを作成する必要があります。初期化はinitialize()で行います。ここで、状態self.Stateの定義、各状態に対してイベント処理メソッドを結びつける辞書self.state_func_dicを定義する必要があります。以下はアプリの状態INIT、WAIT、SPEECHとしている例です。

from sspypapero import Server, SsPyPapero

class App1(SsPyPapero):
    def initialize(self):
        self.State = enum.Enum("State", "INIT WAIT SPEECH")
        self.state = self.State.INIT
        self.state_func_dic = {
            self.State.INIT: self.in_init,
            self.State.WAIT: self.in_wait,
            self.State.SPEECH: self.in_speech,
        }

(2) 初期状態の処理
 アプリの初期状態INITでの処理(ここではin_init)では、Readyイベントを待って、処理を開始する必要があります。状態変数self.stateは代入で書き換えても良いですが、trans_state()を使うとデバッグログが出力されます。

    def in_init(self, name, msg):
        if name == 'Ready':
            self.speech('こんにちは')
            self.trans_state(self.State.SPEECH)

(3) 終話検知とタイマー
 self.paperoが従来のpypapero.Paperoオブジェクトなので例えばself.papero.send_start_speech()等でパペロを制御できますが、発話についてはself.speech()を使うことで終話検知ができます。
 終話イベントは”_endOfSpeech”です。ここでは最初に”こんにちは”としゃべり、しゃべり終わったら待ち状態に入ります。待ち状態では真ん中ボタンで単にしゃべり、右ボタンではセンサー値を取得した上でしゃべります。
 左ボタンではタイマーをかけ、2秒後にしゃべります。タイマーセットset_timer()の第一引数は待つ秒数、第二引数はタイマー識別子です。タイマーイベントは”_timer”で、この時msg.get(‘TimerID’)でタイマー識別子を取得できるので、複数のタイマーを同時に使う場合にはこれで判別できます。

    def in_wait(self, name, msg):
        logger.debug('event: {}'.format(name))
        if name == 'detectButton':
            logger.info("APP detect button")
            status = msg.get('Status')
            if status == 'C':
                self.speech('まん中のボタンが押されたよ。')
                self.trans_state(self.State.SPEECH)
            elif status == 'L':
                self.papero.send_get_sensor_value()
            elif status == 'R':
                self.set_timer(2, '1234')
        elif name == '_timer':
            if msg.get('TimerID') == '1234':
                self.speech('タイマーメッセージ')
                self.trans_state(self.State.SPEECH)
        elif name == 'getSensorValueRes':
            temp = msg.get('TEMP')
            rh = msg.get('RH')
            ir = msg.get('IR')
            irlst = ir.split(',')
            irpos = irlst[-1]
            self.speech(
                '今の温度は{}度、湿度は{}%、赤外線センサーは{}です。'.format(temp, rh, irpos))
            self.trans_state(self.State.SPEECH)

    def in_speech(self, name, msg):
        if name == '_endOfSpeech':
            self.trans_state(self.State.WAIT)

 この例は実質的な状態は1つで状態遷移を活用できていませんが、実際のアプリでは状態遷移で処理を分ける必要がある場合が多いと思います。

(4) 起動
 起動はServerクラスのインスタンスを作成し、アプリのクラス(インスタンスではありません)をset_appでセットし、start()してください。
なおライブラリ側で引数–portによりポート番号を指定できる様にしています。app_assignはパペロ識別子に対して割り当てるアプリ名を記述しますが、ひとまず以下の通りとしてください。app_dictは自アプリ名をキーとしてアプリのクラスを登録してください。

if __name__ == '__main__':
    server = Server()
    server.set_app(app_assign=[('', 'app1'), ], app_dict={'app1', App1})
    server.start()

接続時の動作

 中継クライアントから接続が来ると、アプリのインスタンスが生成されて、その接続に結びつけられます。Tornadoのアーキテクチャにより、プロセスやスレッドは作られません。なお、中継クライアントから送られるパペロの識別子はself.papero_idに保持されています。

デプロイ手順

(1) Cloud Foundryアプリの作成
 Python/TornadoのプログラムをIBM Cloudで動かすためには、カタログからCloud Foundry アプリのPythonを選択し、名前を決めて作成します。ここではpaperoi-server-appとします。同じ名前では作成できないので独自の名前を決め、以後読み替えてください。メモリは256Mまで指定できますが、64Mとしました。
(2) CLIインストール
アプリを作成すると表示される「開始」ページの、「Bluemixコマンド・ライン・インターフェースのダウンロード」ボタンからインストーラーをダウンロードして表示されている手順通りに実行します。bxコマンドが使用できるようになります。
(3) ログイン

$ bx login

(4) ターゲットの指定

$ bx target --cf

(5) ファイルの用意
新しいディレクトリを作成して必要なファイルを用意します。

ライブラリ及びアプリファイル:
 ダウンロード、解凍して作成したディレクトリ直下に置いてください。
whlパッケージが./localディレクトリにあることを確認してください。

Procfile:

web: python sspypaperoapp1.py --port=$PORT

manifest.yml:

---
applications:
 - name: paperoi-server-app
   random-route: true
   memory: 64M

requirements.txt:

tornado==4.5.3
ws4py==0.4.3
./local/sspypapero-0.0.1-py3-none-any.whl

 ws4pyは使いませんが、pypapero.pyが参照しているため必要です。

runtime.txt

python-3.6.2

setup.py:

setup(
    name='paperoi-server-app',
    version='0.0.0',
    description='paperoi server app',
)

(6) デプロイ

bx app push paperoi-server-app

(7) アプリの停止

bx app stop paperoi-server-app

(8) アプリの起動

bx app start paperoi-server-app

(9) アプリの状態表示

bx app list

 
ライトアカウントは1インスタンスという記述も見かけるのですが、メモリ64Mで作成した場合には4インスタンスまで動かすことができる様です。

動作確認

(1) サーバアプリを上記手順で起動します。
(2) 使用するPaPeRo iに、最新のLinuxの/etc/sslをコピーします。電源OFFしても良いようにするためには/Extension以下に置いておき、起動スクリプトで/etc/sslにコピーする必要があります。
(3) PaPeRo iでインターネットへ接続できることを確認します。
(4) PaPeRo iの時刻を合わせます。時刻がずれているとクラウドへの接続ができません。

# date 2018.3.26-10:20

(5) PaPeRo iにgo版中継クライアントwsrelaycliを転送し、起動します。-wssvrでパペロのURL、-cloudでサーバURL(/ws/papero)、-paperoで任意のパペロ識別子を指定します。

# chmod +x wsrelaycli
# ./wsrelaycli -wssvr ws://0.0.0.0:8088/papero -cloud wss://paperoi-server-app.mybluemix.net/ws/papero -papero 12345678

 但し-cloudには実際は(1)のアドレスを指定してください。

 これで複数のPaPeRo iが作成したアプリで動作することが確認できました。ブロードバンド回線のLANで動作させる分には遅延はほとんど気にならない程度で、実用性はあると思われます。
 なお通信ライブラリは試作段階のため、仕様が変わる可能性があります。


0