CircuitPythonにてESP32にRESTなAPIサーバを構築する  

# はじめに

今回は、[前回の記事](https://qiita.com/nak435/items/72c4751f2a3bb2c8009f)で構築したWebサーバを改造して、RESTなAPIサーバに仕立てることを行う。


まずは、adafruitバンドルライブラリのHTTPサーバが実現しているアクセスパス（URI）の切り分けの方法を見ておく。

# adafruit_httpserver.server.HTTPServerのルート（アクセスパス）切り分けの方法

server.routeをデコレートして、アクセスされる〔パス（URI）とHTTPメソッド〕のペアごとに処理（ハンドラ）を定義しておくことで実現する。

```python:（コード抜粋）

#(1) path:/ method:GET
@server.route(path="/") #method省略時はGET
def base(request: HTTPRequest):
    print(query_params)
    # path:/ method:GETの処理
      :  : 
    with HTTPResponse(request, content_type=MIMEType.TYPE_HTML) as response:
        response.send_file("index.html")


#(2) path:/ method:POST
@server.route(path="/", method=HTTPMethod.POST)
def base_post(request: HTTPRequest):
    raw_text = request.raw_request.decode("utf8")
    print(raw_text)
    # path:/ method:POSTの処理
    # POSTされた情報に対応した処理を行う
      :  : 
    with HTTPResponse(request, content_type=MIMEType.TYPE_HTML) as response:
        response.send("・・・・") #html


#(3) path:/RGB method:PUT
@server.route(path="/RGB", method=HTTPMethod.PUT)
def rgb_put(request: HTTPRequest):
    # path:/RGB method:PUTの処理
      :  : 


#(4) otherwise
# 上記以外の場合 ⇒ そのような定義はできない

```

ブラウザ等のクライアントからサーバのパス（URI）にアクセスされると、サーバはデコレートされた〔パスとHTTPメソッド〕のペアを比較して**完全一致**するハンドラを呼び出すことを行う。

完全一致するハンドラが定義されていない場合、サーバは、
1. **GETメソッドでパスが`/`の場合は**、`/index.html`をファイルシステムから入力してレスポンスを返す。
2. **GETメソッドでパスが`/`以外の場合は**、パス全体をファイルパスとして、ファイルシステムから入力してレスポンスを返す。これで、CSSファイルやJSファイル、imageファイルなどの読み込みを実現している。<br />もしファイルが存在しない場合は、Status`404`(Not Found)のレスポンスを返す。
3. **それ以外の場合は**、Status`400`(Bad Request）のレスポンスを返す。

RESTなAPIサーバにするため、では、上記のコードにて**PUTメソッドでパス**`/RGB/RED`をリクエストした場合は、どうなるか？　答え **⇒** <i><b>ハンドラが未定義のため、`400`エラーとなる。</b></i>

色の種類数と同じだけのハンドラを定義すればよいのだが、色設定APIを`PUT /RGB/color`とするため、ハンドラを一つで定義したいのだ。

そこで、**routeの定義を改造し**、パスの完全一致に加え、**前方一致でのハンドラ定義を可能とする**。

使い方は、↓こうだ。routeの引数pathの最後の文字が`*`のとき、それより前の文字列が一致（前方一致）する場合に、そのメソッドが呼び出されるようになる。

```python:
#(3) path:/RGB/color method:PUT
@server.route(path="/RGB/*", method=HTTPMethod.PUT)
def rgb_put(request: HTTPRequest):
  #request.pathを参照しcolorを決定する
  :  : 

```


# adafruit_httpserver.route._HTTPRouteを改造

githubから[ソースコード](https://raw.githubusercontent.com/adafruit/Adafruit_CircuitPython_HTTPServer/main/adafruit_httpserver/route.py)を入手し、数行手を加えるだけ。


```diff_python:adafruit_httpserver.route._HTTPRoute
# SPDX-FileCopyrightText: Copyright (c) 2022 Dan Halbert for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
`adafruit_httpserver.route._HTTPRoute`
====================================================
* Author(s): Dan Halbert, Michał Pokusa
"""

#from .methods import HTTPMethod
from adafruit_httpserver.methods import HTTPMethod


class _HTTPRoute:
    """Route definition for different paths, see `adafruit_httpserver.server.HTTPServer.route`."""

    def __init__(self, path: str = "", method: HTTPMethod = HTTPMethod.GET) -> None:

        self.path = path
        self.method = method

    def __hash__(self) -> int:
        return hash(self.method) ^ hash(self.path)

    def __eq__(self, other: "_HTTPRoute") -> bool:
+       if self.path[-1] == '*':
+           prepath = self.path[0:len(self.path)-1]
+           return self.method == other.method and other.path.startswith(prepath)
        return self.method == other.method and self.path == other.path

    def __repr__(self) -> str:
        return f"HTTPRoute(path={repr(self.path)}, method={repr(self.method)})"

```


# 実装例

[前回の記事の実装例](https://qiita.com/nak435/items/72c4751f2a3bb2c8009f#%E5%AE%9F%E8%A3%85%E4%BE%8B)では、colorの指定はPOSTのパラメタで実装していたが、今回のRESTなAPIサーバでは、`PUT /RGB/color`を非同期通信で呼び出す。

非同期通信はJavaScriptで実現するため、HTML/JSから先に示す。

```html:index.html（非同期通信）
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>API 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>
  <script>
    function callAPI(url) {
      const requestOptions = {
        method: 'PUT',
        headers: { 'Content-Type': 'text/plain' }
      //body: 無し
      };
      fetch(url, requestOptions)
        .then(response => {
          return response.json();
        })
        .then(json => {
          const temp_id = document.getElementById('temperature');
          temp_id.textContent = json['temperature'];
          const now_id = document.getElementById('now');
          now_id.textContent = json['now'];
        })
        .catch(error => {
          console.log("fetch failer");
        });
    };    
  </script>
  <p class="text-center bg-primary text-info h1">RGB Color Controller API</p>
  <div id="wrap" style="display: block;">
    <p class="h6">This is RGB-LED color controller on ESP32 original board using a custom API server with CircuitPython. </p>
    <div class="container-fluid d-grid gap-2">
      <button type="button" class="btn btn-primary w-100" onclick="callAPI('/RGB/Blue');">Blue</button>
      <button type="button" class="btn btn-success w-100" onclick="callAPI('/RGB/Green');">Green</button>
      <button type="button" class="btn btn-danger w-100" onclick="callAPI('/RGB/Red');">Red</button>
      <button type="button" class="btn btn-warning w-100" onclick="callAPI('/RGB/Yellow');">Yellow</button>
      <button type="button" class="btn btn-outline-dark w-100" onclick="callAPI('/RGB/White');">White</button>
      <button type="button" class="btn btn-secondary w-100" onclick="callAPI('/RGB/Off');">Off</button>
      <button type="button" class="btn btn-info w-100" onclick="callAPI('/RGB/Random');">Random</button>
    </div>
    <hr>
    <p class="lead text-center">CPU Temperature : <span id="temperature">$TEMP$</span> ﾟC</p>
    <p class="text-primary text-end h7">at <span id="now">$NOW$</span> &nbsp;</p>
  </div>
</body>
</html>
```

色設定のAPIを`fetch`で呼び出している。リターン情報として、画面下部に表示するESP32のコア温度と現在時刻をjsonで受け取り、HTML(DOM)の該当テキストを直接更新する。
もし、パラメタを有するAPIならば、ボディにJSONを設定することもできる。

APIサーバのコードを次に示す。

```python:APIサーバ（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
import json

# 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]
colorSet = {'RED':RED, 'YELLOW':YELLOW, 'GREEN':GREEN, 'CYAN':CYAN, 'BLUE':BLUE, 'PURPLE':PURPLE, 'WHITE':WHITE, 'OFF':OFF}
pixels = neopixel.NeoPixel(pixel_pin, num_pixels, brightness=0.3, auto_write=True)
pixels[0] = OFF
last_color = None

#wifi ssid & psk
SSID = "XXXXX"
PSK = "YYYYY"

wifi.radio.connect(ssid=SSID, password=PSK)

time.sleep(1)

print(f'WiFi Connected! SSID is {SSID}')

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

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

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

#(1) path:/ method:GET ⇒ index.htmlを応答する 
@server.route("/")
def base(request: HTTPRequest):
    with HTTPResponse(request, content_type=MIMEType.TYPE_HTML) as response:
        response.send(webpage('index.html', document_root))

#(2) path:/RGB/color method:PUT ⇒ RGB-LEDの色を設定して、コア温度と現在時刻をjsonで応答する 
@server.route("/RGB/*", method=HTTPMethod.PUT)
def rgb(request: HTTPRequest):
    global last_color
    key = request.path.split('/')[-1].upper()
    print(key)
    if key in colorSet:
        color = colorSet[key]
        last_color = color
        pixels[0] = last_color
    elif key == "RANDOM":
        for _ in range(15):
            while (color := random.choice(COLORS)) == last_color:
                pass
            last_color = color
            pixels[0] = color
            time.sleep(0.1)
    else:
        HTTPResponse(request, status=CommonHTTPStatus.BAD_REQUEST_400).send()
        return

    with HTTPResponse(request, content_type=MIMEType.TYPE_JSON) as response:
        json_obj = { 
            "temperature": f"{microcontroller.cpu.temperature:.1f}",
            "now": f"{datetime.now()}"
        }
        response.send(json.dumps(json_obj))


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

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

ページ全体の更新が無いため、ボタンを押した時のレスポンス性能が断然よくなった。


# 任意の色を設定

HTML/JSとサーバ処理に少し手を加え、colorを色名以外に任意の色[`#rrggbb`]としても指定できるようにすれば、設定できる色のバリエーションを格段に増やすことができる。
APIは`PUT /RGB/#rrggbb`（rgbの各色を00〜ffの16進数で指定）。ところが、`#`以降は~~サーバ処理で切り捨てられているようで、routeハンドラに渡って来ない~~クライアントのfetch内で切り捨てられているようで、サーバに渡って来ない。仕方ないので、APIを`PUT /RGB/_rrggbb`とした。

> `#`はフラグメント（アンカー）を意味するため特別な扱いがされているのかも？


```python:ハンドラ追加処理
    elif colour[0] == '_' and len(colour) == 7:
        hex_value = int(colour[1:], 16)
        b = hex_value & 0xff; hex_value >>= 8
        g = hex_value & 0xff; hex_value >>= 8
        r = hex_value & 0xff
        pixels[0] = (r, g, b)
```

index.htmlにもカラーピッカーを表示して、onchangeイベントでAPIを呼ぶように追加。


```html:追加部分のみ
<div class="mb-3">
Any Color<input type="color" class="form-control form-control-color w-100" name="color" id="colorPicker" title="choose color" onchange="callAPI('/RGB/_' + document.getElementById('colorPicker').value.slice(-6));">
</div>
```

グリッド上で指を滑らせるとリアルタイムに色が変化する。しかし、スペクトラム上の動きではさすがにイベント発生量が多く、色更新に遅れが生じる。一回のAPIで100msほど掛かっているので、致し方無い。


![IMG_0019+.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/a1bbdfac-e920-cd6b-cffa-daa95aefe0fb.png)

今回の改造で、こんなことも簡単に実装できるようになった。



# おわりに

今回の改造でESP32のWebサーバの応用範囲をさらに広げることができたと確信している。

なお、今回の`_HTTPRoute`の改修を、Adafruitに正式にプルリクしてみようと思う。

<br>


# 追記（プルリクの結末）

上記の改修を[プルリク](https://github.com/adafruit/Adafruit_CircuitPython_HTTPServer/pull/42)しました。そのまま採用されるとこはありませんでしたが、これがきっかけで、もっと汎用的で高機能なroute機能が[プルリク](https://github.com/adafruit/Adafruit_CircuitPython_HTTPServer/pull/43)されマージされました。
少しは役に立てたと思います。

<br>



**以上**