🐝

ESP32をWebサーバにする

2023/06/27に公開

はじめに

この記事ではESP32をWebサーバにして、PCやスマホからアクセスできるようにする方法を解説します。

In English

This article describes how to run web server on ESP32 development board in Japanese.

基礎知識

ソケットについて

IPネットワーク(インターネットなど)で通信を行うためにソケットとよばれる仕組みが利用されています。

ソケット(socket)は通信をする相手方のと自分をIPアドレスポート番号の組で識別します。

ポートは同じネットワークインターフェイスを使用する複数のアプリケーションを区別するために用いられる16ビットの整数値で表され、65,535が最大値です。

よく使われるポート番号は予約されていて、ウェルノウン・ポート(well-known ports)とよばれIANA(Internet Assigned Numbers Authority)で管理されています。
例えば、80番はHTTP、443番はHTTPS、53番はDNSとなっています。

IPアドレスとポート番号以外に、通信を行うプロトコル(TCPUDP)の指定も必要となります。IPアドレスやポート番号が同一でもプロトコルが異なると通信できません。

Pythonでは (192.168.10.20, 80) のようにタプルとしてアドレスを扱います

ソケットを使用して相手方に要求を送信する側をクライアントといい、送られてきた要求を処理する側をサーバとよびます。
ネットワークアプリケーションはほとんどがクライアント/サーバモデルとなっています。

altSocket

HTTPについて

HTTPに関する参照URL
https://developer.mozilla.org/ja/docs/Web/HTTP

WebサーバとクライアントのブラウザはHTTP(Hyper Text Transfer Protocol)とよばれるプロトコルを使用してやりとりされます。

サンプルプログラム1

ブラウザからESP32開発ボードのIPアドレスにアクセスがあると、変数htmlに定義されている簡単なメッセージを送信してソケットを閉じるWebサーバプログラムのサンプル。

プログラムの説明

14行目:htmlという変数にクライアントから接続があった場合に送出するHTTPヘッダーとHTMLで書かれたボディーを定義しています。
29~34行目:WiFi接続を調べ、接続されていればifconfig()の戻り値を表示し、接続されていなければメッセージを表示してプログラムを終了する。
38行目:TCPソケットインスタンス作成。
41行目:クライアントソケットを閉じた後にすぐにソケットを再利用できるようにオプションを設定。
44行目:すでに設定されているIPアドレスの80番ポートを使用するようにaddr変数に代入する。
47行目:ソケットインスタンスsにIPアドレスとポート番号を割り当てる(バインドする)。
50行目:ソケットをリッスンする(接続要求が来るまでずっとポートを見張っている)。アクセプトできるのは一つのコネクションのみとする。
55行目:クライアントからの接続要求を受け付ける。戻り値はクライアントとやりとりするための新しいソケットオブジェクトおよびクライアントのIPアドレス、ポート番号で構成されるタプル。
59行目~63行目:クライアントから送られてきたヘッダ情報などを表示。
65行目:html変数にセットしたデータを送信。
66行目:55行目で開いたソケットをクローズ。
72行目:CTRL-Cが押されたら、最初に作成したソケットsをクローズします。

althttpserver1.py

コピペ用 httpserver1.py
httpserverl.py
# ESP32 Web server sample
# Before run this code, you should connect WiFi
# Jun 1st 2023

import network
import socket
import sys
import time

# Default port number for WWW
PORT = 80

# Make HTTP header and HTML contents
html = """
HTTP/1.0 200 OK
Server: ESP32 HTTP Server
Content-type: text/html; charset=utf-8

<!DOCTYPE html>
<html>
   <head><title>ESP32 web server</title></head>
   <body>
       <h1>Welcome to ESP32 web server</h1>
   </body>
</html>
"""

# Check WiFi connection and print IP address if connected
wlan = network.WLAN(network.STA_IF)
if wlan.isconnected():
   print(wlan.ifconfig())
else:
   print("Connect WiFi first.")
   sys.exit(-1)

try:
   # Make socket as 's'
   s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

   # Would like to re-use the same socket address even it is already in use.
   s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

   # 0.0.0.0 means any IP addresses given to ESP32 WiFi interface
   addr = ('0.0.0.0', PORT)

   # Assign IP address and Port
   s.bind(addr)

   # Only 1 conncection is acceptable
   s.listen(1)

   print("Waiting for connection...")

   while True:
       cl, addr = s.accept()
       print("Connected from", addr)

       # Print HTTP request from client
       buff = cl.recv(512)
       if len(buff) > 0:
           print("----- Client sent followings ------")
           print(buff.decode("utf-8"))
           print("----------\n")

       cl.sendall(html)
       cl.close()
       print("Connection closed and waiting for next connection")


# Close socket if CTRL-C is press
except KeyboardInterrupt:
   s.close()

実行例

シェルに表示される内容

('192.168.1.64', '255.255.255.0', '192.168.1.254', '192.2168.1.254')
Waiting for connection...
Connected from ('192.168.1.66', 59664)
----- Client sent followings ------
GET / HTTP/1.1
Host: 192.168.1.64
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/113.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: ja,en-US;q=0.7,en;q=0.3
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Sec-GPC: 1


----------

Connection closed and waiting for next connection
Connected from ('192.168.1.66', 59665)
----- Client sent followings ------
GET /favicon.ico HTTP/1.1
Host: 192.168.1.64
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/113.0
Accept: image/avif,image/webp,*/*
Accept-Language: ja,en-US;q=0.7,en;q=0.3
Accept-Encoding: gzip, deflate
Connection: keep-alive
Referer: http://192.168.1.64/
Sec-GPC: 1


----------

Connection closed and waiting for next connection

サンプルプログラム2

formタグでONとOFFのボタンを表示するだけのサンプルプログラムです。
どのボタンが押されたかの判定はサンプルプログラム3で行います。

プログラムの説明

27行目〜34行目:formタグを使ってONとOFFのボタンを作っています。

althttpserver2.py

コピペ用 httpserver2.py
httpserver2.py
# ESP32 Web server sample (Display buttons)
# Before run this code, you should connect WiFi
# Jun 1st 2023 iot101@zenn.dev

import network
import socket
import sys
import time

# Default port number for WWW
PORT = 80

# Make HTTP header and HTML contents
html = """
HTTP/1.0 200 OK
Server: ESP32 HTTP Server
Content-type: text/html; charset=utf-8


<!DOCTYPE html>
<html>
    <head>
        <title>ESP32 web server</title>
    </head>
    <body>
        <h1>Welcome to ESP32 web server</h1>
        <form method="GET" acthon="#">
            <div>
                <input type="submit" name="switch" value="ON">
            </div>
            <div>
                <input type="submit" name="switch" value="OFF">
            </div>
        </form>
    </body>
</html>

"""



# Check WiFi connection and print IP address if connected
wlan = network.WLAN(network.STA_IF)
if wlan.isconnected():
    print(wlan.ifconfig())
else:
    print("Connect WiFi first.")
    sys.exit(-1)

try:
    # Make socket as 's'
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    # Would like to re-use the same socket address even it is already in use.
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    # 0.0.0.0 means any IP addresses given to ESP32 WiFi interface
    addr = ('0.0.0.0', PORT)

    # Assign IP address and Port
    s.bind(addr)

    # Only 1 conncection is acceptable
    s.listen(1)

    print("Waiting for connection...")

    while True:
        cl, addr = s.accept()
        print("Connected from", addr)

        # Print HTTP request from client
        buff = cl.recv(512)
        
        if len(buff) > 0:
            print("----- Client sent followings ------")
            print(buff.decode("utf-8"))
            print("----------\n")

        cl.sendall(html)
        cl.close()
        print("Connection closed and waiting for next connection")


# Close socket if CTRL-C is press
except KeyboardInterrupt:
    s.close()




サンプルプログラム3

どのボタンが押されたかを判定してThonnyのシェル部に表示するプログラムです。

プログラムの説明

GETメソッドで送っているので、URLにボタンの情報が含まれます。
例えば、次のようになります。
http://192.168.1.10/?switch=ON

84行目、89行目:buff変数をfind()で検索しています。find()は見つかったインデックス(何番目の文字列かの情報)を返します。buff変数にはクライアントが送ってきた GET /?switch=ON HTTP/1.1 のようなデータが入っているので、例えば、buff.find("/?switch=ON")は4を返します。(Gが0、Eが1.../が4)

althttpserver3.py

コピペ用 httpserver3.py
httpserver3.py
# ESP32 Web server sample (Display buttons)
# Before run this code, you should connect WiFi
# Jun 1st 2023 iot101@zenn.dev

import network
import socket
import sys
import time

# Default port number for WWW
PORT = 80

# Make HTTP header and HTML contents
html = """
HTTP/1.0 200 OK
Server: ESP32 HTTP Server
Content-type: text/html; charset=utf-8


<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>ESP32 web server</title>
    </head>
    <body>
        <h1>Welcome to ESP32 web server</h1>
        <form method="GET" acthon="#">
            <div>
                <input type="submit" name="switch" value="ON">
            </div>
            <div>
                <input type="submit" name="switch" value="OFF">
            </div>
        </form>
    </body>
</html>

"""



# Check WiFi connection and print IP address if connected
wlan = network.WLAN(network.STA_IF)
if wlan.isconnected():
    print(wlan.ifconfig())
else:
    print("Connect WiFi first.")
    sys.exit(-1)

try:
    # Make socket as 's'
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    # Would like to re-use the same socket address even it is already in use.
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    # 0.0.0.0 means any IP addresses given to ESP32 WiFi interface
    addr = ('0.0.0.0', PORT)

    # Assign IP address and Port
    s.bind(addr)

    # Only 1 conncection is acceptable
    s.listen(1)

    print("Waiting for connection...")

    while True:
        cl, addr = s.accept()
        print("Connected from", addr)

        # Print HTTP request from client
        buff = cl.recv(512)
        # Convert bytes to strings
        buff = buff.decode("utf-8")
       
        if len(buff) > 0:
            print("----- Client sent followings ------")
            print(buff)
            print("----------\n")
            
        # Check which button was pressed
        if buff.find("/?switch=ON") == 4:
            #print(buff.find("/?switch=ON"))
            print("ONが押されました。")


        if buff.find("/?switch=OFF") == 4:
            #print(buff.find("/?switch=OFF"))
            print("OFFが押されました。")
        cl.sendall(html)
        cl.close()
        print("Connection closed and waiting for next connection")


# Close socket if CTRL-C is press
except KeyboardInterrupt:
    s.close()

サンプルプログラム4

どのボタンが押されているかをブラウザにも表示されるプログラムです。

プログラムの説明

57行目、61行目:押されたボタンの情報をstatusと言う変数に代入しています。
65行目:ファイルの最初の方で定義していたhtml変数の代入を移動しました。
88行目:html変数を途中で分割し、statusを割り込ませています。

althttpserver4.py

コピペ用 httpserver4.py
httpserver4.py
# ESP32 Web server sample (Display buttons)
# Before run this code, you should connect WiFi
# Jun 1st 2023 iot101@zenn.dev

import network
import socket
import sys
import time

# Default port number for WWW
PORT = 80

status = ""

# Check WiFi connection and print IP address if connected
wlan = network.WLAN(network.STA_IF)
if wlan.isconnected():
    print(wlan.ifconfig())
else:
    print("Connect WiFi first.")
    sys.exit(-1)

try:
    # Make socket as 's'
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    # Would like to re-use the same socket address even it is already in use.
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    # 0.0.0.0 means any IP addresses given to ESP32 WiFi interface
    addr = ('0.0.0.0', PORT)

    # Assign IP address and Port
    s.bind(addr)

    # Only 1 conncection is acceptable
    s.listen(1)

    print("Waiting for connection...")

    while True:
        cl, addr = s.accept()
        print("Connected from", addr)

        # Print HTTP request from client
        buff = cl.recv(512)
        # Convert bytes to strings
        buff = buff.decode("utf-8")
       
        if len(buff) > 0:
            print("----- Client sent followings ------")
            print(buff)
            print("----------\n")
            
        # Check which button was pressed
        if buff.find("/?switch=ON") == 4:
            status = "ON"
            print("ONが押されました。")

        if buff.find("/?switch=OFF") == 4:
            status = "OFF"
            print("OFFが押されました。")
            
        # Make HTTP header and HTML contents
        html = """
HTTP/1.0 200 OK
Server: ESP32 HTTP Server
Content-type: text/html; charset=utf-8


<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>ESP32 web server</title>
    </head>
    <body>
        <h1>Welcome to ESP32 web server</h1>
        <form method="GET" acthon="#">
            <div>
                <input type="submit" name="switch" value="ON">
            </div>
            <div>
                <input type="submit" name="switch" value="OFF">
            </div>
        </form>
        <div>
""" + status + """
    が押されています。
    </div>
    </body>
</html>

"""
        cl.sendall(html)
        cl.close()
        print("Connection closed and waiting for next connection")


# Close socket if CTRL-C is press
except KeyboardInterrupt:
    s.close()

サンプルプログラム5

どのボタンが押されているかをブラウザにも表示されるプログラムです。
サンプルプログラム4は醜いので書き換えてみました。

プログラムの説明

35行目:あとでどのボタンが押されたかの文字列に置き換えるために、%%MESSAGE%% を追加しました。
87行目、91行目:replace() でhtml変数にある %%MESSAGE%% をどのボタンが押されたかの文字列に置き換えて html_new 変数に代入するようにし、96行目で html_new を送信するようにしました。

althttpserver5.py

コピペ用 httpserver5.py
httpserver4.py
# ESP32 Web server sample (Display which button is pressed)
# Before run this code, you should connect WiFi
# Jun 1st 2023 iot101@zenn.dev

import network
import socket
import sys

# Default port number for WWW
PORT = 80

# Make HTTP header and HTML contents
html = """
HTTP/1.0 200 OK
Server: ESP32 HTTP Server
Content-type: text/html; charset=utf-8


<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>ESP32 web server</title>
    </head>
    <body>
        <h1>Welcome to ESP32 web server</h1>
        <form method="GET" acthon="#">
            <div>
                <input type="submit" name="switch" value="ON">
            </div>
            <div>
                <input type="submit" name="switch" value="OFF">
            </div>
            <div>
            %%MESSAGE%%
            </div>
        </form>
    </body>
</html>

"""


# Check WiFi connection and print IP address if connected
wlan = network.WLAN(network.STA_IF)
if wlan.isconnected():
    print(wlan.ifconfig())
else:
    print("Connect WiFi first.")
    sys.exit(-1)

try:
    # Make socket as 's'
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    # Would like to re-use the same socket address even it is already in use.
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    # 0.0.0.0 means any IP addresses given to ESP32 WiFi interface
    addr = ('0.0.0.0', PORT)

    # Assign IP address and Port
    s.bind(addr)

    # Only 1 conncection is acceptable
    s.listen(1)

    print("Waiting for connection...")

    while True:
        cl, addr = s.accept()
        print("Connected from", addr)

        # Print HTTP request from client
        buff = cl.recv(512)
        # Convert bytes to strings
        buff = buff.decode("utf-8")

        if len(buff) > 0:
            print("----- Client sent followings ------")
            print(buff)
            print("----------\n")

        # Check which button was pressed
        if buff.find("/?switch=ON") == 4:
            print("ONが押されました。")
            html_new = html.replace("%%MESSAGE%%", "ON が押されました。")

        elif buff.find("/?switch=OFF") == 4:
            print("OFFが押されました。")
            html_new = html.replace("%%MESSAGE%%", "OFF が押されました。")

        else:
            html_new = html.replace("%%MESSAGE%%", "")

        cl.sendall(html_new)
        cl.close()
        print("Connection closed and waiting for next connection")


# Close socket if CTRL-C is press
except KeyboardInterrupt:
    s.close()

演習問題

  1. ブラウザでアクセスすると超音波センサで測定した距離をブラウザ上に表示するプログラムを作成して下さい。
    (ファイル名:httpserver-ex1.py)
    超音波センサで距離を測定する方法についてはこちらの記事を参照して下さい。

  2. ブラウザ上に「ド」、「ミ」、「ソ」のボタンを表示し、それぞれのボタンを押すとESP32開発ボード上の圧電スピーカから対応する音がなるようなプログラムを作成して下さい。
    (ファイル名:httpserver-ex2.py)
    圧電スピーカで音を出す方法についてはこちらの記事を参照して下さい。

Discussion