ESP-WROOM-02でLinuxサーバーを操作する

HTTP + RESTサーバー


今までFTPとpythonのファイル監視ライブラリを使ってESPからLinuxサーバー(RaspberryPiやOrangePi)を操作 する方法を紹介しました。
ただ、FTPプロトコルはセキュリティの関係で難しいという場合も有ります。
そこで、HTTP+RESTサーバーを使って、ESPからLinuxサーバーを操作する方法を紹介します。
RESTサーバーの構築にはFlaskを使います。

手順は以下の様になります。
RaspberryPi/OrangePi ESP8266
Flaskを使ってRESTサーバを実行する

HTTPクライアントを使ってRESTサーバーに要求を発行する
要求に応じたコマンドを実行する

Flaskをインストールする

以下の手順でFlaskをインストールします。
$ sudo apt-get install python-pip

$ sudo apt-get install python-setuptools

$ python -m pip install flask

Linux側で実行するスクリプトを準備する

サンプルとして以下の3つのスクリプトを作成します。
いずれもスクリプト自体は/sys/classデバイスを使ってLチカする簡単なものです。

  • $HOME/start.sh
  • オンボードLEDをフラッシュを開始するスクリプト
    #!/bin/bash
    sudo sh -c "echo none >/sys/class/leds/led0/trigger"
    sudo sh -c "echo heartbeat >/sys/class/leds/led0/trigger"

  • $HOME/stop.sh
  • オンボードLEDをフラッシュを停止するスクリプト
    #!/bin/bash
    sudo sh -c "echo none >/sys/class/leds/led0/trigger"
    sudo sh -c "echo 0 >/sys/class/leds/led0/brightness"

  • $HOME/gpio.sh
  • GPIOをON/OFFするスクリプト
    #!/bin/bash
    #set -x
    if [ $# -ne 2 ]; then
        exit 1
    fi

    CMD=$1
    GPIO=$2
    if [ ! -d /sys/class/gpio/gpio${GPIO} ]; then
      sudo sh -c "echo ${GPIO} > /sys/class/gpio/export"
      sudo sh -c "echo out > /sys/class/gpio/gpio${GPIO}/direction"
    fi

    if [ ${CMD^^} == "ON" ];then
      sudo sh -c "echo 1 > /sys/class/gpio/gpio${GPIO}/value"
    fi
    if [ ${CMD^^} == "OFF" ];then
      sudo sh -c "echo 0 > /sys/class/gpio/gpio${GPIO}/value"
    fi

    RESTサーバーを起動する

    以下のスクリプトを実行し、RESTサーバーを起動します。
    HTTPでパラメータを渡す方法には、URLパス変数とURLパラメータの2つが有りますが、
    パス変数はパラメータの個数が固定されるという制限が有ります。

  • /home/pi/flask/shell.py
  • # -*- coding: utf-8 -*-
    import os
    import sys
    import subprocess
    import platform
    import json
    from subprocess import PIPE
    from flask import Flask, request

    app = Flask(__name__)

    # http://192.168.10.15:8000
    @app.route("/")
    def hello():
        print("hello")
        return "Hello World!"

    # URLパス変数
    # http://192.168.10.15:8000/test1/on.sh
    # http://192.168.10.15:8000/test1/off.sh
    @app.route("/test1/<file>")
    def test1(file=None):
        print("test1 file={}".format(file))
        execPath=os.path.join(os.environ.get("HOME"), file)
        print("test1 execPath={}".format(execPath))
        if (os.path.exists(execPath)):
            if (os.access(execPath, os.X_OK)):
                print("{} is Executable".format(execPath))
                version = platform.python_version_tuple()
                print("version={} major={} minor={}".format(version, version[0], version[1]))
                # for python2
                if (version[0] == "2"):
                    res = subprocess.check_output([execPath])
                # for python3
                if (version[0] == "3"):
                    proc = subprocess.run([execPath], encoding='utf-8', stdout=subprocess.PIPE)
                    res = proc.stdout
                print('STDOUT: {}'.format(res))
                responce = {"result": "OK"}
            else:
                print("{} is Not Executable".format(execPath))
                responce = {"result": "FAIL"}
        else:
            print("{} is Not Found".format(execPath))
            responce = {"result": "FAIL"}

        return json.dumps(responce)

    # URLパラメータ
    # http://192.168.10.15:8000/test2?path=gpio.sh&cmd=on&gpio=11
    # http://192.168.10.15:8000/test2?path=gpio.sh&cmd=off&gpio=11
    @app.route("/test2")
    def test2():
        file = request.args.get('path', default=None, type=str)
        cmd = request.args.get('cmd', default=None, type=str)
        gpio = request.args.get('gpio', default=None, type=str)
        print("test2 file={}".format(file))
        execPath=os.path.join(os.environ.get("HOME"), file)
        print("test2 execPath={}".format(execPath))
        if (os.path.exists(execPath)):
            if (os.access(execPath, os.X_OK)):
                if (cmd != None and gpio != None):
                    print("{} is Executable".format(execPath))
                    print("cmd={} gpio={}".format(cmd, gpio))
                    version = platform.python_version_tuple()
                    print("version={} major={} minor={}".format(version, version[0], version[1]))
                    # for python2
                    if (version[0] == "2"):
                        res = subprocess.check_output([execPath, cmd, gpio])
                    # for python3
                    if (version[0] == "3"):
                        proc = subprocess.run([execPath, cmd, gpio], encoding='utf-8', stdout=subprocess.PIPE)
                        res = proc.stdout
                    print('STDOUT: {}'.format(res))
                    responce = {"result": "OK"}
                else:
                    print("cmd & gpio Not set")
                    responce = {"result": "FAIL"}
            else:
                print("{} is Not Executable".format(execPath))
                responce = {"result": "FAIL"}
        else:
            print("{} is Not Found".format(execPath))
            responce = {"result": "FAIL"}

        return json.dumps(responce)

    if __name__ == "__main__":
        #app.run()
        print("app.url_map={}".format(app.url_map))
        app.run(host='0.0.0.0', port=8000, debug=True)

    curlを使ってテストする

    別のターミナルから

    curl "http://localhost:8000/test1/start.sh"
    を実行すると、オンボードの黄色のLEDが点滅します。

    curl "http://localhost:8000/test1/stop.sh"
    を実行すると、オンボードの黄色の点滅が止まります。

    curl "http://localhost:8000/test2?path=gpio.sh&cmd=on&gpio=11"
    を実行すると、GPIO11がONします。

    curl "http://localhost:8000/test2?path=gpio.sh&cmd=off&gpio=11"
    を実行すると、GPIO11がOFFします。


    HTTPを使って要求を発行する

    ESP側のHTTPクライアントはこ ちらを参考に作りました。
    アクセスポイントのSSID、パスワード、RESTサーバーのIPアドレスは、自分の環境に合わせて変更してください。
    #include <ESP8266WiFi.h>
    #include <ESP8266HTTPClient.h>

    const char* ssid = "AccessPointName";
    const char* password  = "hogehoge";
    const String server  = "ServerIp";
    const uint16_t port  = 8000;

    void postRequest(String url) {
        HTTPClient http;
        http.begin(server, port, url);
        int httpCode = http.GET();
        Serial.print("httpCode=");
        Serial.println(httpCode);
        String result = "";
        if (httpCode < 0) {
          result = http.errorToString(httpCode);
        } else if (http.getSize() < 0) {
          result =  "size is invalid";
        } else {
          result = http.getString();
        }
        http.end();
        Serial.print("result=");
        Serial.println(result);
    }

    void setup() {
      Serial.begin(115200);
      delay(10);
      Serial.println("booted!");

      WiFi.begin(ssid, password);
      while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
      }
      WiFi.mode(WIFI_STA);
      Serial.println("WiFi connected");
      Serial.println("IP address: ");
      Serial.println(WiFi.localIP());
    }

    void loop() {
      byte inChar;
      if (Serial.available() > 0) {
        inChar = Serial.read();

        if (inChar == '0') {
          //http.begin("192.168.10.15", 8000, "/test1/start.sh");
          postRequest("/test1/start.sh");
        }

        if (inChar == '1') {
          //http.begin("192.168.10.15", 8000, "/test1/stop.sh");
          postRequest("/test1/stop.sh");
        }

        if (inChar == '2') {
          //http.begin("192.168.10.15", 8000, "/test2?path=gpio.sh&cmd=on&gpio=11");
          postRequest("/test2?path=gpio.sh&cmd=on&gpio=11");
        }

        if (inChar == '3') {
          //http.begin("192.168.10.15", 8000, "/test2?path=gpio.sh&cmd=off&gpio=11");
          postRequest("/test2?path=gpio.sh&cmd=off&gpio=11");
        }
      }

      delay(10);  // give time to the WiFi handling in the background

    }

    Arduino-IDEのシリアルコンソールで[0]を入力するとRaspberryのオンボードLEDがフラッシュし、
    [1]を入力するとLEDのフラッシュが止まります。

    Arduino-IDEのシリアルコンソールで[2]を入力するとRaspberryのGPIO11がONになり、
    [3]を入力するとGPIO11がOFFになります。



    Flaskとよく似たWebFrameworkにTornadoが有ります。
    TornadoでもRESTサーバーを構築することができます。

    Tornadoをインストールする

    以下の手順でTornadoをインストールします。
    TornadoのV5.0以上にはHTTPClientが動かない不具合が有ります。
    こちらで色々 議論されていますが、結局V4.5.3をインストールするしか解決策はない様です。
    pipでインストールできるTornadoのバージョン一覧は、こちらで 見ることができます。
    condaでインストールすると、必ず6.0.3がインストールされてしまうので、注意が必要です。
    $ sudo apt install python-pip python-setuptools

    $ python -m pip install -U wheel

    $ python -m pip install tornado==4.5.3

    Linux側で実行するスクリプトを準備する

    Flaskと同じスクリプトを使用します。

    RESTサーバーを起動する

    以下のスクリプトを実行し、RESTサーバーを起動します。
    HTTPでパラメータを渡す方法には、URLパス変数とURLパラメータの2つが有りますが、
    パス変数はパラメータの個数が固定されるという制限はFlaskと同じです。

  • /home/pi/tornado/shell.py
  • # -*- coding: utf-8 -*-
    import os
    import sys
    import subprocess
    import platform
    import json
    import tornado.httpserver
    import tornado.ioloop
    import tornado.options
    import tornado.web
    #from tornado.web import url

    from tornado.options import define, options
    define("port", default=8000, help="run on the given port", type=int)

    # http://192.168.10.15:8000
    class IndexHandler(tornado.web.RequestHandler):
        def get(self):
            print("IndexHandler:get")
            self.write("Hello World")

    # URLパス変数
    # http://192.168.10.15:8000/test1/hoge
    class Test1Handler(tornado.web.RequestHandler):
        def get(self, file):
            print("file={} len={}".format(file, len(file)))
            execPath=os.path.join(os.environ.get("HOME"), file)
            print("test1 execPath={}".format(execPath))
            if (os.path.exists(execPath)):
                if (os.access(execPath, os.X_OK)):
                    print("{} is Executable".format(execPath))
                    version = platform.python_version_tuple()
                    print("version={} major={} minor={}".format(version, version[0], version[1]))
                    # for python2
                    if (version[0] == "2"):
                        res = subprocess.check_output([execPath])
                    # for python3
                    if (version[0] == "3"):
                        proc = subprocess.run([execPath], encoding='utf-8', stdout=subprocess.PIPE)
                        res = proc.stdout
                    print('STDOUT: {}'.format(res))
                    responce = {"result": "OK"}
                else:
                    print("{} is Not Executable".format(execPath))
                    responce = {"result": "FAIL"}
            else:
                print("{} is Not Found".format(execPath))
                responce = {"result": "FAIL"}

            self.write(json.dumps(responce))

    # URLパラメータ
    # http://192.168.10.15:8000/test2?path=hoge
    class Test2Handler(tornado.web.RequestHandler):
        def get(self):
            file = self.get_argument('path', None)
            cmd = self.get_argument('cmd', None)
            gpio = self.get_argument('gpio', None)
            print("test2 file={}".format(file))
            execPath=os.path.join(os.environ.get("HOME"), file)
            print("test2 execPath={}".format(execPath))
            if (os.path.exists(execPath)):
                if (os.access(execPath, os.X_OK)):
                    if (cmd != None and gpio != None):
                        print("{} is Executable".format(execPath))
                        print("cmd={} gpio={}".format(cmd, gpio))
                        version = platform.python_version_tuple()
                        print("version={} major={} minor={}".format(version, version[0], version[1]))
                        # for python2
                        if (version[0] == "2"):
                            res = subprocess.check_output([execPath, cmd, gpio])
                        # for python3
                        if (version[0] == "3"):
                            proc = subprocess.run([execPath, cmd, gpio], encoding='utf-8', stdout=subprocess.PIPE)
                            res = proc.stdout
                        print('STDOUT: {}'.format(res))
                        responce = {"result": "OK"}
                    else:
                        print("cmd & gpio Not set")
                        responce = {"result": "FAIL"}
                else:
                    print("{} is Not Executable".format(execPath))
                    responce = {"result": "FAIL"}
            else:
                print("{} is Not Found".format(execPath))
                responce = {"result": "FAIL"}

            self.write(json.dumps(responce))

    '''
    def make_app():
        return tornado.web.Application([
            (r"/", IndexHandler),
            (r"/test1/(.*)", Test1Handler),
            (r"/test2", Test2Handler),
        ],debug=True,)
    '''

    if __name__ == "__main__":
        tornado.options.parse_command_line()
        app = tornado.web.Application(
            handlers=[
                (r"/", IndexHandler),
                (r"/test1/(.*)", Test1Handler),
                (r"/test2", Test2Handler),
            ],debug=True
        )
        http_server = tornado.httpserver.HTTPServer(app)
        http_server.listen(options.port)
        tornado.ioloop.IOLoop.current().sta

    HTTPを使って要求を発行する

    ESP側のHTTPクライアントはFlaskの時と全く同じものを使います。
    RESTサーバーを使う場合、クライアント側はサーバー側の実装に依存しないのが特徴です。

    Arduino-IDEのシリアルコンソールで[0]を入力するとRaspberryのオンボードLEDがフラッシュし、
    [1]を入力するとLEDのフラッシュが止まります。

    Arduino-IDEのシリアルコンソールで[2]を入力するとRaspberryのGPIO11がONになり、
    [3]を入力するとGPIO11がOFFになります。