はじめに
今回は、前回の記事で構築した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メソッド〕のペアを比較して完全一致するハンドラを呼び出すことを行う。
完全一致するハンドラが定義されていない場合、サーバは、
-
GETメソッドでパスが
/
の場合は、/index.html
をファイルシステムから入力してレスポンスを返す。 -
GETメソッドでパスが
/
以外の場合は、パス全体をファイルパスとして、ファイルシステムから入力してレスポンスを返す。これで、CSSファイルやJSファイル、imageファイルなどの読み込みを実現している。
もしファイルが存在しない場合は、Status404
(Not Found)のレスポンスを返す。 -
それ以外の場合は、Status
400
(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からソースコードを入手し、数行手を加えるだけ。
# 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から先に示す。
<!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> </p>
</div>
</body>
</html>
色設定のAPIをfetch
で呼び出している。リターン情報として、画面下部に表示するESP32のコア温度と現在時刻をjsonで受け取り、HTML(DOM)の該当テキストを直接更新する。
もし、パラメタを有するAPIならば、ボディにJSONを設定することもできる。
APIサーバのコードを次に示す。
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ほど掛かっているので、致し方無い。
今回の改造で、こんなことも簡単に実装できるようになった。
おわりに
今回の改造でESP32のWebサーバの応用範囲をさらに広げることができたと確信している。
なお、今回の_HTTPRoute
の改修を、Adafruitに正式にプルリクしてみようと思う。
追記(プルリクの結末)
上記の改修をプルリクしました。そのまま採用されるとこはありませんでしたが、これがきっかけで、もっと汎用的で高機能なroute機能がプルリクされマージされました。
少しは役に立てたと思います。
以上