センサー値をリアルタイムグラフ表示する

PaPeRo iのセンサー値をWeb画面にリアルタイムで表示する例を紹介します。

構成

 PaPeRo i上のWebサーバにアクセスするとブラウザ上にPaPeRo iのセンサー値のグラフがリアルタイムで更新される様にする方法を考えます。
画面遷移を伴わずにブラウザの画面を変化させるにはJavaScriptを使う必要があり、またセンサーデータはWebSocketでPaPeRo i→ブラウザの方向で送れば遅延なく画面に反映できます。WebSocketを使う場合、ブラウザ側がクライアントなのでPaPeRo iがサーバになる必要があります。
 つまりPaPeRo i本体またはPC等で、

  • センサーグラフ画面の表示のためのWebサーバ
  • センサーデータ通信のためのWebSocketサーバ
  • センサーデータをWebSocketで送るPaPeRo iアプリ

を動かし、HTML/JavaScriptでセンサーデータの画面表示・更新を行います。

Webサーバ

 Webサーバ、WebSocketサーバにはTornadoを使用します。今回Webサーバは固定のhtmlファイルとjsファイルを公開するだけなので、固定ファイル用のStaticFileHandlerを使用する方法もありますが、今回は普通のRequestHandlerを使いました。この場合、Webに公開するするhtml/jsのファイルごとに、以下の様にクラスを作るだけです。

class FileHandler(tornado.web.RequestHandler):
    def get(self):
        self.render(公開するファイル名)

今回必要なファイルは後述の’paperograph.html’、’paperograph.js’、’smoothie.js’です。URLと結びつけるのはWebSocketと同時にやります。

WebSocketサーバ

 ブラウザで’paperograph.html’を開いた時点で’paperograph.js’からWebSocketサーバに接続に来ることにします。今回必要なのはセンサーデータを全クライアントにブロードキャストする事だけなので、WebSocketサーバクラスのクラス変数clientsでコネクションを管理し、ブロードキャスト用のメソッドinvoke_broadcast()を用意します。

class WsHandler(tornado.websocket.WebSocketHandler):
    clients = []

    def initialize(self):
        """ initialize (called by tornado) """
        self.clients.append(self)

    def on_close(self):
        logger.debug("WsHandler")
        self.clients.remove(self)

    def send(self, msg, binary=False):
        logger.debug("WsHandler msg:{}".format(msg))
        super().write_message(msg, binary)

    @classmethod
    def broadcast(cls, msg):
        for con in cls.clients:
            con.send(msg)

    @classmethod
    def invoke_broadcast(cls, msg):
        tornado.ioloop.IOLoop.current().add_callback(cls.broadcast, msg)

なおブロードキャストはtornadoのスレッド内からであればbroadcast()で良いのですが、invoke_broadcast()では別スレッドから呼べる様にadd_callback()を挟んでいます(tornadoの仕様です)。
 tornadoスレッドは以下の様にURLとポート番号を指定して起動します。

def start_web():
    app = tornado.web.Application([
        (r'/paperograph.html', HtmlHandler),
        (r'/paperograph.js', JsHandler),
        (r'/smoothie.js', Js2Handler),
        (r'/__ws', WsHandler)
        ],
        template_path=os.path.dirname(__file__),
    )
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()

PaPeRo iアプリ

 温度、湿度、人感、照度のグラフを表示することにします。照度以外はsend_get_sensor_value()でまとめて取得出来ます。照度センサーはsend_start_lum_sensor()、send_get_lum_sensor_value()、send_stop_lum_sensor()を、レスポンスを待って順に実行する必要がある様です(send_stop_lum_sensor()は省略可能?)。これらを全てシーケンシャルに実行するには状態遷移プログラムとして記述すると簡潔に出来ます。このために、今回はpypapero.pyのラッパークラスstpypapero.pyを使いました。これはサーバ型通信ライブラリの内部で使用していたものを、さらに簡潔に使用できる様にしたものです。self.stateの書き換えで状態遷移する方法をやめ、self.handlerを状態ごとのイベント処理メソッドで書き換えることで状態遷移する方法としました。これで状態のenum定義が不要になりました。c言語などでは状態が分かり辛くなるので良くありませんが、Pythonではstr(self.handler)でメソッド名が文字列で見えるためこれで困りません。

class SensorApp(StPyPapero):
    def initialize(self):
        self.handler = self.in_init

    # 状態遷移メソッド
    def go_lum_start(self):
        self.papero.send_start_lum_sensor()
        self.trans_state(self.in_lum_start)

    def go_lum_get(self):
        self.papero.send_get_lum_sensor_value()
        self.trans_state(self.in_lum_get)

    def go_lum_stop(self):
        self.papero.send_stop_lum_sensor()
        self.trans_state(self.in_lum_stop)

    def go_sensors(self):
        self.papero.send_get_sensor_value()
        self.trans_state(self.in_sensors)

    def go_time_wait(self):
        self.set_timer(0.1, 'tm0')
        self.trans_state(self.in_time_wait)

    # 状態ごとのイベント処理メソッド
    def in_init(self, name, msg):
        if name == 'Ready':
            self.go_lum_start()

    def in_lum_start(self, name, msg):
        if name == 'startLumSensorRes':
            self.go_lum_get()

    def in_lum_get(self, name, msg):
        if name == 'getLumSensorValueRes':
            lx = msg.get('Return')
            WsHandler.invoke_broadcast({'lx': lx})
            #self.go_lum_stop()
            self.go_sensors()

    def in_lum_stop(self, name, msg):
        if name == 'stopLumSensorRes':
            self.go_sensors()

    def in_sensors(self, name, msg):
        if name == 'getSensorValueRes':
            if 'ret' not in msg or 0 <= msg.get('ret'):
                temp = msg.get('TEM')
                rh = msg.get('HYG')
                ir = msg.get('IR')
                irlst = ir.split(',') if ir else []
                WsHandler.invoke_broadcast({'temp': temp, 'rh': rh, 'ir': irlst})
            else:
                logger.info('error ret={}'.format(msg.get('ret')))
            self.go_time_wait()

    def in_time_wait(self, name, msg):
        if name == '_timer':
            if msg.get('TimerID') == 'tm0':
                self.go_lum_start()

センサー値が取得出来た時点で、WebSocketサーバクラスWsHandlerのinvoke_broadcast()でJSON形式でデータをブロードキャストしています。また、CPU負荷を調節できるように、時間待ち状態を入れています。

グラフ表示

 グラフ表示用のJavaScriptライブラリはMITライセンスのSmoothie Charts(確認時v1.35)を利用しました。ダウンロードして他のファイルと同じディレクトリに置くか、paperograph.htmlを修正してCDNを参照して下さい。使い方は、HTMLでグラフ表示用のcanvasを定義した上で、
(1) var char = new SmoothieChart()でグラフを作成
(2) var data = new TimeSeries()でデータ列用オブジェクトを作成
(3) chart.addTimeSeries(data)でグラフとデータ列を結びつける
(4) chart.streamTo(document.getElementById(ID))でグラフとcanvasを結びつける
(5) data.append(時刻, 値)でデータを追加
です。
グラフは1.温度&湿度、2.人感、3.照度の3つ表示します。人感センサーの感知結果”Lost”, “left”, “Center”, “Right”については、人感センサーの4値のグラフ内に、縦軸の比率で”Lost”=0, “left”=1/4, “Center”=1/2, “Right”=3/4となるようにプロットしますが、このグラフはオートスケールで上下限が変動するためずれが生じます。

動作確認

 このログラムはPaPeRo i上でもラズパイでもPCでも、pythonが動作する環境であれば動かすことが出来ます。pypapero.py、smoothie.js、末尾記載のファイルを全て同じディレクトリに置いて下さい。今回PaPeRo iのアドレスはpaperograph.pyに直書きしているので必要なら修正し、以下で起動して下さい。

> python3 paperograph.py

ローカルPCで動かした場合であれば、ブラウザでlocalhost:8888/paperograph.htmlを開くとグラフが表示されます。別のホストで動かした場合にはlocalhostの部分をそのIPアドレスに置き換えて下さい。

ソース

(1) paperograph.py

# -*- coding: utf-8 -*-
import os
from logging import (getLogger, debug, info, warn, error, critical,
                     DEBUG, INFO, WARN, ERROR, CRITICAL, basicConfig)

import tornado.ioloop
import tornado.web
import tornado.websocket

from stpypapero import StPyPaperoThread as StPyPapero


logger = getLogger(__name__)


class HtmlHandler(tornado.web.RequestHandler):
    def get(self):
        self.render('paperograph.html')


class JsHandler(tornado.web.RequestHandler):
    def get(self):
        self.render('paperograph.js')


class Js2Handler(tornado.web.RequestHandler):
    def get(self):
        self.render('smoothie.js')


class WsHandler(tornado.websocket.WebSocketHandler):
    clients = []

    def initialize(self):
        """ initialize (called by tornado) """
        self.clients.append(self)

    def open(self, *args, **kwargs):
        logger.debug("WsHandler args:{} kwargs:{}".format(args, kwargs))

    def on_message(self, msg):
        logger.debug("WsHandler msg:{}".format(msg))

    def on_close(self):
        logger.debug("WsHandler")
        self.clients.remove(self)

    def send(self, msg, binary=False):
        logger.debug("WsHandler msg:{}".format(msg))
        super().write_message(msg, binary)

    @classmethod
    def broadcast(cls, msg):
        for con in cls.clients:
            con.send(msg)

    @classmethod
    def invoke_broadcast(cls, msg):
        tornado.ioloop.IOLoop.current().add_callback(cls.broadcast, msg)


def start_web():
    app = tornado.web.Application([
        (r'/paperograph.html', HtmlHandler),
        (r'/paperograph.js', JsHandler),
        (r'/smoothie.js', Js2Handler),
        (r'/__ws', WsHandler)
        ],
        template_path=os.path.dirname(__file__),
    )
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()


class SensorApp(StPyPapero):
    def initialize(self):
        self.handler = self.in_init
        # states are:
        # init, sensors, time_wait, lum_start, lum_get, lum_stop

    def trans_state(self, newhandler):
        # ログを抑制するためのオーバーライド
        self.handler = newhandler

    # 状態遷移メソッド

    def go_lum_start(self):
        self.papero.send_start_lum_sensor()
        self.trans_state(self.in_lum_start)

    def go_lum_get(self):
        self.papero.send_get_lum_sensor_value()
        self.trans_state(self.in_lum_get)

    def go_lum_stop(self):
        self.papero.send_stop_lum_sensor()
        self.trans_state(self.in_lum_stop)

    def go_sensors(self):
        self.papero.send_get_sensor_value()
        self.trans_state(self.in_sensors)

    def go_time_wait(self):
        self.set_timer(0.1, 'tm0')
        self.trans_state(self.in_time_wait)

    # 状態ごとのイベント処理メソッド

    def in_init(self, name, msg):
        if name == 'Ready':
            # self.go_sensors()
            self.go_lum_start()

    def in_lum_start(self, name, msg):
        logger.debug('event: {}'.format(name))
        if name == 'startLumSensorRes':
            self.go_lum_get()

    def in_lum_get(self, name, msg):
        logger.debug('event: {}'.format(name))
        if name == 'getLumSensorValueRes':
            lx = msg.get('Return')
            WsHandler.invoke_broadcast({'lx': lx})
            #self.go_lum_stop()
            self.go_sensors()

    def in_lum_stop(self, name, msg):
        logger.debug('event: {}'.format(name))
        if name == 'stopLumSensorRes':
            self.go_sensors()

    def in_sensors(self, name, msg):
        logger.debug('event: {}'.format(name))
        if name == 'getSensorValueRes':
            if 'ret' not in msg or 0 <= msg.get('ret'):
                temp = msg.get('TEM')
                rh = msg.get('HYG')
                ir = msg.get('IR')
                irlst = ir.split(',') if ir else []
                WsHandler.invoke_broadcast({'temp': temp, 'rh': rh, 'ir': irlst})
            else:
                logger.info('error ret={}'.format(msg.get('ret')))
            self.go_time_wait()

    def in_time_wait(self, name, msg):
        logger.debug('event: {}'.format(name))
        if name == '_timer':
            if msg.get('TimerID') == 'tm0':
                self.go_lum_start()
                # self.go_sensors()


def main():
    url = 'ws://192.168.1.1:8088/papero'
    papero = SensorApp(url=url)
    papero.start()
    start_web()


if __name__ == '__main__':
    basicConfig(format='%(asctime)-15s %(levelname)s %(module)s.%(funcName)s %(message)s')
    logger = getLogger()  # root logger
    # logger.setLevel(DEBUG)
    logger.setLevel(INFO)
    logger.info('start')
    main()

(2) paperograph.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>センサーグラフ</title>
</head>
<body>
<div id="realtime-graph">
    <canvas id="realtime-temp" width="500" height="200"></canvas>
    <canvas id="realtime-ir" width="500" height="200"></canvas>
    <canvas id="realtime-lx" width="500" height="200"></canvas>
</div>

<input type="range" id="scroll-speed-range" min="0" max="6" step="1" style="width: 80px;">
<br>
<span>temp</span>
<input type="number" id="temp" readonly>
<br>
<span>rh</span>
<input type="number" id="rh" readonly>
<br>
<span>ir-temp</span>
<input type="number" id="ir-temp" readonly>
<br>
<span>ir1</span>
<input type="number" id="ir1" readonly>
<br>
<span>ir2</span>
<input type="number" id="ir2" readonly>
<br>
<span>ir3</span>
<input type="number" id="ir3" readonly>
<br>
<span>ir4</span>
<input type="number" id="ir4" readonly>
<br>
<span>ir-result</span>
<input type="text" id="ir-result" readonly>
<br>
<span>lx</span>
<input type="number" id="lx" readonly>
<!--<script src="https://cdnjs.cloudflare.com/ajax/libs/smoothie/1.34.0/smoothie.js"></script>-->
<script src="smoothie.js"></script>
<script src="paperograph.js"></script>
</body>

(3) paperograph.js

(function() {
  var chartDataIr1 = new TimeSeries();
  var chartDataIr2 = new TimeSeries();
  var chartDataIr3 = new TimeSeries();
  var chartDataIr4 = new TimeSeries();
  var chartDataIrLst = [chartDataIr1, chartDataIr2, chartDataIr3, chartDataIr4, ];
  var chartDataIrRes = new TimeSeries();
  var chartSensors = [chartDataIr1, chartDataIr2, chartDataIr3, chartDataIr4, chartDataIrRes];
  var chartDataTemp = new TimeSeries();
  var chartDataRh = new TimeSeries();
  var chartDataLx = new TimeSeries();
  var chartIr = new SmoothieChart({interpolation:'linear'});
  var chartTemp = new SmoothieChart({interpolation:'linear'});
  var chartLx = new SmoothieChart({interpolation:'linear'});
  chartTemp.options.minValue = 0;
  chartTemp.options.maxValue = 80;
  chartLx.options.minValue = 0;
  var defaultScroll = 50;
  var startChart = function () {
    chartIr.addTimeSeries(chartDataIr1, {
      strokeStyle: 'rgba(0, 255, 0, 1)',  // 0-2-0
      lineWidth: 4 });
    chartIr.addTimeSeries(chartDataIr2, {
      strokeStyle: 'rgba(255, 128, 0, 1)',  // 2-1-0
      lineWidth: 4 });
    chartIr.addTimeSeries(chartDataIr3, {
      strokeStyle: 'rgba(0, 128, 255, 1)',  // 0-1-2
      lineWidth: 4 });
    chartIr.addTimeSeries(chartDataIr4, {
      strokeStyle: 'rgba(128, 128, 255, 1)',  // 1-1-2
      lineWidth: 4 });
    chartIr.addTimeSeries(chartDataIrRes, {
      strokeStyle: 'rgba(255, 0, 0, 1)',  // 2-0-0
      lineWidth: 2 });
    chartTemp.addTimeSeries(chartDataTemp, {
      strokeStyle: 'rgba(0, 255, 0, 1)',  // 0-2-0
      fillStyle: 'rgba(0, 255, 0, 0.2)',
      lineWidth: 3 });
    chartTemp.addTimeSeries(chartDataRh, {
      strokeStyle: 'rgba(255, 128, 0, 1)',  // 2-1-0
      fillStyle: 'rgba(255, 128, 0, 0.2)',
      lineWidth: 3 });
    chartLx.addTimeSeries(chartDataLx, {
      strokeStyle: 'rgba(0, 128, 255, 1)',  // 0-1-2
      fillStyle: 'rgba(0, 128, 255, 0.2)',
      lineWidth: 3 });
    chartTemp.streamTo(document.getElementById("realtime-temp"), defaultScroll);
    chartIr.streamTo(document.getElementById("realtime-ir"), defaultScroll);
    chartLx.streamTo(document.getElementById("realtime-lx"), defaultScroll);
    chartTemp.start();
    chartIr.start();
    chartLx.start();
  };
  document.body.onload = startChart;
  // チャートデータリストの最小値最大値を取得する
  var getChartMinMax = function(lst) {
    var minv = lst[0].minValue;
    var maxv = lst[0].maxValue;
    for (var j = 1; j < lst.length; j++) {
      var cmin = lst[j].minValue;
      var cmax = lst[j].maxValue;
      if (cmin < minv) {
        minv = cmin;
      }
      if (cmax > maxv) {
        maxv = cmax;
      }
    }
    return [minv, maxv];
  };

  var ws;
  var proto = 'https:' == location.protocol ? 'wss:' : 'ws:';
  var ws_url = proto + "//" + location.host + "/__ws";
  if ("WebSocket" in window) {
    ws = new WebSocket(ws_url);
  } else if ("MozWebSocket" in window) {
    ws = new MozWebSocket(ws_url);
  }
  function wssend(dat) {
    if (ws.readyState == 1) {
      ws.send(dat);
    } else {
      alert('接続が切れました。リロードして下さい。');
    }
  };
  var elms = {
    'temp': document.getElementById('temp'),
    'rh': document.getElementById('rh'),
    'ir-temp': document.getElementById('ir-temp'),
    'ir1': document.getElementById('ir1'),
    'ir2': document.getElementById('ir2'),
    'ir3': document.getElementById('ir3'),
    'ir4': document.getElementById('ir4'),
    'ir-result': document.getElementById('ir-result'),
    'lx': document.getElementById('lx'),
  };
  // スクロール速度選択
  var SPEED_LIST = [5, 10, 20, 50, 100, 200, 500];
  var scrollSpeedRange = document.getElementById('scroll-speed-range');
  if (scrollSpeedRange) {
    // スクロール速度指定range 変更ハンドラ
    scrollSpeedRange.addEventListener('change', function() {
      var sel = scrollSpeedRange.value;
      var val = ((0 <= sel)&&(sel < SPEED_LIST.length)) ? SPEED_LIST[sel] : defaultScroll;
      chartTemp.options.millisPerPixel = val;
      chartIr.options.millisPerPixel = val;
      chartLx.options.millisPerPixel = val;
    });
  }
  var setval = function(elm, val, chartdata, now) {
    if (elm) {
      elm.value = val;
    }
    if (chartdata) {
      chartdata.append(now, val);
    }
  };
  ws.onclose = function(event) {
    chartIr.stop();
    chartTemp.stop();
    chartLx.stop();
  };
  // メッセージを受信した際の処理
  ws.onmessage = function(event){
    console.log(event.data);
    var js = JSON.parse(event.data);
    var nowdate = new Date();
    var now = nowdate.getTime();
    if ('temp' in js) {
      setval(elms['temp'], js['temp'], chartDataTemp, now);
      setval(elms['rh'], js['rh'], chartDataRh, now);
      var ir = js['ir'];
      if (ir) {
        var irtemp = ir[0] / 64 * 0.125 + 26.75;
        setval(elms['ir-temp'], irtemp, null, null);
        setval(elms['ir1'], -ir[1], chartDataIr1, now);
        setval(elms['ir2'], -ir[2], chartDataIr2, now);
        setval(elms['ir3'], -ir[3], chartDataIr3, now);
        setval(elms['ir4'], -ir[4], chartDataIr4, now);
        setval(elms['ir-result'], ir[5], null, null);
        var irmm = getChartMinMax(chartDataIrLst);
        var irmin = irmm[0];
        var irmax = irmm[1];
        var irw = irmax - irmin;
        var resval;
        var resdic = {
          'Lost': 0,
          'Left': irw * 0.25,
          'Center': irw * 0.5,
          'Right': irw * 0.75,
        }
        if (ir[5] in resdic) {
          resval = irmin + resdic[ir[5]];
        }
        else {
          resval = irmin;
        }
        chartDataIrRes.append(now, resval);
      }
    }
    else if ('lx' in js) {
      setval(elms['lx'], js['lx'], chartDataLx, now);
    }
  };
})();

(4) stpypapero.py

# -*- coding: utf-8 -*-
# pypaperoラッパークラスstpypapero
# (c) 2017-2018 Sophia Planning Inc.
# LICENSE: MIT
#
# Ver   Date       Description
# 0.2.0 2018/05/22 状態変数を廃止、状態処理メソッドself.handlerを書き換える方法に変更
# 0.1.1 2018/02/20 speech()にmouth_en, head_en追加
# 0.1.0 2018/02/09 名称変更paperoappbase->stpypapero、on_startup削除、
#                  メッセージのリスト(現状要素数常に1)ではなく要素を渡すよう変更、
#                  paperoの引数指定追加、処理関数の返値をTRUEでプログラム終了(return書かなければ継続)に反転
#                  タイマー機能追加
# 0.0.2 2018/02/02 基本クラスAppBaseと派生クラスThreadAppBaseに分離
# 0.0.1 2017/12/19 Initial version
"""
使い方:
(1) StPyPaperoを継承したクラスを作る
(2) in_xxxで各状態ごとの処理を記述
(3) initialize()でself.handlerに初期状態用のin_xxxを設定
(4) 状態遷移時にはself.handlerを別のin_xxxに書き換える

機能:
(1) 終話検知
 speech(text)
 イベント: '_endOfSpeech'
(2) タイマー
 set_timer(delay, timer_id)
 cancel_timer(timer_id)
 イベント: 'Name: '_timer', 'TimerID': timer_id
"""
import sys
import threading
import random
from logging import (getLogger, basicConfig, DEBUG, INFO, WARN, ERROR,)

import pypapero

logger = getLogger(__name__)


# mouth data for speech
MOUTH_DAT = [
    [
        "G3" + "G3" + "G3" + "G3" + "G3" + "G3" + "G3" + "G3" + "G3", "3",
        "NNN" + "G3" + "G3" + "G3" + "NNN", "3",
        "NNNN" + "G3" + "NNNN", "3",
        "N" + "G3" + "G3" + "G3" + "G3" + "G3" + "G3" + "G3" + "N", "3",
        "NN" + "G3" + "G3" + "G3" + "G3" + "G3" + "NN", "3",
    ]
]

# head data for speech (random select)
HEAD_DAT = [
    [ # 0
        ["A4T300L",  "R0T300L",  "R0T300L",  "R0T300L", "A0T300L", ],  # x
        ["A3T300L", "A-3T300L", "A3T300L", "A-6T300L", "A0T300L", ],   # y
    ], [ # 1
        ["A-4T300L",  "R0T300L",  "R0T300L",  "R0T300L", "A0T300L", ],  # x
        ["A-3T300L", "A3T300L", "A-3T300L", "A6T300L", "A0T300L", ],    # y
    ], [ # 2
        ["A-3T300L",  "R0T300L",  "R5T300L",  "R0T300L", "A0T300L", ],  # x
        ["A-3T300L", "A3T300L", "A-2T300L", "A4T300L", "A0T300L", ],    # y
    ]
]


class StPyPapero(object):
    # papero application class
    def __init__(self, simid='', simname='', url=None, polling_sec=0.2, papero=None):
        if url is None:
            simid, simname, url = pypapero.get_params_from_commandline(sys.argv)
        self._polling_sec = polling_sec
        self._eos_wait_res = False  # send_get_speech_staus and waiting response
        self._eos_polling = False
        self._eos_callback = None  # end of speech callback
        self._timer_dic = {}
        if papero is None:
            papero = pypapero.Papero(simid, simname, url)
        self.papero = papero
        self.handler = None
        self.initialize()
        if 0 < self.papero.errOccurred:
            logger.error(self.papero.errDetail)
        else:
            self._inner_event_handler('Ready')

    """virtual
    def initialize(self):
        self.handler = self.in_init
    """

    def trans_state(self, newhandler):
        # transit state
        logger.info('state: {} -> {}'.format(self.handler, newhandler))
        self.handler = newhandler

    def speech(self, text,
               language=None, speaker_id=None,
               pitch=None, speed=None, volume=None,
               pause=None, comma_pause=None, urgent=None, priority="normal",
               eos_callback=None, mouth_en=True, head_en=True):
        # speech with head and mouth
        logger.debug('start speech')
        self._eos_polling = True
        self._eos_callback = eos_callback
        if mouth_en:
            self.papero.send_turn_led_on('mouth', MOUTH_DAT[0], repeat=True)
        if head_en:
            hidx = random.randint(0, len(HEAD_DAT) - 1)
            self.papero.send_move_head(HEAD_DAT[hidx][1], HEAD_DAT[hidx][0])
        self.papero.send_start_speech(text, language=language, speaker_id=speaker_id,
                                      pitch=pitch, speed=speed, volume=volume,
                                      pause=pause, comma_pause=comma_pause,
                                      urgent=urgent, priority=priority)

    def recv_msg(self):
        msg = self.papero.papero_robot_message_recv(self._polling_sec)
        return self._inner_event_handler(msg)

    def _inner_event_handler(self, msg):
        if self.papero.errOccurred != 0:
            return True
        # msg = self.recv_msg()
        if msg is None:
            if self._eos_polling and not self._eos_wait_res:
                # poll end of speech
                self.papero.send_get_speech_status()
                self._eos_wait_res = True
            msg = self._timeout_hook()
        if msg is None:
            return False
        else:
            res = self._handle_msg(msg)
            return res

    def run(self):
        while True:
            if self.recv_msg():
                break
        self.papero.papero_cleanup()

    def _timeout_hook(self):
        return None

    """
    def in_init(self, name, msg):
        if name == 'Ready':
            self.trans_state(self.State.IDLE)

    def in_idle(self, name, msg):
        pass
    """

    def _handle_msg(self, msg):
        if msg is None:
            return False
        if isinstance(msg, str):
            name = msg
        elif isinstance(msg, list):
            if 1 == len(msg):
                msg = msg[0]
                name = msg.get('Name')
            else:
                name = 'MsgList'
        else:
            name = msg.get('Name')
        if name == "getSpeechStatusRes":
            self._eos_wait_res = False
            ret = msg.get("Return")
            if self._eos_polling and (ret == 0):
                logger.debug('end of speech')
                self._eos_polling = False
                self.papero.send_turn_led_off('mouth')
                if callable(self._eos_callback):
                    self._eos_callback()
                # change name
                name = "_endOfSpeech"
        # call function for this state
        if callable(self.handler):
            self.handler(name, msg)

    # callbackが別スレッドで動く事に注意
    def _set_timeout(self, delay, callback, *args, **kwargs):
        hdl = threading.Timer(delay, callback, args, kwargs)
        hdl.start()
        return hdl

    def _remove_timeout(self, hdl):
        hdl.cancel()

    def _send_timeout_msg(self, timer_id):
        msg = ('{{"Name": "RobotMessage", "Messages": [' +
               '{{"Name": "_timer", "TimerID": "{}"}}] }}').format(timer_id)
        self.papero.queFromCom.put(msg)

    def set_timer(self, delay, timer_id):
        hdl = self._set_timeout(delay, self._send_timeout_msg, timer_id)
        self._timer_dic[timer_id] = hdl

    def cancel_timer(self, timer_id):
        if timer_id in self._timer_dic:
            hdl = self._timer_dic[timer_id]
            self._remove_timeout(hdl)


class StPyPaperoThread(threading.Thread, StPyPapero):
    def __init__(self, simid='', simname='', url=None, polling_sec=0.2, papero=None):
        if url is None:
            simid, simname, url = pypapero.get_params_from_commandline(sys.argv)
        # threading.Thread.__init__()
        super(StPyPaperoThread, self).__init__()
        # PyPaperoAppBase.__init__()
        super(threading.Thread, self).__init__(simid=simid, simname=simname,
                                               url=url, polling_sec=polling_sec,
                                               papero=papero)

    def run(self):
        return super(threading.Thread, self).run()

0