サーバ型PaPeRo i制御用ライブラリについて

 クラウドアプリからPaPeRo iを制御するで試作したサーバ型PaPeRo i制御用ライブラリについてもう少しご紹介します。

複数アプリの振り分け

 従来のpypaperoの場合、アプリのインスタンスが作られて、その中でWebSocketクライアント接続を行う形でしたが、サーバ型の場合、接続が先にでき、それを契機にアプリのインスタンスを生成して接続と結びつけています。アプリはSsPyPaperoを継承したクラスとして作る必要がありますが、動作が異なる複数のアプリクラスを作っておいて、パペロの識別子などでアプリを振り分けることができます。
 これはServer.set_app()で指定できるようにしています。アプリにはアプリ名をつけ、アプリ名とアプリクラスの対応をapp_dictに指定し、パペロの識別子とアプリ名の対応をapp_assignに指定します。例えばpapero1がパペロ識別子、app1/2がアプリ名、App1/2がアプリクラス、として、papero1にApp1を、その他のパペロをApp2に割り振る場合には以下の様に指定します。

server = Server(default_port=8860)
server.set_app(app_assign=[('papero1', 'app1'), ('', 'app2'), ],
               app_dict={'app1': App1, 'app2': App2},
               )

 app_dictが辞書なのにapp_assignがリストなのは、割り当ては単なる対応ではなく、デフォルトのアプリ(上記ではApp2)等を記述できる様にするためです。

動作中のアプリの切り替え

 WebSocketの接続を契機にアプリのインスタンスを生成して接続と結びつけていると書きましたが、この操作は接続時に限らずいつでも可能です。つまりサーバプロセスを止めたりすることなく、特定のパペロの動作中のアプリをいつでも切り替えることができます。アプリをTornadoのポリシー通り、単一スレッドノンブロッキング動作で作成できていれば、切り替えにより不正な動作が発生する心配もほとんどないと思われます。切り替えを行うスレッドもTornadoのスレッドで動くので、ということは切り替え対象のパペロアプリが何かの処理の途中で止まったりしていることは無いとことが保証されるからです。
 この動作中のアプリの切り替えは、接続用クラスのクラスメソッドSsWsHandler.change_app_for_papero()で可能です。これはアプリクラスを指定し、新しく生成したアプリインスタンスを割り当てるAPIで、現状はこれしか用意していませんが、動作中のインスタンスを一時外しておいて割り込み用アプリを走らせ、終わったら一時外していたインスタンスに戻す、といったことも簡単にできる見込みです。
 以下は後述のデモアプリでWeb画面からアプリの切り替えを行う部分のコードです。

def on_app_start_btn(self):
    gid = self.gid.val
    uid = self.uid.val
    app_name = self.app_name.val
    logger.debug('change app for papero:{} to app:{}'.format('{}:{}'.format(gid, uid), app_name))
    SsWsHandler.change_app_for_papero(gid, uid, app_name)

複数のパペロの操作

 サーバ型は本質が複数のパペロを扱う動作であるため、従来のpypaperoで複数のパペロを操作する場合とは違いがあります。
(1) 不定数のパペロを扱える
 pypaperoでは操作する台数(少なくとも上限)が限定されましたが、サーバ型ではその時接続しているパペロに一斉にしゃべらせる、といったことが可能です。
(2) ネットワーク設定が簡単
 pypaperoでは基本的に操作対象のパペロのIPアドレスを知っている必要があり、それによってパペロを識別しますが、サーバ型では中継クライアントが送ってくるパペロの識別子でパペロを識別します。一見同じことに思えますが、大きな違いがあります。
 パペロの識別子をMACアドレスなど個体により異なる番号にすることで、パペロに個別の設定を行うことなく、全て同じソフト、設定で出荷し、サーバ側だけでシステムの設定を行うことが可能になります。
 これはIPアドレスの固定が不要で、パペロをWANポート接続でDHCPクライアント動作で使用することができることにもよります。またWi-Fi子機設定であっても特別な設定なしに接続が可能です。

 実際の操作としては、パペロの識別子からアプリのインスタンスを見つけ、直接send_start_speech()などの操作を行う方法も可能ではありますが、それよりも、正規表現で対象のパペロを指定してユーザ定義イベントを投げ、アプリ側のイベント処理として、そのイベントに対する処理を記述する方法をお勧めします。このユーザ定義イベント送信は、SsWsHandler.send_user_msg_to_paperos()です(コードは後述)。以下はアプリのイベントの受信処理の方の例で、イベント送信時に’_speech’としゃべる内容を指定した場合にこの様にイベントを扱いしゃべらせることができます。

    def in_wait(self, name, msg):
        logger.debug('event: {}'.format(name))
        if name == 'detectButton':
            ...
        elif name == '_timer':
            ...
        elif name == 'getSensorValueRes':
            ...
        elif name == '_speech':
            txt = msg.get('UserData')
            if txt is None:
                self.speech('スピーチメッセージを受信したよ')
            else:
                self.speech(txt)

デモ

 sspypaperoapp1/2に記述されている二つのアプリの切り替えと、接続しているパペロに一斉にしゃべらせることがweb画面からできる例です。Web画面にはwebconfを利用しています。

from logging import (getLogger, debug, info, warn, error, critical,
                     DEBUG, INFO, WARN, ERROR, CRITICAL, basicConfig)

from webconf import (Page, ConfDef, Item, Button, SelectBoxByList, Table, Text,
                     TextArea, selectboxbylist_item)

from sspypapero import Server, SsWsHandler

import sspypaperoapp1
import sspypaperoapp2
import applist

logger = getLogger(__name__)


class SimpleConf(ConfDef):
    def __init__(self, filename=None, doload=True):
        Item.set_item_default(eot=' ', eol='')
        self.gid = Text('パペログループ', val='')
        self.add_br()
        self.target_re = Text('しゃべるメッセージ送信:パペロ選択指定', val='.*')
        self.speech_txt = Text('メッセージ', val='こんにちは')
        self.send_btn = Button('送る', onclick=self.on_send_btn)
        self.add_br()
        self.uid = Text('アプリ切換え:パペロ識別子', val='')
        self.app_name = SelectBoxByList('アプリ名', val='', lst=self.get_app_select_list())
        self.app_start_btn = Button('アプリ開始', onclick=self.on_app_start_btn)
        self.enumdic = {
        }
        super(SimpleConf, self).__init__(filename=filename, doload=doload)

    def on_app_start_btn(self):
        gid = self.gid.val
        uid = self.uid.val
        app_name = self.app_name.val
        logger.debug('change app for papero:{} to app:{}'.format('{}:{}'.format(gid, uid), app_name))
        SsWsHandler.change_app_for_papero(gid, uid, app_name)

    def on_send_btn(self):
        gid = self.gid.val
        uidre = self.target_re.val
        txt = self.speech_txt.val
        SsWsHandler.send_user_msg_to_paperos(gid, uidre, '_speech', txt)

    def get_app_select_list(self):
        lst = [selectboxbylist_item(disp=x[0], value=x[0]) for x in applist.app_cls_list]
        return lst


if __name__ == '__main__':
    confobj = SimpleConf('conf.json')
    page = Page('/conf', title='設定', conf_obj=confobj, enumdic=confobj.enumdic)
    conf_handlers = page.get_handlers()
    server = Server(default_port=8860)
    server.set_app(app_assign=[('p.*', 'app2'), ('', 'app1'), ],
                   app_dict={x[0]: x[1] for x in applist.app_cls_list},
                   )
    server.extend_handlers(conf_handlers)
    server.start()

 なお中継クライアントは以下の様に起動します。今回は簡単にローカルで動作させています。

> wsrelaycli.exe -wssvr ws://192.168.1.1:8088/papero -cloud ws://localhost:8860/ws/papero -papero papero2

まとめ

 サーバ型ライブラリは、実装作業としては通信の方向を変えただけというとらえ方もできるのですが、上記の通り動作としては大きな違いがあり「PaPeRo iアプリサーバ」と呼んだ方が本質を表しているかもしれません。
 当初クラウドから操作する目的で作成したのですが、できてみると、LAN環境が整った大きな施設で沢山のパペロを使ったサービスを、サーバも込みで提供するといった形により適している様に感じます。


0