youtubeでESP8266(NodeMCU V1.0)のチュートリアル

最近は引っ越して、何かと忙しく電子工作は中断状態だ。

一昨日、新しく買ったTVでyoutubeを観てると、NodeMCU V1.0のWebServerの構築のチュートリアルビデオがリストに上がってきたので観てみた。これによると結構簡単にWebServerが構築できるようなのだ。
そういえば、昨年アマゾンの中華ショップで色々と電子部品を購入したのだが放置状態、このビデオで使われているNodeMCU V1.0もあったはず、ふたつ買ったNodeMCU V1.0(ESP8266)もひとつだけ封を開けオンボードLEDをチカチカさせただけだった。
とう言うわけでチュートリアルビデオとネットを頼りで学習してみることにした。
You need to a flashplayer enabled browser to view this YouTube video

ESP8266 for Arduinoの備忘録

最近は物覚えが悪くなっているので、少し時間が経つと学習したことをすっかり忘れてまう可能性があるのでメモを残す。

ESP8266 for Arduinoの基本

ArduinoでESP8266をWebServerに仕立てるには、以下のようにプログラムを書けばよいらしい、結構シンプルな記述ができるようだ。
WiFiオブジェクトでWiFiルータに接続し、severオブジェクトでWebサーバの振る舞いを書くということみたいだ。

#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>

ESP8266WebServer server(80);

const char* ssid = "xxxxxx";
const char* password = "xxxxxx";

void setup()
{
    Serial.begin(115200);
    WiFi.begin(ssid, password);

    while (WiFi.status() != WL_CONNECTED)
    {
        Serial.print(".");
        delay(500);
    }
    Serial.println("");
    Serial.print("Local IP: ");
    Serial.println(WiFi.localIP());

    server.on("/", []() {
        server.send(200, "text/plain", "Hello World!");
    });
    server.begin();
    Serial.println("HTTP server started");
}

void loop()
{
    server.handleClient();
}

サーバオブジェクトのURLルーティング

サーバオブジェクトのURLルーティングはserver.on関数とserver.onNotFound関数が引き受け、応答レスポンスを記述するようだ。

書き方は以下のとおりserver.on関数にURLルートと応答関数のポインタを渡す。

  ・
  ・
void onRoot()
{
    server.send(200, "text/plain", "Hello World!");
}
  ・
  ・  
void setup()
{
  ・
  ・
    server.on("/", onRoot);
  ・
  ・
}

または、以下のようにラムダ式で書いてもOKなようだ。

void setup()
{
  ・
  ・
    server.on("/", []() {
        server.send(200, "text/plain", "Hello World!");
    });
  ・
  ・
}

ちなみにWebでは存在しないファイルにアクセスしたときのURLルーティングはserver.onNotFound関数が引き受け、以下のように書くようだ。

  ・
  ・
void handleNotFound()
{
    server.send(404, "text/plain", "File Not Found");
}
  ・
  ・
void setup()
{
  ・
  ・
    server.onNotFound(handleNotFound);
  ・
  ・
}

LEDサーバで実験

サーバといえば情報などを資源を提供するのが役目、NodeMCU V1.0(ESP8266)はセンサなどのハードウェアを自在に扱えるので、ハードウェアの資源を提供するサーバに仕立てることにする。

ここではNodeMCU V1.0(ESP8266)のオンボードLEDというデバイスを提供するサーバに仕立ててみた。
以下の機能を実装した。

  • LEDの点灯・消灯の制御
  • “LEDの点灯・消灯の回数”という情報の提供

コードは以下の通り。

#include <ArduinoJson.h>
#include <ESP8266WebServer.h>
#include <ESP8266WiFi.h>

ESP8266WebServer server(80);

const char* ssid = "XXXXXXXXX";
const char* password = "XXXXXXXX";

// HTML
const String html_header =
    "<!doctype html>"
    "<html><head><meta charset=\"UTF-8\"/>"
    "<meta name=\"viewport\" content=\"width=device-width\"/>"
    "<title>ESP8266 テスト</title>"
    "</head><body>";

const String html_footer = "</body></html>";

int led_on_req_count = 0;
int led_off_req_count = 0;

void handleNotFound()
{
    digitalWrite(LED_BUILTIN, LOW); // ボード上のLED点灯
    String message = "File Not Found\n\n";
    message += "URI: ";
    message += server.uri();
    message += "\nMethod: ";
    message += (server.method() == HTTP_GET) ? "GET" : "POST";
    message += "\nArguments: ";
    message += server.args();
    message += "\n";
    for (uint8_t i = 0; i < server.args(); i++)
    {
        message += " " + server.argName(i) + ": " + server.arg(i) + "\n";
    }
    server.send(404, "text/plain", message);
    digitalWrite(LED_BUILTIN, HIGH); // ボード上のLED消灯
}

void setup()
{
    pinMode(LED_BUILTIN, OUTPUT); // Initialize the LED_BUILTIN pin as an output

    Serial.begin(115200);
    WiFi.begin(ssid, password);

    while (WiFi.status() != WL_CONNECTED)
    {
        Serial.print(".");
        delay(500);
    }
    Serial.println("");
    Serial.print("Local IP: ");
    Serial.println(WiFi.localIP());

    server.on("/", []() {
        String html =
            html_header +
            "<h1>NodeMCU!</h1><br>ようこそESP8266 for Arduinoのプログラムへ" +
            html_footer;
        server.send(200, "text/html", html);

    });

    server.on("/led/on", []() {
        String html =
            html_header + "<h1>NodeMCU!</h1><br>LED点灯" + html_footer;
        digitalWrite(LED_BUILTIN, LOW);
        led_on_req_count++;
        server.send(200, "text/html", html);
    });

    server.on("/led/off", []() {
        String html =
            html_header + "<h1>NodeMCU!</h1><br>LED消灯" + html_footer;
        digitalWrite(LED_BUILTIN, HIGH);
        led_off_req_count++;
        server.send(200, "text/html", html);
    });

    server.on("/ledToggle", []() {
        String html;

        if (digitalRead(LED_BUILTIN) == HIGH)
        {
            html = html_header + "<h1>NodeMCU!</h1><br>LED点灯" + html_footer;
            digitalWrite(LED_BUILTIN, LOW);
            led_on_req_count++;
        }
        else
        {
            html = html_header + "<h1>NodeMCU!</h1><br>LED消灯" + html_footer;
            digitalWrite(LED_BUILTIN, HIGH);
            led_off_req_count++;
        }
        server.send(200, "text/html", html);

    });

    server.on("/led", []() {
        String html;
        String led_condition = server.arg("light");

        if (led_condition == "on")
        {
            html = html_header + "<h1>NodeMCU!</h1><br>LED点灯" + html_footer;
            digitalWrite(LED_BUILTIN, LOW);
            led_on_req_count++;
        }
        else if (led_condition == "off")
        {
            html = html_header + "<h1>NodeMCU!</h1><br>LED消灯" + html_footer;
            digitalWrite(LED_BUILTIN, HIGH);
            led_off_req_count++;
        }
        else
        {
            html = html_header +
                   "<h1>NodeMCU!</h1><br>データの渡し方が間違ってます!!" +
                   html_footer;
            digitalWrite(LED_BUILTIN, HIGH);
        }
        server.send(200, "text/html", html);
    });

    server.on("/jBlink", []() {
        String data = server.arg("plain");
        StaticJsonBuffer<200> jBuffer;
        JsonObject& jObject = jBuffer.parseObject(data);
        String n = jObject["times"];
        String t = jObject["delay"];
        for (int i = 0; i < n.toInt(); i++)
        {
            digitalWrite(LED_BUILTIN, LOW);
            delay(t.toInt());
            led_on_req_count++;

            digitalWrite(LED_BUILTIN, HIGH);
            delay(t.toInt());
            led_off_req_count++;

            server.send(204, "");
        }
    });

    server.on("/led.json", []() {
        String json_output;
        StaticJsonBuffer<200> jBuffer;
        JsonObject& root = jBuffer.createObject();
        // データ登録
        root["name"] = "ESP8266WebServer";
        root["on_req_count"] = led_on_req_count;
        root["off_req_count"] = led_off_req_count;
        root.printTo(json_output);
        server.send(200, "application/json", json_output);
    });

    server.onNotFound(handleNotFound);

    server.begin();
    Serial.println("HTTP server started");
}

void loop()
{
    server.handleClient();
}

LEDの点灯・消灯の制御

LEDの点灯・消灯の制御は、いくつかの方法が考えられたので冗長的であるが実装した。

URLルーティングでLED制御

一番簡単なのが、URLルーティングでLED制御だ。

“http://xxx.xxx.xxx.xxx/led/on”にブラウザなどでアクセスしたときLEDは点灯させるには、以下のように書く。

  ・
  ・
    server.on("/led/on", []() {
        String html =
            html_header + "<h1>NodeMCU!</h1><br>LED点灯" + html_footer;
        digitalWrite(LED_BUILTIN, LOW);
        led_on_req_count++;
        server.send(200, "text/html", html);
    });
  ・
  ・

“http://xxx.xxx.xxx.xxx/led/off”にブラウザなどでアクセスしたときLEDは消灯させるには、以下のように書く。

  ・
  ・
    server.on("/led/off", []() {
        String html =
            html_header + "<h1>NodeMCU!</h1><br>LED消灯" + html_footer;
        digitalWrite(LED_BUILTIN, HIGH);
        led_off_req_count++;
        server.send(200, "text/html", html);
    });
  ・
  ・

“http://xxx.xxx.xxx.xxx/ledToggle”にブラウザなどでアクセスしたときLEDは点灯/消灯させるには、以下のように書く。

  ・
  ・
    server.on("/ledToggle", []() {
        String html;

        if (digitalRead(LED_BUILTIN) == HIGH)
        {
            html = html_header + "<h1>NodeMCU!</h1><br>LED点灯" + html_footer;
            digitalWrite(LED_BUILTIN, LOW);
            led_on_req_count++;
        }
        else
        {
            html = html_header + "<h1>NodeMCU!</h1><br>LED消灯" + html_footer;
            digitalWrite(LED_BUILTIN, HIGH);
            led_off_req_count++;
        }
        server.send(200, "text/html", html);
    });
  ・
  ・

GETメソッドによるLED制御

“http://xxx.xxx.xxx.xxx/led?light=on”にブラウザなどでアクセスしたときLEDを点灯。または”http://xxx.xxx.xxx.xxx/led?light=off”のときLEDを消灯させるには以下のように書く。

  ・
  ・
    server.on("/led", []() {
        String html;
        String led_condition = server.arg("light");

        if (led_condition == "on")
        {
            html = html_header + "<h1>NodeMCU!</h1><br>LED点灯" + html_footer;
            digitalWrite(LED_BUILTIN, LOW);
            led_on_req_count++;
        }
        else if (led_condition == "off")
        {
            html = html_header + "<h1>NodeMCU!</h1><br>LED消灯" + html_footer;
            digitalWrite(LED_BUILTIN, HIGH);
            led_off_req_count++;
        }
        else
        {
            html = html_header +
                   "<h1>NodeMCU!</h1><br>データの渡し方が間違ってます!!" +
                   html_footer;
            digitalWrite(LED_BUILTIN, HIGH);
        }
        server.send(200, "text/html", html);
    });
  ・
  ・

WebAPIでLED制御

LEDの制御をするWebAPIを実装してみる。データの受け渡しはXMLとかjsonとかが一般的らしいのだが、ここはjavascriptで簡単に扱えるjsonを扱う。

実装したのは{“times”:5,”delay”:250}というjsonデータを送れば、(250ms x 2)間隔で5回点滅するというWebAPIである。

  ・
  ・
    server.on("/jBlink", []() {
        String data = server.arg("plain");
        StaticJsonBuffer<200> jBuffer;
        JsonObject& jObject = jBuffer.parseObject(data);
        String n = jObject["times"];
        String t = jObject["delay"];
        for (int i = 0; i < n.toInt(); i++)
        {
            digitalWrite(LED_BUILTIN, LOW);
            delay(t.toInt());
            led_on_req_count++;

            digitalWrite(LED_BUILTIN, HIGH);
            delay(t.toInt());
            led_off_req_count++;

            server.send(204, "");
        }
    });
  ・
  ・

そしてRubyで書いたjsonを”http://xxx.xxx.xxx.xxx/jBlink”に送るクライアントプログラム

#!/usr/bin/env ruby

require 'uri'
require 'net/http'
require 'json'

uri = URI('http://xxx.xxx.xxx.xxx/jBlink')
req = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json')
req.body = {:times => 10, :delay => 250}.to_json
http = Net::HTTP.new(uri.hostname, uri.port)
http.set_debug_output $stderr  #

http.start do |h|
  response = h.request(req)
end

そしてUbuntu上で実行したときの応答。
デバッグモードにしてるのでjsonデータが無事送信されたことがわかる。

bant@ORCA:~/workspace/Arduino/webserver_ruby_client/client$ ./blink_req.rb 
opening connection to 192.168.1.108:80...
opened
<- "POST /jBlink HTTP/1.1\r\nContent-Type: application/json\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: 192.168.1.108\r\nContent-Length: 24\r\n\r\n"
<- "{\"times\":10,\"delay\":250}"
-> "HTTP/1.1 204 No Content\r\n"
-> "Content-Type: \r\n"
-> "Content-Length: 0\r\n"
-> "Connection: close\r\n"
-> "Access-Control-Allow-Origin: *\r\n"
-> "\r\n"
Conn close

“LEDの点灯・消灯の回数”という情報の提供

ブラウザ・WebAPIで情報提供

以下の実装で”http://xxx.xxx.xxx.xxx/led.json”というWeb上仮想ファイルにアクセスされたらjsonデータを送信する。

  ・
  ・
    server.on("/led.json", []() {
        String json_output;
        StaticJsonBuffer<200> jBuffer;
        JsonObject& root = jBuffer.createObject();
        // データ登録
        root["name"] = "ESP8266WebServer";
        root["on_req_count"] = led_on_req_count;
        root["off_req_count"] = led_off_req_count;
        root.printTo(json_output);
        server.send(200, "application/json", json_output);
    });
  ・
  ・

rubyで書いたクライアントプログラム

#!/usr/bin/env ruby

require 'net/http'
require 'uri'
require 'json'

uri = URI.parse('http://xxx.xxx.xxx.xxx/led.json')

http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = false
http.set_debug_output $stderr

res = http.start do |h|
  h.get(uri.request_uri)
end

if res.code == '200'
  result = JSON.parse(res.body)
  puts result
else
  puts "ERROR!! #{res.code} #{res.message}"
end

そしてUbuntu上で実行したときの応答。
デバッグモードにしているが、こちらは表示が少々うざい(^_^;;

bant@ORCA:~/workspace/Arduino/webserver_ruby_client/client$ ./json_req.rb 
opening connection to 192.168.1.108:80...
opened
<- "GET /led.json HTTP/1.1\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: 192.168.1.108\r\n\r\n"
-> "HTTP/1.1 200 OK\r\n"
-> "Content-Type: application/json\r\n"
-> "Content-Length: 64\r\n"
-> "Connection: close\r\n"
-> "Access-Control-Allow-Origin: *\r\n"
-> "\r\n"
reading 64 bytes...
-> ""
-> "{\"name\":\"ESP8266WebServer\",\"on_req_count\":30,\"off_req_count\":30}"
read 64 bytes
Conn close
{"name"=>"ESP8266WebServer", "on_req_count"=>30, "off_req_count"=>30}

30回ずつLEDの点灯・消灯をしたことがわかる。
Webブラウザ上のベタな表示なら利用するのは大変だが、jsonやXMLのような構造的なデータならばアプリで活用しやすい。

とりあえずWebServerはこれで終わり

クライアントとかUDPなどでの実験は残っているが、WebServerに関しては、これぐらいツボを抑えておけば、なんとかなりそうだ。