豆腐とコンソメ

豆腐とコンソメ

もろもろのプログラム勉強記録

RaspBerryPiでラジコンをつくる(11):socket.ioをFlaskで実装する

日記

ラズパイにはまってから、IoTというキーワードによく反応するようになりました。
なかでも、トイレの使用状況をお知らせする、なんてのはよく聞く話かと思います。
先日、サービスエリアに寄った際も、トイレの混雑状況をお知らせする大きな看板が目につきました。
「空」だったり「混」という文字が出ているのを見て、お!っと思った次第です。
この仕組み自体は昔からあったものかもしれませんが、嬉しかった私は、奥さんに「見て!すごい目まぐるしく表示がかわってる!」とお伝えし、「満!混!満!混!」とはしゃいでいたら、奥さんにいろいろな意味で止めてくれる?、と窘められました。

さて、話はかわりますが、ここにきて、というか最初からかもしれませんが、ブログの目的が曖昧だったりします。
今はモチベーションを維持するためにも、こうやってやったことを発信できれば、と思っているのですが、成果物が雑だったり、成果物に対してブログを書くことに時間がかなり大きくって、どうなんだろうみたいなものもあって悩みどころです。

メモを取るだけだったらEvernoteにも書けばいいとも思いますし、きちんとした意見がほしいのであればQuitaに書いたほうがいいのかな、とか思ったりします。
モチベーションを保つためにもアクセス数はほしいし、切磋琢磨的なところで意見もほしいし、自分のメモとしても役に立ちたいし、みたいなところがいろいろあって、何が一番大事なのかを見失っています。

が、とりあえずは悩んで何もしないよりかは、まず動いている状態が大事だと思いますので、引き続きよろしくお願い致します。

前回の続き

前回はnode.jsを使ってsocket.ioを実装し、ブラウザからのキー入力をサーバーに送信するところまでやりました。
今回は、socket.ioのサーバー側の部分をpythonのWebアプリケーションフレームワークであるFlaskを使って、実装していきたいと思います。
理由は前回もありましたが、ラズパイのGPIOをコントロールする部分をpythonで実装しているためです。

tohutokonsome.hatenablog.com

Flaskとは

PythonのWebアプリケーションフレームワーク
以前扱った、PythonのWebアプリケーションフレームワークの「Django」よりさらに軽量(必要最低限のフレームワーク)な気がする。

実際の作業は公式?のチュートリアルを見て、試してみました。

1. Webアプリケーションを作る準備 — study flask 1 ドキュメント

ここに、socket.ioを実装していきます。

Flaskのsocket.ioについては、以下の記事を参考にさせていただきました。
Flask-SocketIOでWebSocketアプリケーション - Qiita

Welcome to Flask-SocketIO’s documentation! — Flask-SocketIO documentation


初期設定

ラズパイに、必要なpythonモジュールをインストールしていきます。

pythonのバージョンは以下を使用しています。

pi@raspberrypi:~ $ pyenv version
3.5.1 (set by /home/pi/.pyenv/version)

Flaskのインストー

pipで取得します。

pi@raspberrypi:~/myproduct $ sudo pip install Flask

地味にsudo権限だと環境変数変わるっていうのを忘れていて、混乱した。
sudoersを編集する必要があった。

WiringPiでエラーになった場合のメモ - 豆腐とコンソメ

インストールされてますね。

pi@raspberrypi:~/myproduct $ sudo pip freeze
click==6.7
Flask==0.12.2
itsdangerous==0.24
Jinja2==2.9.6
MarkupSafe==1.0
Werkzeug==0.12.2
wiringpi==2.32.1
wiringpi2==2.32.3

Flask-SocketIOのインストー

こちらもFlaskと同様にpipでもってくる。

pi@raspberrypi:~/myproduct $ sudo pip install flask-socketio

こちらも特に問題なくインストールされています。

pi@raspberrypi:~/myproduct $ sudo pip freeze
click==6.7
Flask==0.12.2
Flask-SocketIO==2.9.1
itsdangerous==0.24
Jinja2==2.9.6
MarkupSafe==1.0
python-engineio==1.7.0
python-socketio==1.7.6
six==1.10.0
Werkzeug==0.12.2
wiringpi==2.32.1
wiringpi2==2.32.3

唯一のモジュールの「app.py」の作成

Flaskを使ったチュートリアルでは、ブログを作成していたりしました。
しかし、今回必要なのは、以前作成したレゴカーのコントロール部分の橋渡しだけです。

f:id:konoemario:20170720223155j:plain:w500

なので、Flaskの基本的な機能については深く考えずに、進めます。

ということで、唯一作成するapp.pyの中身はほぼ以下の記事のままになります。

Welcome to Flask-SocketIO’s documentation! — Flask-SocketIO documentation

app.py

# -*- coding: utf-8 -*-
from flask import Flask
from flask_socketio import SocketIO,emit

app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret!'
socketio = SocketIO(app)

@socketio.on('connect',namespace='/legocar')
def init() :
    print('connected')

@socketio.on('sendMessage',namespace='/legocar')
def recieve_order(order) :

#ここにラズパイ操作用のモジュールを組み込む想定
    if order in {'forward','back'}:
       print("accell = " +order)
       testmethod(order)
    elif order in {'break'}:
        print("accell stop = " +order)
        testmethod(order)
    elif order in {'right','left'}:
       print("handle = " +order)
       testmethod(order)
    else:
        pass


@socketio.on('test',namespace='/legocar')
def testmethod(order):
    emit('test',order)


if __name__ == '__main__':
    socketio.run(app,host='0.0.0.0',port=5000)

検証がいろいろと足りていないのですが、自分のメモがてらポイントを記載しておきます。

  • Flaskは以下のように条件を、@構文で定義して、それに該当する処理である場合、直下の関数が呼ばれるみたい
#URLディスパッチャ、ホスト名のみでアクセスした場合の処理
@app.route('/')
def index():
    return 'Hello world!'
  • node.jsのsocket.onみたいなイベントも同じように@socketio.on(‘イベント名’,namespace)、直下に関数でいける

  • node.jsの場合、namespaceは使っていなかったけれども、どうもFlask-SocketIOでは必須?(未検証)

  • socketio.run(app)ではなくsocketio.run(app,host=‘0.0.0.0’,port=5000)とすること。 socketio.run(app)の場合、ローカルホスト内でしかFlaskにアクセスできない

そうしましたら、上記、app.pyをラズパイ上で実行します。

Flaskの起動

pi@raspberrypi:~/myproduct/legocar/legocar/server $ sudo python app.py 
WebSocket transport not available. Install eventlet or gevent and gevent-websocket for improved performance.
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)

なにやらいわれていますが、無事起動しました。

これが終わったら、前回作成した「controller.html」を修正します。


controll.htmlの修正

前回、サーバー側のsocket.ioはnode.jsで実装していました。
これをさきほど作成したapp.pyに向きを変えます。

f:id:konoemario:20170720224254j:plain:w500

といっても、io.connectに引数として接続先を渡してあげるだけです。

controller.html(修正前)

    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.0/jquery.min.js"></script>
    <script src="/socket.io/socket.io.js"></script>
    <script>
        var socket = io.connect();

io.connectは引数なしの場合、デフォルトHTTPリクエストで要求したサーバー(ここではnode.jsのWebサーバー)に接続します。
引数を渡してあげることで、指定したサーバーに接続することができるみたいです。

また、接続先のアドレスですが、「ラズパイのIPアドレス:5000/legocar」としています。
ホスト名以降の/legocarってなんぞって感じなのですが、app.pyのnamespaceで指定したものになります。
namespaceそのものをあんまり理解してないので、ここではふーん、ぐらいで終えています。

controller.html(修正後)

    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.0/jquery.min.js"></script>
    <script src="/socket.io/socket.io.js"></script>
    <script>
        var socket = io.connect('http://ラズパイのIPアドレス:5000/legocar');

疎通確認

それでは、ラズパイにログインして、前回作成したnode.jsのプロジェクトとpythonのapp.pyを起動してみます。

node.jsのWebサーバー起動

pi@raspberrypi:~/myproduct/legoCarController $ node server.js

Flaskの起動(さっきと同じ)

pi@raspberrypi:~/myproduct/legocar/legocar/server $ sudo python app.py 
WebSocket transport not available. Install eventlet or gevent and gevent-websocket for improved performance.
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)

これで、「http://ラズパイのIP:6677/controller」にアクセスすると、controller.htmlが表示され、ブラウザ上では、前回と同様の内容が表示されるます。
また、Flaskを起動しているコンソール画面には、ブラウザからのキー入力に応じて、app.pyのprintしている内容が表示されることが確認できます。

192.168.1.5 - - [19/Jul/2017 15:58:07] "POST /socket.io/?EIO=3&transport=polling&t=LrPexJY&sid=9364d0a57e82400da39bf9470d70d056 HTTP/1.1" 200 -
192.168.1.5 - - [19/Jul/2017 15:58:07] "GET /socket.io/?EIO=3&transport=polling&t=LrPexJc&sid=9364d0a57e82400da39bf9470d70d056 HTTP/1.1" 200 -
accell = forward
192.168.1.5 - - [19/Jul/2017 15:58:07] "POST /socket.io/?EIO=3&transport=polling&t=LrPexSK&sid=9364d0a57e82400da39bf9470d70d056 HTTP/1.1" 200 -
192.168.1.5 - - [19/Jul/2017 15:58:07] "GET /socket.io/?EIO=3&transport=polling&t=LrPexMK&sid=9364d0a57e82400da39bf9470d70d056 HTTP/1.1" 200 -
accell stop = break

なんだか、POST、GETがsocketと同じタイミングで吐き出されているのが気になりますが。一旦棚にあげています。

ラズパイコントロール用の処理を追加する

後は、以前以下の記事のときに作成したラズパイコントロール用の処理である「legocar_controller.py 」をapp.pyから呼ぶだけです。

tohutokonsome.hatenablog.com

ひどく汚いコードですが、とりあえず自分の環境では稼働が確認できました。

 SocketIO,emit
import legocar_controller  as LegoCar

app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret!'
socketio = SocketIO(app)


myLegoCar = LegoCar.LegoCarController()


@socketio.on('connect',namespace='/legocar')
def init() :
    print('connected')

@socketio.on('sendMessage',namespace='/legocar')
def recieve_order(order) :
#TODO ひどいコードなので直したい
    if order in {'forward'}:
       print("accell forward " +order)
       #レゴカーのアクセル(前)
       myLegoCar.accelerator(b"w")
       testmethod(order)
    elif order in {'back'}:
        print("accell back " +order)
        #レゴカーのアクセル(後)
        myLegoCar.accelerator(b"s")
        testmethod(order)
    elif order in {'break'}:
        print("accell stop " +order)
        myLegoCar.accelerator(b"f")
        testmethod(order)
    elif order in {'right'}:
       print("handle = " +order)
       #レゴカーのハンドル(右)
       myLegoCar.handle(b"d")
       testmethod(order)
    elif order in {'left'}:
        print("handle = " +order)
        #レゴカーのハンドル(右と左)
        myLegoCar.handle(b"a")
        testmethod(order)
    else:
        pass

@socketio.on('test',namespace='/legocar')
def testmethod(order):
    emit('test',order)

if __name__ == '__main__':
    socketio.run(app,host='0.0.0.0',port=5000)

次回は、操作するUIだったり、カメラ部分を改善していきたいと思います。