Raspberry Pi PicoとWIZnetのEthernetモジュールでWebサーバーを立てて謎APIを作って遊ぼう

こんにちは、あっきぃです。この記事はRaspberry Pi Advent Calendar 2023の15日目の記事です。

WIZnetさんからサンプルをいただいたので、今回はそちらを使って少し遊んでみます。レビューのほうは別の方がする予定となっているため、私からはとにかくなんか遊ぶ感じの話題をお届けします。ちなみにいただいたのは以下の3種類(下のHATは2つ)。WIZnetさん、ありがとうございます!

今回遊ぶ内容的にはどれを使用しても同じですが、今回はEthernet HATの方を使用しました。こちらの場合別途Raspberry Pi Picoが必要になりますが、EVB-Picoよりも全長が少し短くなってコンパクトになります。また、EVB-Picoと比べるとボード上の通電LEDがPicoで覆い隠せるため、実運用に投入するとLEDが眩しくて邪魔という問題が緩和できます。まあ、EVB-PicoのEVBはEValuation Boardの略だと思うので、実運用に適しているのかは不明ですが……。

なお、WIZnet製品は、スイッチサイエンスで購入できます。

https://www.switch-science.com/collections/all/cat:%E3%83%A1%E3%83%BC%E3%82%AB%E3%83%BC%2F%E3%83%96%E3%83%A9%E3%83%B3%E3%83%89_WIZnet

今回はCircuitPythonを使用して、Raspberry Pi Picoが接続されているPCのマウスカーソル操作をWeb API化して、リモート操作できるようにしてみます。

CircuitPythonを使う時、Macユーザーは注意

Macユーザー向けにナウな注意として、現在のmacOS Sonoma(14.2時点で問題は継続中)では、macOSの不具合によって、CircuitPythonを接続してすぐにスクリプトの読み書きをすると、I/Oエラーが発生してデータが破損するなどのおそれがあります。以下のページにあるシェルスクリプトを使用して再マウントすることで問題を回避できます。

https://learn.adafruit.com/welcome-to-circuitpython/troubleshooting#macos-sonoma-14-dot-x-disk-errors-writing-to-circuitpy-3160304

ネットワークに接続する

WIZnetのデバイスを扱うには、adafruit_wiznet5kモジュールを使用します。CircuitPythonのライブラリバンドルに収録されているので、ここからadafruit_winzet5kモジュールをデバイス上のlibディレクトリにコピーして使用します。

WIZnetのデバイスを使ってDHCPでアドレスを取得するまでのコードは、以下のようになります。このコードをデバイス上の/code.pyとして保存します。

import board
import busio
import digitalio
import time
from adafruit_wiznet5k.adafruit_wiznet5k import WIZNET5K

# SPIの初期化
SPI_SCK = board.GP18
SPI_TX = board.GP19
SPI_RX = board.GP16
SPI_CSn = board.GP17
W5500_RSTn = board.GP20
cs = digitalio.DigitalInOut(SPI_CSn)
spi_bus = busio.SPI(SPI_SCK, MOSI=SPI_TX, MISO=SPI_RX)

# デバイスをリセットする
ethernetRst = digitalio.DigitalInOut(W5500_RSTn)
ethernetRst.direction = digitalio.Direction.OUTPUT
ethernetRst.value = False
time.sleep(1)
ethernetRst.value = True

# 接続する
eth = WIZNET5K(spi_bus, cs, is_dhcp=True)
print("MAC Address:", [hex(i) for i in eth.mac_address])
print("My IP address is:", eth.pretty_ip(eth.ip_address))

実行結果は以下の通り。デバイスはMACアドレスは持っていないため、”dead beef feed”になっています。MACアドレスを指定することは可能なため、複数動かすなどの場合には適宜変更すると良さそうです。

code.py output:
MAC Address: ['0xde', '0xad', '0xbe', '0xef', '0xfe', '0xed']
My IP address is: 192.168.29.127

Code done running.

Webサーバーにする

CircuitPythonでWebサーバーを作成するにはadafruit_httpserverモジュールを使うのが簡単です。ただ、WIZnetのボード向けのCircuitPythonにはhashlibモジュールが内蔵されていないため、現在のモジュールのバージョンでは、インポート時にエラーが発生します。そのため、ライブラリバンドルからではなく、Githubリポジトリから最新のコードを取得して、adafruit_httpserver/response.pyにあるimport hashlibの行をコメントアウトする必要があります。この問題は報告をして、現在やり取りをしているところです。 (追記)修正されたようです。リリースもされたため最新のバージョンが使用可能です。

https://github.com/adafruit/Adafruit_CircuitPython_HTTPServer/issues/73

ライブラリの方に一手間が発生してしまいましたが、気を取り直して、上記のIPアドレスを取得するコードに、次のコードを書き加えると、Webサーバーが起動するようになります。

import adafruit_wiznet5k.adafruit_wiznet5k_socket as socket
from adafruit_httpserver import Server, Request, Response

socket.set_interface(eth)
server = Server(socket)

@server.route("/")
def root(request: Request):
    print("GET /")
    return Response(request, "Hello!")

server.serve_forever(str(eth.pretty_ip(eth.ip_address)))

実行時に表示されたIPアドレスにアクセスすると、Hello!の文字が表示されました。

マウスを操作する

個人的にCircuitPythonを気に入っている理由の一つに、USB HIDデバイスとして使えるという点が上げられます。MicroPythonにはないので、私がCircuitPython推しなのはこの一点が大きいです。

USB HIDとして使えるようにするには、adafruit_hidモジュールを使用します。code.pyにコードを書く……前に、以下の内容を記述したboot.pyを用意する必要があります。

import usb_hid

usb_hid.enable((usb_hid.Device.MOUSE,))

Webサーバーが動くようになったcode.pyは一旦リネームして退避しておき、あたらしいcode.pyには以下のコードを用意します。

import time
import usb_hid
from adafruit_hid.mouse import Mouse

mouse = 0
while not mouse:
    try:
        mouse = Mouse(usb_hid.devices)
    except:
        pass
    time.sleep(1)

for i in range(0, 8):
    mouse.move(x=20, y=20)
    time.sleep(0.25)

boot.pyの反映はソフトリセットではできないため、code.pyの保存までできたら、ケーブルを抜き差ししてハードリセットします。(macOS Sonomaユーザーは上に書いた再マウントのWorkaroundを忘れずに!)。

抜き差し直後に、マウスカーソルをよく見ると、右下に少しだけカクカクと移動していきます。hidモジュールはクリックなどの操作ももちろんできるので、マクロ的な操作も作ることができます。

マウス移動をWeb APIにする

WIZnetのデバイス初期化、Webサーバー起動、マウス操作を組み合わせて、マウスカーソル移動をWeb APIにしてみます。

マウス操作のテストコードのうち、import timeとfor文以外をコピーして、退避したWebサーバーのコードをcode.pyにリネームし直し、コピーしたコードをcode.pyの最初の方にでも貼りつけておきます。

/にアクセスしたときは、マウスカーソルを操作するボタンを配置したHTMLを返すように変更します。また、マウスカーソルを上下左右に20px移動するAPIとして、/mouse/up・/mouse/down・/mouse/left・/mouse/rightを作成します。

Hello!を返すためのコードのかたまり(4行)を消して、以下のコードを追加します。

indexhtml = """<!DOCTYPE html><html><head><meta name="viewport" content="width=device-width,initial-scale=1"><meta charset="utf-8"><title>Web mouse cursor API</title><link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous"><script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script><script>function s(p){fetch(p).then(function(r){return r.text()}).then(function(t){document.getElementById('r').innerText=t;});}</script></head><body><main class="px-3 col-6 mx-auto text-center"><h1 class="mt-2">Web mouse cursor API</h1><div class="container"><div class="row"><button class="btn btn-primary mt-3 mb-3" onclick="s('/mouse/up');">👆</button></div><div class="row d-flex justify-content-between"><button class="btn btn-primary col-5 mt-3 mb-3" onclick="s('/mouse/left');">👈</button><button class="btn btn-primary col-5 mt-3 mb-3" onclick="s('/mouse/right');">👉</button></div><div class="row"><button class="btn btn-primary col-12 mt-3 mb-3" onclick="s('/mouse/down');">👇</button></div></div><div id="r" class="m-3"></div></main></body></html>"""

@server.route("/")
def root(request: Request):
    print("GET /")
    return Response(request, indexhtml, content_type="text/html")

@server.route("/mouse/<direction>")
def move_mouse(request: Request, direction: str):
    print("GET /mouse/%s" % direction)
    if direction == "up":
        mouse.move(x=0, y=-20)
    elif direction == "down":
        mouse.move(x=0, y=20)
    elif direction == "left":
        mouse.move(x=-20, y=0)
    elif direction == "right":
        mouse.move(x=20, y=0)
    else:
        return Response(request, "invalid parameter")
    return Response(request, "moved mouse cursor to %s side" % direction)

マウスの操作APIについては@server.route("/mouse/<direction>")のようにパラメータを変数に取ることができるので、これを活用しました。あとはパラメータに応じてマウスカーソルを移動させて、メッセージを返します。

完成

Web画面を開くと、以下のような画面が表示され、ボタンをクリックするとマウスカーソルが移動します。

スマートフォンなど、デバイスを接続していない別の端末からアクセスして操作するとちょっと楽しいと思います(楽しさの感じ方は人によります!)。

なお、同じネットワーク内にいる他人にアドレスがバレると、いたずらされる可能性があるので、程々に遊んだら片付けるのが安全でしょう。ちなみに私はインターネットに公開してURLをSNSで共有し、10分くらいマウス操作を開放して遊びました。操作している人たちは私のPCのカーソルがどうなっているか見えてもいないのに、なぜかとても楽しそうだったので何よりです。

まとめ

WIZnetのEthernet拡張を使用してWebサーバーを立ち上げて、マウスカーソル移動をWeb API化する例を紹介しました。

Raspberry Pi Picoでネットワーク接続と言えば、現在ではPico Wがあるため、無線LANでいいという考え方もできます。ただ、無線LANが不安定な環境だったり、そもそも無線LANがない環境で使いたい場合などには、やはり有線LANがあると嬉しいですよね。

WIZnetのEthernetモジュールは、ライブラリが揃っていて扱いやすい印象です。今回したCircuitPython以外に、MicroPythonでも使用可能です。難点は、MACアドレスを持っていないので自分で定義が必要な点と、EVB-Picoボードでは通電LEDが目障りになりがちな点でしょうか……。通電LEDは、いざとなればテープや引っ付き虫などで隠してしまっても良さそうです。

今回作成したコードは、諸々整えた状態で以下に公開してあります。

https://github.com/Akkiesoft/akkiesoft-pico/tree/main/CircuitPython/web_mouse

実は、ほぼ同じノリでマクロキーボードAPIというものを以前作っているのですが、こちらで使用していたadafruit_wsgiモジュールでは通信速度が非常に遅く、今回adafruit_httpserverモジュールで書いてみたら高速で転送できるようになったので、マクロキーボードAPIもそのうち書き直したいですね。