Help us understand the problem. What is going on with this article?

ラズパイ + Bottleでお手軽にIoT環境を作る

More than 1 year has passed since last update.

この記事について

ラズパイを使った製作例でよくある、スマートフォンなどのブラウザからLEDやボタンなどのデバイスを制御するやつを作ります。Node.jsを使用した例はよくあるのですが、個人的にはPython + bottle.pyでやるのが簡単だと思いますので、その方法をまとめます。

作るもの

  • Raspberry Pi上で動くWebサーバー
    • GPIOなどの周辺デバイスへアクセスするWeb APIを提供する
  • クライアント側に表示されるビューとWeb APIアクセス用処理
    • htmlとJavaScript

環境

  • Raspberry Pi Zero W (Pi2でも3でも大丈夫です)
    • この記事ではIPアドレスは192.168.1.88として記載しています
    • RPi.GPIOのインストールをしておいてください。別のライブラリを使用している方は適宜置き換えて記事をお読みください。
  • デバイス
    • LED x 2 (PORT5とPORT6に接続。できればトランジスタアレイ経由)
    • ボタン x 2 (PORT20とPORT21に接続。反対側はGNDに接続)

Bottleとは?

Python用の軽量なWebアプリケーションフレームワークです。WebサーバーをPythonを使用して簡単に作成することが出来ます。

全体像

image.png

全体の動作の流れは以下のようになります。
1. Raspberry Pi上でWebサーバーを立ち上げる
- index.py + bottle.py
2. ユーザがルートパスにアクセスすると、サーバはhtmlビュー(index.tpl)を返す。
- 例えば、http://192.168.1.88:8080
- このhtmlビューにはLED制御のためのボタンなどが配置されている
3. ユーザがボタンなどをクリックすると、JavaScriptコード(caller.js)がLED制御用のWebAPIを呼ぶ
- 例えば、http://192.168.1.88:8080/setLed
4. WebサーバはこのLED制御用のWebAPIを受け取ると、GPIOを制御してLEDをOn/Offする (この部分は通常のPythonと同じ)

作り方

Webサーバーを作成する

適当なディレクトリで作業を行います。ここでは、MyServerとします。そこにbottle.pyを配置します。また、メインコードとなるindex.pyをそこで実装します。下記コマンドでは例としてnanoでindex.pyを編集しようとしていますが、WindowsなどのホストPCで作成してコピーする方が楽だと思います。

bash: 準備コマンド
mkdir MyServer
cd MyServer
wget https://bottlepy.org/bottle.py
nano index.py &

index.pyは下記のようになります。コードの概要は、

  • main関数
    • GPIOポートの初期化
    • サーバを8080ポートとして立ち上げる
  • atExit関数
    • 終了時に必ず呼ばれる関数。GPIOの解放処理を行う。(Ctrl-Cなどで終了した場合に備えて。なくてもいい)
  • setLedEntry関数
    • LEDをOn/OffするWebAPIのエントリ関数
    • @routeが関数の前についているのが、公開されるAPI用の処理になります。APIのURLと種別(GET/POST)を指定します。
    • setLEDはPOSTとして、引数はJSONで受けています。
  • getButtonEntry関数
    • ボタンの状態を返すWebAPIのエントリ関数
    • 状態をJSONに詰めて返します
  • root関数
    • ユーザがルートURLにアクセスしたときに表示するページを指定します。ここでは、index.tplというファイルを返すようにしています。tplというのはテンプレートと呼ばれるファイルですが、本プロジェクトではただのhtmlファイルとして扱います。
  • static関数
    • cssやJavaScriptコードの場所を指定します
index.py
#!/bin/env python
# coding: utf-8
import json
from bottle import route, run, request, HTTPResponse, template, static_file
import RPi.GPIO
import atexit

GPIO_PORT_LED_0 = 5
GPIO_PORT_LED_1 = 6
GPIO_PORT_BTN_0 = 20
GPIO_PORT_BTN_1 = 21

@route('/static/:path#.+#', name='static')
def static(path):
    return static_file(path, root='static')

@route('/')
def root():
    return template("index")

# curl -H "Accept: application/json" -H "Content-type: application/json" -X POST -d '{"num":"0", "onoff":true}' http://192.168.1.88:8080/setLed
@route('/setLed', method='POST')
def setLedEntry():
    var = request.json
    # print (var)
    if (var["num"] == "0" ):
        RPi.GPIO.output(GPIO_PORT_LED_0, var["onoff"])
    elif (var["num"] == "1" ):
        RPi.GPIO.output(GPIO_PORT_LED_1, var["onoff"])
    retBody = {"ret": "ok"}
    r = HTTPResponse(status=200, body=retBody)
    r.set_header('Content-Type', 'application/json')
    return r

# curl -H "Accept: application/json" -H "Content-type: application/json" -X POST -d '{"dummy":"0"}' http://192.168.1.88:8080/getButton
@route('/getButton', method='POST')
def getButtonEntry():
    retBody = {
        "ret": "ok",
        "btn_0": RPi.GPIO.input(GPIO_PORT_BTN_0),
        "btn_1": RPi.GPIO.input(GPIO_PORT_BTN_1)
    }
    r = HTTPResponse(status=200, body=retBody)
    r.set_header('Content-Type', 'application/json')
    return r

def main():
    print("Initialize port")
    RPi.GPIO.setmode(RPi.GPIO.BCM)
    RPi.GPIO.setup(GPIO_PORT_LED_0, RPi.GPIO.OUT) 
    RPi.GPIO.setup(GPIO_PORT_LED_1, RPi.GPIO.OUT) 
    RPi.GPIO.setup(GPIO_PORT_BTN_0, RPi.GPIO.IN, pull_up_down = RPi.GPIO.PUD_UP) 
    RPi.GPIO.setup(GPIO_PORT_BTN_1, RPi.GPIO.IN, pull_up_down = RPi.GPIO.PUD_UP) 
    RPi.GPIO.output(GPIO_PORT_LED_0, 0)
    RPi.GPIO.output(GPIO_PORT_LED_1, 0)

    print('Server Start')
    run(host='0.0.0.0', port=8080, debug=True, reloader=True)
    # run(host='0.0.0.0', port=8080, debug=False, reloader=False)

def atExit():
    print("atExit")
    RPi.GPIO.cleanup()

if __name__ == '__main__':
    atexit.register(atExit)
    main()

Webサーバを立ち上げて、WebAPIを叩いてみる

Raspberry Pi上で下記コマンドでサーバを立ち上げます。

python index.py

この状態でWebAPIを叩いてみます。JSON引数のPOSTを発行できればなんでもいいのですが、CURLコマンドを使うと下記のようになります。発行元はホストPCでもラズパイ上の別のターミナルからでもOKです。これらのコマンドを打ってLEDが点灯したりボタンの状態が取得できればOKです

curl -H "Accept: application/json" -H "Content-type: application/json" -X POST -d '{"num":"0", "onoff":true}' http://192.168.1.88:8080/setLed
curl -H "Accept: application/json" -H "Content-type: application/json" -X POST -d '{"dummy":"0"}' http://192.168.1.88:8080/getButton

クライアントビューを用意する。

この状態で、ブラウザ(同じネットワーク内にあれば、ラズパイからでもPCからでもいいです。)からhttp://192.168.1.88:8080/にアクセスしてみてください。indexファイルが見つからないというエラーが返ってくるはずです。これは、index.pyのrootの指定によって、index.tplファイルを返すことになっているのですが、まだこのファイルを用意していないためです。これから作ります。

ブラウザに表示されるビューのためにはhtmlファイルが必要です。これは、bottleではテンプレートと呼ばれ、拡張子は.tplになります。実際にはhtmlと同じに扱えます。以下のようなディレクトリ構造を用意してください。

MyServer/
 - index.py, bottle.py
 - views/
    - index.tpl (新規作成ファイル)
 - static/js/
    - caller.js (新規作成ファイル)

index.tplの中身は以下のようになります。LED用とボタン用のチェックボックスがそれぞれ2つあるだけのシンプルなページです。LED用のチェックボックスには、ユーザがボタンをクリックしたタイミングでsetLed関数を呼ぶようにonclickイベントに登録しています。

index.tpl
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="Pragma" content="no-cache">
        <meta http-equiv="Cache-Control" content="no-cache">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <title>Controller</title>
        <!-- load css files -->
        <!-- <link rel="stylesheet" href="static/css/bootstrap.min.css"> -->
    </head>
    <body>      
        <main>
        <h1>Controller</h1>
        <div>
            <div>
                <h2>LED</h2>
                LED0: <input type="checkbox" id="checkbox-led-0" name="checkbox-led-0" data-on-color="primary" onclick="setLed('0', this.checked)"><br>
                LED1: <input type="checkbox" id="checkbox-led-1" name="checkbox-led-1" data-on-color="primary" onclick="setLed('1', this.checked)">
                <hr>
            </div>
            <div>
                <h2>Button</h2>
                Button0: <input type="checkbox" id="checkbox-button-0" name="checkbox-button-0" data-on-color="primary"><br>
                Button1: <input type="checkbox" id="checkbox-button-1" name="checkbox-button-1" data-on-color="primary">
                <hr>
            </div>
        </div>
        </main>
        <!-- load script files -->
        <script type="text/javascript" src="static/js/caller.js" charset="utf-8"></script>
    </body>
</html>

この状態で再度、http://192.168.1.88:8080にブラウザからアクセスしてみてください。以下のようなページが表示されるはずです。
A_controller.jpg

WebサーバとやりとりするJavaScriptコードを作成する

この状態でボタンをクリックしても何も起きません。クライアント側での動作を記したJavaScriptコードが必要になります。これをcaller.jsに実装します。コードは下記になります。コードの概要は、

  • setLed関数
    • http://192.168.1.88:8080/setLed APIを呼び出す
  • getButton関数
    • setIntervalによって定期的に呼び出される
    • http://192.168.1.88:8080/getButton APIを呼び出し、その結果でボタン用チェックボックスの状態を更新する
  • callApi関数
    • WEB APIを呼び出すためのサブルーチン。いわゆるAjaxとかいう技術です
caller.js
var SERVER_URL = "http://192.168.1.88:8080/"

setInterval(getButton, 200)

function setLed(num, onoff) {
    callApi(
        SERVER_URL + "setLed",
        {
            "num": num,
            "onoff": onoff
        },
        function (o) {
        });
}

function getButton() {
    callApi(
        SERVER_URL + "getButton",
        {"dummy":"dummy"},
        function (o) {
            console.log(o.responseText);
            var retJson = eval('new Object(' + o.responseText + ')');
            document.getElementById("checkbox-button-0").checked = retJson.btn_0 == "1";
            document.getElementById("checkbox-button-1").checked = retJson.btn_1 == "1";
        });
}

function callApi(url, jsonObj, callback) {
    var xhr = new XMLHttpRequest();
    xhr.open('POST', url);
    xhr.setRequestHeader('Content-Type', 'application/json');
    xhr.setRequestHeader('Accept', 'application/json');

    xhr.onreadystatechange = (function(myxhr) {
        return function() {
            if (xhr.readyState == 4 && xhr.status == 200) {
                callback(myxhr);
            }
        }
    })(xhr);

    xhr.send(JSON.stringify(jsonObj));
}

このcaller.jsをRaspberry Pi上にコピーして、再度http://192.168.1.88:8080にアクセスしてください。LEDの制御やボタン状態の取得ができるはずです。もしもうまく動かない場合は、ブラウザ上でキャッシュのクリアをしてみてください。

この記事で紹介したソースコード

https://github.com/take-iwiw/RaspberryPiDeviceServer/tree/master/A_Basic

おまけ

もう少し複雑なプロジェクト

これまで紹介したプロジェクトは、サーバ側は全てPythonで記載されています。また、制御するのはGPIOのみです。やはりデバイスドライバはCで書きたかったので、Cで作成したデバイスドライバ(so形式のライブラリ)をctypes経由で呼び出すバージョンを作成しました。また、制御対象のデバイスとしてPWM制御によるモーターとスピーカー。I2C制御による加速度センサとOLEDディスプレイを追加しました。

全体図はこちらになります。
image.png
ソースコードはこちらになります。
https://github.com/take-iwiw/RaspberryPiDeviceServer/tree/master/B_Advanced

別の技術を使用した例

今回使用した技術をまとめると、次のようになります。

  • Webアプリケーションフレームワーク: bottle
  • クライアント側フレームワーク: なし
  • Python-C連携: ctypes (おまけプロジェクトで使用)

それぞれ別の技術を使用することもできます。例えば、以下のような技術を使用することもできます

  • Webアプリケーションフレームワーク: Flask
  • クライアント側フレームワーク: AngularJS
  • Python-C連携: Boost

Flaskの方がbottleよりも少し高機能です。また、AngularJSを使用すると、htmlとJavaScriptの親和性を高めることが出来ます。ctypesだとC++には未対応ですが、Boostだとクラスを使用することもできます。これらを使用した場合の全体図がこちらになります。

image.png

ソースコードはこちらになります。
https://github.com/take-iwiw/InputDeviceServer

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした