LoginSignup
0
0

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-03-17

はじめに

今回は、前回の記事で構築したWebサーバを改造して、RESTなAPIサーバに仕立てることを行う。

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

adafruit_httpserver.server.HTTPServerのルート(アクセスパス)切り分けの方法

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

(コード抜粋)

#(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ファイルなどの読み込みを実現している。
    もしファイルが存在しない場合は、Status404(Not Found)のレスポンスを返す。
  3. それ以外の場合は、Status400(Bad Request)のレスポンスを返す。

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

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

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

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

#(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からソースコードを入手し、数行手を加えるだけ。

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)})"

実装例

前回の記事の実装例では、colorの指定はPOSTのパラメタで実装していたが、今回のRESTなAPIサーバでは、PUT /RGB/colorを非同期通信で呼び出す。

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

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サーバのコードを次に示す。

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とした。

#はフラグメント(アンカー)を意味するため特別な扱いがされているのかも?

ハンドラ追加処理
    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を呼ぶように追加。

追加部分のみ
<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

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

おわりに

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

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


追記(プルリクの結末)

上記の改修をプルリクしました。そのまま採用されるとこはありませんでしたが、これがきっかけで、もっと汎用的で高機能なroute機能がプルリクされマージされました。
少しは役に立てたと思います。


以上

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0