Microdot:Webサーバーフレームワーク

ESP32にはWiFi機能が標準で搭載されており、周辺機器が何も接続されていない裸の開発ボードでも、ネットと接続して色々と楽しむことができます。

ネットを使用するプログラムの一つの要としてWEBサーバーがあります。

PCやサーバー上では、WEBサーバはそれ専用のソフトウェア:apacheやnginxなどを使い、プログラムそのものを書くことは稀ですが、ESP32でWEBサーバーを構築する方法の一つは、ソケットを使いポートを開いてリスンして...という、古典的な方法が基本になります。この方法だと、WEBサーバーの規模が大きくなったり、機能が向上してくると、あっという間にプログラムがわけのわからない状況に陥ってしまいます。

この様な問題を避けるため、ESP32上でも、PCやサーバー上でのWEB構築の様に少なくともそれなりのWEBサーバーソフトウェアが使え、アプリケーション機能を構築する場合には、何らかのフレームワークが使えることが望まれます。ありがたいことに、ESP32のような小さなマイクロコントローラでもフレームワークを使ってWEBサーバーを開発することができるようになってきました。ここでは、MicroPython用のWEBサーバーフレームワークのMicrodotの使用法を紹介します。

Microdotoのドキュメントやソースは以下のリンクをご参照ください。


Microdotの利用準備

Microdotを利用するためには、通常はMicrodotのライブラリを開発ボードにインストールする必要がありますが、マイクロファンのESP32用のMicroPythonファームウェアにはあらかじめ組み込まれているので、インストール作業なしにすぐに使用できます。

オリジナルのMicroPythonファームウェアを使用する場合には、Thonnyのパッケージ管理システムでmicrodotを検索して開発ボードにインストールすることができます。


静的ページの表示

表示するページは、MicroPythonのプログラム内に書き込む方法と、ファイルに保存されたページを表示する方法の2種類を利用できます。

単純なページ表示:初めてのMicrodot

ブラウザからのリクエストに対して、プログラム内に書かれたHTMLドキュメントを返すもっとも単純なWEBサーバーを以下に示します。

import network, time
from microdot import Microdot, Response

ssid = 'YOUR_SSID'
passwd = 'YOUR_PASSWD'

def wifi_connect(ssid, passwd):
    wifi = network.WLAN(network.STA_IF)
    wifi.active(True)
    if not wifi.isconnected():
        print('connecting to network...')
        wifi.connect(ssid, passwd)
        while not wifi.isconnected():
            print('.',end='')
            time.sleep_ms(500)
        print()
    return wifi

# アクセスポイントへの接続
wifi = wifi_connect(ssid, passwd)
print('network config:', wifi.ifconfig())

# ブラウザに送るHTMLテキスト
htmldoc = '''<!DOCTYPE html>
<html>
    <head>
        <title>Microdot Hello</title>
        <meta charset="UTF-8">
    </head>
    <body>
        <h1>初めてめてのWEBサーバー</h1>
        <p>こんにちは、みなさん!</p>
    </body>
</html>
'''

# WEBサーバーの設定
app = Microdot()
Response.default_content_type = 'text/html'

@app.route('/') # ルート(/: root)に対するレスポンス
def index(request):
    return htmldoc # HIMLドキュメントをブラウザに返す

# WEBサーバーの実行
app.run(port=80, debug=True)

4行目から21行目までは、指定したWiFiのアクセスポイントに接続するための処理です。これ以降のプログラムでも、この部分は共通です。

21行目のコードで表示されるタプルの第1要素で、WEBサーバーに割り当てられたIPアドレスが表示されるので、WEBサーバーのアクセスにはそのIPアドレスを使用してください。

このプログラムでは、ブラウザに送るHTMLドキュメントは、ファイルに保存されたものではなく、23行からヒアドキュメント形式で記述したものを使用しています。

WEBサーバーの実質的なコードは、37行から46行の実質6行のみです!シンプルですね。

ブラウザからのリクエストに対しては、41行に、ルートに対するレスポンスのみを記述しています。したがって、ブラウザに入力するWEBサーバーアドレスは、[IPアドレス]あるいは、[IPアドレス/]と入力してください。

このレスポンスでは、単純に23行目のHTMLドキュメントをブラウザに返しています。

これでめでたくWEBページが表示されます。

複数のページ表示

1ページしか表示できないと、いろいろと不便ですよね。次に、トップページと2つの追加ページからなるWEBサイトのプログラム例を使用します。ページ間は、リンクのクリックで移動できます。

import network, time
from microdot import Microdot, Response

ssid = 'YOUR_SSID'
passwd = 'YOUR_PASSWD'

def wifi_connect(ssid, passwd):
    wifi = network.WLAN(network.STA_IF)
    wifi.active(True)
    if not wifi.isconnected():
        print('connecting to network...')
        wifi.connect(ssid, passwd)
        while not wifi.isconnected():
            print('.',end='')
            time.sleep_ms(500)
        print()
    return wifi

# アクセスポイントへの接続
wifi = wifi_connect(ssid, passwd)
print('network config:', wifi.ifconfig())

# ブラウザに送るHTMLテキスト
homedoc = '''<!DOCTYPE html>
<html>
    <head>
        <title>Microdot Multi: Top</title>
        <meta charset="UTF-8">
    </head>
    <body>
        <h1>トップページ</H1>
        <h1>ページへのリンク</h1>
        <UL>
        <LI><a href=/page1>ページ1</a>
        <LI><a href=/page2>ページ2</a>
        </UL>
    </body>
</html>
'''

page1doc = '''<!DOCTYPE html>
<html>
    <head>
        <title>Microdot Multi: Page1</title>
        <meta charset="UTF-8">
    </head>
    <body>
        <h1>ページ1</H1>
        <h1>ページへのリンク</h1>
        <UL>
        <LI><a href=/>トップページ</a>
        <LI><a href=/page2>ページ2</a>
        </UL>
    </body>
</html>
'''

page2doc = '''<!DOCTYPE html>
<html>
    <head>
        <title>Microdot Multi: Page2</title>
        <meta charset="UTF-8">
    </head>
    <body>
        <h1>ページ2</H1>
        <h1>ページへのリンク</h1>
        <UL>
        <LI><a href=/>トップページ</a>
        <LI><a href=/page1>ページ1</a>
        </UL>
    </body>
</html>
'''

# WEBサーバーの設定
app = Microdot()
Response.default_content_type = 'text/html'

@app.route('/') # ルート(/: root)に対するレスポンス
def index(request):
    return homedoc # HIMLドキュメントをブラウザに返す

@app.get('/page1') # /page1に対するレスポンス
def page1(request):
    return page1doc

@app.get('/page2') # /page2に対するレスポンス
def page2(request):
    return page2doc

# WEBサーバーの実行
app.run(port=80, debug=True)

追加されたページに対応したレスポンスを83行から89行に追加しています。ブラウザでリンクをクリックしてサーバーに送られるリクエストはGET形式なので、get()を使ってレスポンスを記述しています。ルート(/)に対するレスポンスと同様に、route()を使用して記述してもかまいません。

コード自体は単純で短いのですが、HTMLドキュメントも増え、プログラムが長くなってきましたね。

ファイルで保存したページの表示

ページ数が増えてきたり、ページの記述が複雑になってくると、プログラム内にそれを記述するのは、煩雑になってきますね。また、プログラムとドキュメントが分離されていないのは、開発やメンテナンスの障害になります。

そこで、プログラムとドキュメントを分離し、一般的なWEBサーバーの様に、ファイルで保存されたドキュメントをブラウザに返す形式にプログラムを変更しましょう。

ファイルに保存されたHTMLドキュメントを使用することもあり、例えばpage1をpage1.htmlの様に、拡張子付きのファイル名に変更しています。

import network, time
from microdot import Microdot, Response, send_file

ssid = 'YOUR_SSID'
passwd = 'YOUR_PASSWD'

def wifi_connect(ssid, passwd):
    wifi = network.WLAN(network.STA_IF)
    wifi.active(True)
    if not wifi.isconnected():
        print('connecting to network...')
        wifi.connect(ssid, passwd)
        while not wifi.isconnected():
            print('.',end='')
            time.sleep_ms(500)
        print()
    return wifi

# アクセスポイントへの接続
wifi = wifi_connect(ssid, passwd)
print('network config:', wifi.ifconfig())

# WEBサーバーの設定
app = Microdot()
Response.default_content_type = 'text/html'

@app.route('/') # ルート(/: root)に対するレスポンス
def index(request):
    return send_file('/html/multi.html') # HIMLドキュメントをブラウザに返す

@app.get('/page1.html') # /page1に対するレスポンス
def page1(request):
    return send_file('/html/page1.html')

@app.get('/page2.html') # /page2に対するレスポンス
def page2(request):
    return send_file('/html/page2.html')

# WEBサーバーの実行
app.run(port=80, debug=True)

ファイルに保存されたHTMLドキュメントをブラウザに返すには、send_file()を使用します。

HTMLドキュメントの記述がなくなったので、プログラムが大変シンプルになりました。

HTMLドキュメントは、開発ボード上のファイルシステムにhtmlフォルダを作成し、そこに保存してください。

<!DOCTYPE html>
<html>
    <head>
        <title>Microdot Multi: Top</title>
        <meta charset="UTF-8">
    </head>
    <body>
        <h1>トップページ</H1>
        <h1>ページへのリンク</h1>
        <UL>
        <LI><a href=/page1.html>ページ1</a>
        <LI><a href=/page2.html>ページ2</a>
        </UL>
    </body>
</html>
<!DOCTYPE html>
<html>
    <head>
        <title>Microdot Multi: Page1</title>
        <meta charset="UTF-8">
    </head>
    <body>
        <h1>ページ1</H1>
        <h1>ページへのリンク</h1>
        <UL>
        <LI><a href=/>トップページ</a>
        <LI><a href=/page2.html>ページ2</a>
        </UL>
    </body>
</html>
<!DOCTYPE html>
<html>
    <head>
        <title>Microdot Multi: Page2</title>
        <meta charset="UTF-8">
    </head>
    <body>
        <h1>ページ2</H1>
        <h1>ページへのリンク</h1>
        <UL>
        <LI><a href=/>トップページ</a>
        <LI><a href=/page1.html>ページ1</a>
        </UL>
    </body>
</html>

HTMLドキュメントは、プログラムと独立して作成や修正ができるようになりました。

ファイルで保存されたHTMLドキュメント利用の一般化

microdot-multifile.pyの例では、HTMLドキュメントの名前を変えたり、ファイルを増やしたりすると、プログラムを変更する必要がありました。

ここでは、一般的なWEBサーバーと同様に、プログラムの変更なしに、HTMLドキュメントファイルの追加削除を行えるように変更したプログラム例を示します。

import network, time
from microdot import Microdot, Response, send_file

ssid = 'YOUR_SSID'
passwd = 'YOUR_PASSWD'

def wifi_connect(ssid, passwd):
    wifi = network.WLAN(network.STA_IF)
    wifi.active(True)
    if not wifi.isconnected():
        print('connecting to network...')
        wifi.connect(ssid, passwd)
        while not wifi.isconnected():
            print('.',end='')
            time.sleep_ms(500)
        print()
    return wifi

# アクセスポイントへの接続
wifi = wifi_connect(ssid, passwd)
print('network config:', wifi.ifconfig())

# WEBサーバーの設定
app = Microdot()
Response.default_content_type = 'text/html'

@app.route('/') # ルート(/: root)に対するレスポンス
def index(request):
    return send_file('/html/multi.html') # HIMLドキュメントをブラウザに返す

@app.get('/<path:file>') # すべてのHTMLファイルに対するレスポンス
def static(request, file):
    return send_file('/html/' + file)

# WEBサーバーの実行
app.run(port=80, debug=True)

27-29行の様に、明示的にパスを指定されたリクエストに対してレスポンスが記述されている場合には、その処理が行われますが、それ以外に対しては31-33行の処理が適用されます。31-33行の処理では、リクエストで与えられているファイル名を使用して、HTMLドキュメントファイルを取得してブラウザに送り返しています。指定されたHTMLドキュメントファイルがなければ単純にエラーとなります。

この方法をとったプログラムでは、プログラムを修正しなくても、HTMLドキュメントファイルを自由に追加削除できるようになります。

アプリケーション機能はありませんが、簡便な記述で、単純だけど汎用のWEBサーバーが出来上がりました。


動的ページの表示

これまでの説明で、静的な(内容が固定された)HTMLドキュメントの表示は行えるようになりました。静的な情報でなく、動的にその時々の情報を表示できるようになると、時間や温度をはじめ、様々な情報表示のアプリケーション機能をWEB上で構築できるようになります。

このように、様々な情報をHTMLドキュメントに動的に埋め込む機能として、多くのWEBサーバーフレームワークには、テンプレート機能が用意されています。

Microdotにもテンプレート機能が組み込まれていますので、ここでは、テンプレート機能を利用した動的ページの表示方法を紹介します。

日時の表示

ESP32を使用した開発ボードでは、バッテリバックアップしたRTCなどを搭載していなくても、NTPで正確な日時を取得できるのでとても便利です。WiFiでアクセスポイントに接続した後にNTPとRTCの時刻同期を行い、ブラウザからのリクエストに応じて、日付と時刻を含んだHTMLドキュメントを返すプログラムを示します。

import network, time, ntptime, utime
from machine import Pin, RTC
from microdot import Microdot, Response
from microdot_utemplate import render_template

ssid = 'YOUR_SSID'
passwd = 'YOUR_PASSWD'

def wifi_connect(ssid, passwd):
    wifi = network.WLAN(network.STA_IF)
    wifi.active(True)
    if not wifi.isconnected():
        print('connecting to network...')
        wifi.connect(ssid, passwd)
        while not wifi.isconnected():
            print('.',end='')
            time.sleep_ms(500)
        print()
    return wifi

# アクセスポイントへの接続
wifi = wifi_connect(ssid, passwd)
print('network config:', wifi.ifconfig())

# NTPとRTCの同期
rtc = RTC()
ntptime.settime() # RTCをNTPと同期
# 日本時間への修正
JST_OFFSET = 9 * 60 * 60 # 9時間(32400秒)
ut = utime.localtime(utime.time() + JST_OFFSET)
rtc.datetime((ut[0], ut[1], ut[2], ut[6]+1, ut[3], ut[4], ut[5], 0))

# WEBサーバーの設定
app = Microdot()
Response.default_content_type = 'text/html'

@app.route('/')
def index(request):
    dt = rtc.datetime()
    d="%4d/%02d/%02d" % dt[0:3] # 日付の取得
    t="%02d:%02d:%02d" % dt[4:7] # 時刻の取得
    return render_template('clock.tpl', date=d, time=t) # 日付と時刻を埋め込んだHTMLの生成

# WEBサーバーの実行
app.run(port=80, debug=True)

このプログラムでは、42行目のrender_template()がテンプレートファイルの書き換えを行います。

WEBサーバーのプログラムにより動的に値を埋め込んで作成されるHTMLドキュメントのもとになるファイルをテンプレートファイルと呼びます。テンプレートファイルの基本的な構成は普通のHTMLファイルと同様ですが、テンプレートファイルclock.tplの1行目に示されるように、プログラムから与えられる変数の定義と、12,13行目に示されるように、その変数を埋め込む場所の指定を行います。

ファイル名と拡張子に特段の制約はありませんが、ここではclock.tplとしています。このテンプレートファイルを、テンプレートファイルのデフォルトフォルダtemplatesに格納してください。

{% args date, time %}
<!DOCTYPE html>
<html>
    <head>
        <title>Microdot Template: Clock</title>
        <meta charset="UTF-8">
    </head>
    <body>
        <h1>テンプレート機能</h1>
        <p>NTPと同期した日時表示</p>
        <TABLE border=1>
        <TR><TH>日付</TH><TD>{{date}}</TD></TR>
        <TR><TH>時刻</TH><TD>{{time}}</TD></TR>
        </TABLE>
    </body>
</html>

テンプレートファイルclock.tplは、Microdotにより書き換えられ、例えば以下のような内容に変換されてWEBブラウザに送られます。


<!DOCTYPE html>
<html>
    <head>
        <title>Microdot Template: Clock</title>
        <meta charset="UTF-8">
    </head>
    <body>
        <h1>テンプレート機能</h1>
        <p>NTPと同期した日時表示</p>
        <TABLE border=1>
        <TR><TH>日付</TH><TD>2023/11/03</TD></TR>
        <TR><TH>時刻</TH><TD>17:19:56</TD></TR>
        </TABLE>
    </body>
</html>

天気予報の表示

気象庁が配信している天気予報データをWEBブラウザに表示するプログラムを示します。プログラムが天気予報を表示する地域は福岡になっていますが、27行と33行の数値コードを書き換えることにより、好きな地域の天気予報を表示させることができます。

ネットから取得してきた天気予報データを、テンプレート機能を使用してHTMLドキュメントに埋め込んでブラウザに送るという点ではmicrodot-nptclock.pyと同じ仕組みですが、テンプレート処理に渡すデータの与え方が異なっています。

このプログラムでは、3日分の天気予報データをテンプレート処理に渡すために、引数がリスト形式になっています。

import network, time, urequests
from machine import Pin
from microdot import Microdot, Response
from microdot_utemplate import render_template

ssid = 'YOUR_SSID'
passwd = 'YOUR_PASSWD'

def wifi_connect(ssid, passwd):
    wifi = network.WLAN(network.STA_IF)
    wifi.active(True)
    if not wifi.isconnected():
        print('connecting to network...')
        wifi.connect(ssid, passwd)
        while not wifi.isconnected():
            print('.',end='')
            time.sleep_ms(500)
        print()
    return wifi

# アクセスポイントへの接続
wifi = wifi_connect(ssid, passwd)
print('network config:', wifi.ifconfig())

# 気象庁予報データの取得:福岡県:40000, 東京都:130000,大阪府:270000
# 参照:https://www.jma.go.jp/bosai/common/const/area.json
jma_url = "https://www.jma.go.jp/bosai/forecast/data/forecast/400000.json"
forecast_json = urequests.get(jma_url).json()

forecast_major = forecast_json[0]["timeSeries"][0] # 参照データセットの選択

area=0 # 福岡県の福岡地方 1:北九州, 2:筑豊, 3:筑後
forecast_area=forecast_major["areas"][area]["area"]["name"] # 地方名は一つ
forecast_date = forecast_major["timeDefines"] # 3日分の日付のリスト
forecast_weather = forecast_major["areas"][area]["weathers"] # 3日分の天気のリスト
forecast_wind = forecast_major["areas"][area]["winds"] # 3日分の風向のリスト
# forecast_waves = forecast_major["areas"][0]["waves"][period]
    
# WEBサーバーの設定
app = Microdot()
Response.default_content_type = 'text/html'

@app.route('/')
def index(request):
    return render_template('weather.tpl', area=forecast_area,
                date=forecast_date, weather=forecast_weather, wind=forecast_wind)

# WEBサーバーの実行
app.run(port=80, debug=True)

このプログラムでも、テンプレートファイルweather.tplを、そのデフォルトフォルダtemplatesに格納してください。

{% args area, date, weather, wind %}
<!DOCTYPE html>
<html>
    <head>
        <title>Microdot Template: Weather Forecast</title>
        <meta charset="UTF-8">
    </head>
    <body>
        <h1>テンプレート機能</h1>
        <p>天気予報</p>
        <TABLE border=1>
                <TR><TH colspan=2 bgcolor="ffaaaa">{{area}}</TH></TR>
                {% for idx in range(0,len(date)) %}
                <TR><TH colspan=2 bgcolor="ffffaa">{{date[idx][:10]}}</TH></TR>
                <TR><TH>天気</TH><TD>{{weather[idx]}}</TD></TR>
                <TR><TH>風</TH><TD>{{wind[idx]}}</TD></TR>
                {% endfor %}
        </TABLE>
    </body>
</html>

1行目で指定されているデータのうち、date, weather, windは、それぞれ3日分のデータをリストで保持しています。13行からの部分は、forループを使用して、1日ごとの天気予報データを3回ほど表の要素として展開しています。

このように、テンプレートファイル内でも、簡単なプログラムを{% %}で括って書くことができます。

温度と湿度の表示

これまでのプログラムはESP32であれば実行可能でしたが、温度と湿度の表示には、そのセンサーが必要になります。以下のプログラム例は、AHT21を搭載したESP32-C3M-TRYでの例を示します。

import network, time
from machine import Pin, I2C
from aht21 import AHT21
from microdot import Microdot, Response
from microdot_utemplate import render_template

i2c = I2C(0)
aht21 = AHT21(i2c)

ssid = 'YOUR_SSID'
passwd = 'YOUR_PASSWD'

def wifi_connect(ssid, passwd):
    wifi = network.WLAN(network.STA_IF)
    wifi.active(True)
    if not wifi.isconnected():
        print('connecting to network...')
        wifi.connect(ssid, passwd)
        while not wifi.isconnected():
            print('.',end='')
            time.sleep_ms(500)
        print()
    return wifi

# アクセスポイントへの接続
wifi = wifi_connect(ssid, passwd)
print('network config:', wifi.ifconfig())

# WEBサーバーの設定
app = Microdot()
Response.default_content_type = 'text/html'

@app.route('/')
def index(request):
    temp='{:.1f}'.format(aht21.temperature)
    hum='{:.1f}'.format(aht21.relative_humidity)
    return render_template('env.tpl', temp=temp,hum=hum)

# WEBサーバーの実行
app.run(port=80, debug=True)

{% args temp, hum %}
<!DOCTYPE html>
<html>
    <head>
        <title>Microdot Example1</title>
        <meta charset="UTF-8">
    </head>
    <body>
        <h1>テンプレート機能</h1>
        <p>開発ボード周辺の温度と湿度</p>
        <TABLE border=1>
        <TR><TH>温度</TH><TD>{{temp}}°C</TD></TR>
        <TR><TH>湿度</TH><TD>{{hum}}%</TD></TR>
        </TABLE>
    </body>
</html>

関連記事

WEBページから開発ボードの機能を制御する方法を紹介しています。

時刻や天気などのNET上のサービスの利用法を紹介しています。