HaLake Magazine

コワーキングスペースHaLakeの技術情報発信サイト!IoT,AR,VRなど最新技術情報をお届け!

ESP32 MicroPython webサーバ~Lチカ

f:id:mischief_cat:20210314152503j:plain

今回はESP32でMicroPythonからWi-Fiを使ってwebサーバを立てたり、その応用をやってみようと思います。

TODO:前の記事の作成とURL張り ESP32でMicroPythonを使えるようにするまでの記事があります。すでに書き込みと実行が出来る前提で話を進めていくので、そちらの記事も合わせて読んで頂けると嬉しいです。



目次

  1. 準備するもの
  2. webサーバを立てる
  3. webでピンの情報を見よう
  4. URLでピンの制御をしてみよう
  5. まとめ



準備したもの

  1. ESP32
  2. LED
  3. ブレッドボード
  4. ジャンパワイヤ
  5. 抵抗(10kΩ,22kΩ)



webサーバを立てる

まずはwebサーバを立ててみましょう。

公式に情報が載っているのでこちらを参考に行っていきます。

micropython-docs-ja.readthedocs.io

↑のリンクにwebサーバを立てるシンプルなプログラムが載っているのでこれを参考にWiFiに接続して、ステーションモード(子機)として動くプログラムを作ります。

最初はとりあえず短く、最小限でhello worldをやってみました。

import network
import socket

#///WiFiに接続
sta_if = network.WLAN(network.STA_IF)
sta_if.active(True)
print('connecting to network...')
sta_if.connect('SSID', 'PASS')#SSIDとPASSは書き換えが必要
while not sta_if.isconnected():
    pass
#///

#///ソケットの作成・登録・接続準備
addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1]
s = socket.socket()
s.bind(addr)
s.listen(1)
print('listening on', sta_if.ifconfig()[0])
#///

#///ソケットの接続待機・送信・切断
while True:
    cl, addr = s.accept()
    print('client connected from', addr)
    response = '<h1>HELLO WORLD</h1>'
    cl.send(response)
    cl.close()
#///

このプログラムを使うときは以下の行を説明に従って書き換えて使用してください。

sta_if.connect('SSID', 'PASS')#SSIDとPASSは書き換えが必要

次の箇所を書き換えてください 。 以降のプログラムでも同じように書き換える必要があります。

  • SSIDを読者が使っているWiFIの名前(SSID)
  • PASSを読者が使っているWiFIのパスワード

  • 注意:この時点で先ほどのプログラムを実行してconnecting to network...と表示されてから動かない場合は書き換えたSSIDとPASSのスペルミスなどを確認してください。

プログラムの書き込み方はREPLに直接コピペする方法かファイルとしてアップロードして頂く方法どちらでも結構ですが、ファイルとしてアップロードした場合は各自で付けてもらったファイル名.pyとなっていると思うので、アップロード後REPLに

import ファイル名

とするだけで実行してくれます。いちいちインポートして関数を呼び出すのが(コマンドの履歴が残らないのもあって)面倒だったので以降でもこの方式をとらせていただきます。

WiFIに接続するためのプログラムは下のリンクを参考にしました。

micropython-docs-ja.readthedocs.io

ローカルの WiFi ネットワークに接続するには、次の関数を流用してください

と書かれているのでこちらをシンプルに書き換えたのが#///WiFiに接続とコメントに書かれている箇所にあたります。

参考にしたシンプルなプログラムでは

addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1]
省略
print('listening on', addr)

となっていますが、これだとaddrsocket.getaddrinfoで設定したIPアドレス0.0.0.0(ローカルマシン上のすべてのIPアドレス)が代入されてしまい、listening on 0.0.0.0と表示されてしまうので、割り当てられたIPアドレスがわかりやすく表示されるように

print('listening on', sta_if.ifconfig()[0])

としてあります。

筆者は勉強不足で知らなかったのですが、プログラム中で出てくるsocketbindなどの言葉はサーバサイドのプログラムでは当たり前の用語だそうです。

プログラムの流れや用語について勉強になったので参考にさせて頂いたサイトのリンクを貼っておきます。プログラムの書式は今回の内容に関係ないのでそこだけは注意してください。

qiita.com

プログラム実行後はWiFi接続に成功すると必ず、

listening on 192.168.X.X

と出力されるので、ブラウザを開きxの部分は各自の環境に合わせて出力される内容と同じく書き換えてください。(以降は同じような説明を省きます。)

http://192.168.x.x

にアクセスするとHELLO WORLDと表示されると思います。

f:id:mischief_cat:20210316231439p:plain



webでピンの情報を見よう

実は前項の参考にしたシンプルなプログラムはピンの状態(HIGH/LOW)をwebに表示してくれるプログラムでした。

なので、一部変更しつつ最低限動くようなプログラムを以下に示します。

import network
import socket
from machine import Pin

available_pins = [4, 5, 12, 13, 14, 15, 18, 19, 21, 22, 23, 25, 26, 27]
pins = [Pin(i, Pin.IN, Pin.PULL_UP) for i in available_pins]

html = """<!DOCTYPE html>
<html>
    <head> <title>ESP32 Pins</title> </head>
    <body> <h1>ESP32 Pins</h1>
        <table border="1"> <tr><th>Pin</th><th>Value</th></tr> %s </table>
    </body>
</html>
"""

#///WiFiに接続
sta_if = network.WLAN(network.STA_IF)
sta_if.active(True)
print('connecting to network...')
sta_if.connect('SSID', 'PASS')#SSIDとPASSは書き換えが必要
while not sta_if.isconnected():
    pass
#///

#///ソケットの作成・登録・接続準備
addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1]
s = socket.socket()
s.bind(addr)
s.listen(1)
print('listening on', sta_if.ifconfig()[0])
#///

#///ソケットの接続待機・送信・切断
while True:
    cl, addr = s.accept()
    print('client connected from', addr)
    rows = ['<tr><td>%s</td><td>%d</td></tr>' % (str(p), p.value()) for p in pins]
    response = html % '\n'.join(rows)
    cl.send(response)
    cl.close()
#///

私も動かして初めてわかったのですが、初期状態でピンのプルアップが有効になっていました。

Pin(i, Pin.IN, Pin.PULL_DOWN)

とするとプルダウンが有効になって値が反転します。

リファレンスには手動で設定していたので意外でしたが、経験不足なのでこれが普通なのかはわかりません。

micropython-docs-ja.readthedocs.io

↑のリンクのリファレンスに載っているようにHIGH/LOWの入出力は以下のピンが使用可のです。

0-19, 21-23, 25-27

このピンはプルアップ、プルダウン両方とも問題なく使えました。

  • 注意:ピン0, 2番は起動時のモード設定に関わっているため、使用する際は注意してください。下に参考サイトを載せるのでESP32-WROOM-32 の概要→起動モードを参照してください。

ht-deko.com

プログラムで設定しているピンは上記の注意で示したピン以外を指定しています。

  • 番号が離散的なため、配列に一旦格納して使っているので、ピンの番号と配列の添え字の対応に注意してください。(以降説明を省く)

プログラム実行後、以下のようににアクセスして実際に動かしてみると、プログラム通りであればプルアップされているので何も配線していなけれなすべて1で表が作成されます。

http://192.168.x.x

使えるピンから適当に選んだ4番ピンとジャンパワイヤでGNDにつなぐいでリロードすると

f:id:mischief_cat:20210316225924p:plain

Pin(4)が0になっていれば成功です。



URLでピンの制御をしてみよう

以下の項を書くのにあたって参考にさせて頂いたサイトのリンクを下に貼っておきます。

qiita.com

URLでピン制御をしようと思ったとき中途半端に知識がついてしまうと、過信して「どんなクエリが来たか返してくれる関数があるだろう」と思ってしまい、無駄に時間をかけて調べてしまいました。(Arduino core for the ESP32のライブラリにはあるので)

リファレンスと上記のサイトを見るとsocket.recv(bufsize)という関数があり、これでクライアントからデータの塊を受信して、その塊の中からクエリの部分だけを手動トリミングする方法をとっていました。他に方法が見つからないためこちらを採用させていただきました。

あくまで通信をするためのモジュールだったことに気付けませんでした。

また、

re.search(r'on=([+-]?\d+)', request)

などの[+-]?\d+は以下を参考にさせて頂き、整数の正規表現を意味しています。

qiita.com

LEDを制御しよう

まずは回路で、同じように組んでみてください。

f:id:mischief_cat:20210316180646p:plain

使用した抵抗は10kΩです。

今回はHIGH/LOWの出力なので前回と同じピン番号の集合が指定されています。

import network
import socket
import time
from machine import Pin 
import re

available_pins = [4, 5, 12, 13, 14, 15, 18, 19, 21, 22, 23, 25, 26, 27]
pins = [Pin(i, Pin.OUT) for i in available_pins]#使えるピン番号が離散的なので配列の添え字とピンの番号の対応に注意

#///WiFiに接続
sta_if = network.WLAN(network.STA_IF)
sta_if.active(True)
print('connecting to network...')
sta_if.connect('SSID', 'PASS')#SSIDとPASSは書き換えが必要
while not sta_if.isconnected():
    pass
#///

#///ソケットの作成・登録・接続準備
addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1]
s = socket.socket()
s.bind(addr)
s.listen(1)
print('listening on', sta_if.ifconfig()[0])
#///

def change_led_st(qs, st:bool):
    qs_num = int(qs.group(1))
    pins[qs_num].value(st)
    status = 'ON' if st == True else 'OFF'
    response = '<h1>ESP32 LED {} : {} Pin</h1>'.format(status, available_pins[qs_num])
    cl.send(response)

while True:
    cl, addr = s.accept()
    print('client from', addr)
    request = str(cl.recv(1024)).lower()
    qs_on = re.search(r'on=([+-]?\d+)', request)
    qs_off = re.search(r'off=([+-]?\d+)', request)

    if qs_on != None:
        change_led_st(qs_on, True)
    elif qs_off != None:
        change_led_st(qs_off, False)
    else:
        response = '<h1>ESP32 LED ERROR</h1>'
        cl.send(response)

    cl.close()

上記のプログラムを書き込んだ後、実行して正常に動作するとlistening on 192.168.x.xと出力されるので、ブラウザを開きxの部分は各自の環境に合わせて出力される内容と同じく書き換えて

http://192.168.x.x/on=5

にアクセスすると

f:id:mischief_cat:20210316175411p:plain

このように表示されるのでLEDを確認すると光っていると思います。

また、

http://192.168.x.x/off=5

にアクセスするとLEDが消えると思います。

f:id:mischief_cat:20210316175456p:plain

URLの最後の数字を0~15(配列の添え字番号)に変えることによって対応するピンのHIGH/LOWを切り替えられるようになっています。

上の例ではURLで5を指定しているので入れるの6(5+1)番目に格納されている15番ピンの制御をしてみました。

指定するピンを変える場合は抵抗につながっているジャンパワイヤを対応するピン番号に差し替えるのを忘れないで下さい。



webページでアナログ値を見よう

まずは回路図と同じように組んでみてください。

しっかりと測れているのか検証するために分圧した電圧を計測します。

抵抗の場所を入れ替えて2通りの測定をしてみます。

読者の方は両方か一方どちらでも構いません。

測定法:A(10kΩの電圧測定) f:id:mischief_cat:20210316182819p:plain

測定法:B(22kΩの電圧測定) f:id:mischief_cat:20210316203516p:plain

使っている抵抗は10kと22kΩなので違う抵抗値を使う場合は値が変わるので注意してください。

アナログ値を読み取る際は注意が必要で前項までとは違うピンを使用します。

micropython-docs-ja.readthedocs.io

上記のリファレンスにもあるように32~39番でしかアナログ値を読むADCが使えないのに加え、以下に注意してください。

  • esp32には内部にホールセンサを搭載しているので、これと併用する場合にはADCの36、37番ピンは同時に使えないので注意が必要です。参考リンクを下に貼っておきます。
  • 37、38番ピンは組み込まれている回路で既に使用されているのでピンとして引き出されていない。
  • 36、39番ピンはそれぞれVP、VNピンに割り振られている。

trac.switch-science.com

esp32.com

import network
import socket
import time
from machine import Pin, ADC
import re

adc_pins = [ADC(Pin(i)) for i in (32, 33, 34, 35,  36, 39)]#ADCに使えるピンが決まっているので、配列の添え字(0~7)とピン(32~39)の番号の対応に注意
for i in adc_pins:
    i.atten(ADC.ATTN_11DB)

#///WiFiに接続
sta_if = network.WLAN(network.STA_IF)
sta_if.active(True)
print('connecting to network...')
sta_if.connect('SSID', 'PASS')#SSIDとPASSは書き換えが必要
while not sta_if.isconnected():
    pass
#///

#///ソケットの作成・登録・接続準備
addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1]
s = socket.socket()
s.bind(addr)
s.listen(1)
print('listening on', sta_if.ifconfig()[0])
#///

def pin_read(qs):
    qs_num = int(qs.group(1))
    val = adc_pins[qs_num].read()
    response = '<h1>ESP32 Read {} Pin : {}</h1>'.format(qs_num+32, val)
    cl.send(response)

#///ソケットの接続待機・送受信・切断
while True:
    cl, addr = s.accept()
    print('client connected from', addr)
    request = str(cl.recv(1024)).lower()
    qs = re.search(r'read=([+-]?\d+)', request)

    if qs != None:
        pin_read(qs)
    else:
        response = '<h1>ESP32 READ ERROR</h1>'
        cl.send(response)

    cl.close()
#///

上記のプログラムでは

adc_pins = [ADC(Pin(i)) for i in range(32, 40)]#ADCに使えるピンが決まっているので、配列の添え字(0~7)とピン(32~39)の番号の対応に注意
i.atten(ADC.ATTN_11DB) for i in adc_pins:

として、11dBの減衰率を設定することで、約3.6Vまでの12bitアナログ値を取得できるようにしています。

本来は1V12bitがデフォルトで入力電圧と戻り値のbit数も変更できるようなのでADCピンの説明で載せたリンクのリファレンスを参考にしてください。

esp32のADCは公式がある程度の誤差があると言っているのでそこも注意する必要があります。

esp32.com

実際にアナログ値が取れているか確認してみましょう。

使う抵抗は22kΩと10kΩの二種類で3.3Vを分圧して測ってみます。

プログラム実行後、

http://192.168.x.x/read=1

とすると、URL最後の数字がadc_pins配列の添え字番号にあたるので、今回は1を指定しているので配列二番目の33番ピンを読み取ることを意味しています。

測定法:A f:id:mischief_cat:20210316224348p:plain

測定法:B f:id:mischief_cat:20210316224430p:plain

f:id:mischief_cat:20210316224015p:plain

左側が理想の値とマルチメータを使った計測で右側がwebページに表示されたアナログ値を電圧に変換したものになります。

結果はこんな感じで、計算は以下サイトのように計算しました。

分解能は12bit計算です。

hegtel.com

miraiworks.org

表の同じ色がついている箇所が重要なので注目してみると少しのずれはありますが、概ね良いアナログ値がとれているので問題なく動作していると思います。

esp32の最大測定電圧が約3.6Vとリファレンスに書かれていたのでアナログ値から変換する計算で3.6Vを使用したことによる誤差(それぞれ3~4%程度)が大きくなっているんだと思います。



まとめ

  • クエリストリングで操作できるようにすることで応用しやすくなった。
  • サーバ側の基本的な処理の流れを学ぶことが出来た。