CircuitPythonにてESP32にWebサーバを構築する2つのモード

# 「2つのモード」とは、

一つは**APモード**、もう一つは**ステーションモード**のこと。

- **APモード** &nbsp;&nbsp;ESP32をWifiのアクセスポイントにしてローカルWebサーバーを構築する場合に使うモード
- **ステーションモード** &nbsp;&nbsp;ESP32を既設のWifiルータに接続することでインターネットにもアクセスできるWebサーバを構築する場合に使うモード


`adafruit_httpserver.server`を使ったCircuitPythonでのコーディング例が少ないので、ここにまとめておく。特にAPモードの例はまったく見つけられなかった。


# Wifiの設定

## 1. APモード

```python:APモード
import wifi

ssid = 'XXXXX' #公開するSSID
psk = 'YYYYYY' #パスワード

wifi.radio.start_ap(ssid=ssid, password=psk)
print(wifi.radio.ipv4_address_ap)
# 192.168.4.1
```

スマホやPCから上記で指定したSSIDに接続する。
passwordは省略できるが、セキュリティが弱くなるので省略はお勧めしない。

APのIPアドレスは`192.168.4.1`。これを変更する方法は分からない。
スマホやPCをこのSSIDに接続すると、`192.168.4.2〜`のIPアドレスが割り当てられる。


## 2. ステーションモード

```python:ステーションモード
import wifi

ssid = 'XXXXX' #接続するSSID
psk = 'YYYYYY' #パスワード

wifi.radio.connect(ssid=ssid, password=psk)
print(wifi.radio.ipv4_address)
# 192.168.xx.xx 
```

IPアドレスは接続したWifiルーターによって払い出されたIPアドレスとなる。
もし、固定IPアドレスとする場合は、下記のコードを追加する。

```diff_python:ステーションモード：固定IPアドレス
  import wifi
+ import ipaddress

+ ipv4 = ipaddress.IPv4Address("192.168.xx.xx")
+ netmask = ipaddress.IPv4Address("255.255.255.0")
+ gateway = ipaddress.IPv4Address("192.168.xx.1")
+ wifi.radio.set_ipv4_address(ipv4=ipv4, netmask=netmask, gateway=gateway)

  ssid = 'XXXXX' #接続するSSID
  psk = 'YYYYYY' #パスワード

  wifi.radio.connect(ssid=ssid, password=psk)
  print(wifi.radio.ipv4_address)
# 192.168.xx.xx 
```

## 3. ホスト名の設定

APモード、ステーションモードのどちらの場合でも、mDNSプロトコルによるホスト名を設定することができる。
ポート番号は、この次に設定するHTTPサーバのポート番号と合わせておくとよい。


```python:It is possible to use the mDNS protocol to make the server accessible via a hostname in addition to an IP address.

#wifi.radio.start_ap(ssid=ssid, password=psk)
# or
wifi.radio.connect(ssid=ssid, password=psk)

mdns_server = mdns.Server(wifi.radio)
mdns_server.hostname = "esp32s3board" #下記注記
mdns_server.advertise_service(service_type="_http", protocol="_tcp", port=80)
```

- ここで指定した`文字列.local`が実際のホスト名となる（上の例だと`esp32s3board.local`）。<br>また、hostnameの設定を省略すると、`cpy-xxyyzz.local`（xxyyzzはMACアドレスの下3バイト）となる。

ホスト名でアクセスできるので活用したい。

```shell
% ping esp32s3board.local
PING esp32s3board.local (192.168.4.1): 56 data bytes
64 bytes from 192.168.4.1: icmp_seq=2 ttl=255 time=6.971 ms
64 bytes from 192.168.4.1: icmp_seq=3 ttl=255 time=4.253 ms
64 bytes from 192.168.4.1: icmp_seq=4 ttl=255 time=8.206 ms
^C
--- esp32s3board.local ping statistics ---
5 packets transmitted, 3 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 4.253/6.477/8.206/3.634 ms
```

<br>


APモード、もしくは ステーションモードでWifiに接続できたので、次はHTTPサーバを設定する。

# HTTPサーバ

[adafruitのバンドルライブラリ](https://docs.circuitpython.org/en/latest/docs/libraries.html#circuitpython-library-bundles)を使用する。

```python:HTTPサーバ
# already connected to WiFi
import socketpool
from adafruit_httpserver.server import HTTPServer
from adafruit_httpserver.request import HTTPRequest
from adafruit_httpserver.response import HTTPResponse
from adafruit_httpserver.methods import HTTPMethod
from adafruit_httpserver.mime_type import MIMEType

pool = socketpool.SocketPool(wifi.radio)
server = HTTPServer(pool)

@server.route("/")
def base(request: HTTPRequest):
    with HTTPResponse(request, content_type=MIMEType.TYPE_HTML) as response:
        #response.send_file("index.html")
        response.send("・・・・") #html

@server.route("/", method=HTTPMethod.POST)
def buttonpress(request: HTTPRequest):
    raw_text = request.raw_request.decode("utf8")
    print(raw_text)
    # POSTされた情報に対応した処理を行う

    with HTTPResponse(request, content_type=MIMEType.TYPE_HTML) as response:
        response.send("・・・・") #html

host_address = str(wifi.radio.ipv4_address) #ステーションモードの場合
#host_address = str(wifi.radio.ipv4_address_ap) # APモードの場合
server.start(host=host_address, port=8080) #ポート番号省略時は 80

while True:
    try:
        server.poll()
    except OSError as error:
        print(error)
        continue

```

# 実装例

GPIO3に接続した[WS2812C](http://www.world-semi.com/Certifications/details-139-4.html)を光らせる色を制御するローカルWebサーバ

```python:ローカルWebサーバ（CircuitPython）
import os
import time
import wifi
import socketpool
import board
import microcontroller
from adafruit_httpserver.server import HTTPServer
from adafruit_httpserver.request import HTTPRequest
from adafruit_httpserver.response import HTTPResponse
from adafruit_httpserver.methods import HTTPMethod
from adafruit_httpserver.mime_type import MIMEType
import neopixel
import random
from adafruit_datetime import datetime

# neopixel on board 
pixel_pin = board.IO3
num_pixels = 1
OFF = (0, 0, 0)
RED = (255, 0, 0)
YELLOW = (255, 150, 0)
GREEN = (0, 255, 0)
CYAN = (0, 255, 255)
BLUE = (0, 0, 255)
PURPLE = (180, 0, 255)
WHITE = (255, 255, 255)
COLORS = [RED, YELLOW, GREEN, CYAN, BLUE, PURPLE, WHITE]
pixels = neopixel.NeoPixel(pixel_pin, num_pixels, brightness=0.3, auto_write=True)
pixels[0] = OFF
last_color = None

#wifi ssid & psk
ssid = 'XXXXX' #公開するSSID
psk = 'YYYYYY' #パスワード

wifi.radio.start_ap(ssid=ssid, password=psk)

time.sleep(1)

print(f'WiFi AP mode Started! SSID is {ssid}')

pool = socketpool.SocketPool(wifi.radio)
server = HTTPServer(pool)

print(f'Web Server Started! Please access to http://{wifi.radio.ipv4_address_ap}')

document_root = '/www_root'

def webpage(filename, root):
    filepath = root + '/' + filename
    with open(filepath, 'r') as f:
        html = f.read()

    html = html.replace('$TEMP$', f'{microcontroller.cpu.temperature:.1f}')
    html = html.replace('$NOW$', f'{datetime.now()}')
    return html

@server.route("/")
def base(request: HTTPRequest):
    with HTTPResponse(request, content_type=MIMEType.TYPE_HTML) as response:
        response.send(webpage('index.html', document_root))

@server.route("/", method=HTTPMethod.POST)
def buttonpress(request: HTTPRequest):
    global last_color
    raw_text = request.raw_request.decode("utf8")
    print(raw_text)
    last_color = None
    if "RED" in raw_text:
        last_color = RED
    if "GREEN" in raw_text:
        last_color = GREEN
    if "BLUE" in raw_text:
        last_color = BLUE
    if "WHITE" in raw_text:
        last_color = WHITE
    if "YELLOW" in raw_text:
        last_color = YELLOW
    if "OFF" in raw_text:
        last_color = OFF
    if last_color is not None:
        pixels[0] = last_color
    elif "RANDOM" in raw_text:
        for _ in range(15):
            while (color := random.choice(COLORS)) == last_color:
                pass
            last_color = color
            pixels[0] = color
            time.sleep(0.1)

    with HTTPResponse(request, content_type=MIMEType.TYPE_HTML) as response:
        response.send(webpage('index.html', document_root))

server.start(host=str(wifi.radio.ipv4_address_ap), root_path=document_root)

while True:
    try:
        server.poll()
    except OSError as error:
        print(error)
        continue

```

```html:index.html（Bootstrap使用）
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Local Web Server Demo</title>
  <link href="css/bootstrap.min.css" rel="stylesheet">
  <script src="js/popper.min.js"></script>
  <script src="js/bootstrap.bundle.min.js"></script>
</head>
<body>
  <p class="text-center bg-primary text-info h1">RGB-LED Color Controller</p>
  <div id="wrap" style="display: block;">
    <p class="h6">This is RGB-LED color controller on ESP32 original board using a local HTTP server with CircuitPython. </p>
    <div class="container-fluid d-grid gap-2">
      <div id="loader" style="display: block;">
        <div class="spinner-border spinner-border text-muted" role="status">
          <span class="visually-hidden">Loading...</span>
        </div>
      </div>
      <form accept-charset="utf-8" method="POST">
        <button type="submit" class="btn btn-primary w-100" name="RGB" value="BLUE">Blue</button>
      </form>
      <form accept-charset="utf-8" method="POST">
        <button type="submit" class="btn btn-success w-100" name="RGB" value="GREEN">Green</button>
      </form>
      <form accept-charset="utf-8" method="POST">
        <button type="submit" class="btn btn-danger w-100" name="RGB" value="RED">Red</button>
      </form>
      <form accept-charset="utf-8" method="POST">
        <button type="submit" class="btn btn-warning w-100" name="RGB" value="YELLOW">Yellow</button>
      </form>
      <form accept-charset="utf-8" method="POST">
        <button type="submit" class="btn btn-outline-dark w-100" name="RGB" value="WHITE">White</button>
      </form>
      <form accept-charset="utf-8" method="POST">
        <button type="submit" class="btn btn-secondary w-100" name="RGB" value="OFF">Off</button>
      </form>
      <form accept-charset="utf-8" method="POST">
        <button type="submit" class="btn btn-info w-100" name="RGB" value="RANDOM">Random</button>
      </form>
      </form>
    </div>
    <hr>
    <p class="lead text-center">CPU Temperature : $TEMP$ ﾟC</p>
    <p class="text-primary text-end h7">at $NOW$ &nbsp;</p>
  </div>
  <script>
    window.onload = () => {
      const loader = document.getElementById('loader');
      loader.style.display = "none";
    }
    window.onsubmit = () => {
      const loader = document.getElementById('loader');
      loader.style.display = "block";
    }
  </script>
</body>
</html>
```

<br />
スマホから接続したときの画面と自作ボード（裏面にESP32S3を実装）

![IMG_0018.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/90926ef2-3a93-7e5e-926d-8b259117ba50.png) ![IMG_1518.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/0bd5ef74-b0ed-8b44-113f-0227956a680e.png)


# まとめ

**Wifi設定**と**HTTPサーバ設定**の2段階で、ESP32にWebサーバを構築できる。

APモードでWebサーバを構築した場合は、BootstrapやJQueryのCDNにアクセスできないため、あらかじめダウンロードしておき、ESP32にコピーを置いて使う必要がある。

当初、[MicroWebSrv2](https://github.com/jczic/MicroWebSrv2)をCircuitPythonで使おうと試みたが、簡単には行かず断念した。

<br />

**以上**