M5StackC3 と IR Unit で、HTTPで赤外線を送受信できる自宅 Web API を作る

tl;dr

  • CircuitPython と ESP32 を使うと簡単に、HTTP Server を立てることができる。
  • CircuitPython の pulseio を使うと、制御が抽象化されて、簡単に信号制御ができる。
  • M5StampC3 と、IR Unit ならケースに入っているし、そのままお家にデプロイしても良さそう。
  • CircuitPython 上でアプリケーションを作り込むより、Raspberry Piなどの上でアプリケーションを作って、HTTP経由でMCUに制御させた方が、アプリケーションの作り込みがやりやすそう。

作ったもの 赤外線送受信 HTTP Server

自宅の WiFi に繋いで、HTTP経由で赤外線を送受信できる Web Server を M5StampC3 と IR Unit で作成しました。

POST /api/send_ir で以下のような JSON を送ります(ちなみに、わが家の SONY の電源ボタン)。

{
  "pulse": [
    2403, 642, 1170, 645, 588, 621, 1167, 648, 588, 621, 1191, 624, 588, 621,
    588, 621, 1167, 648, 588, 621, 588, 624, 585, 624, 588
  ]
}

すると、赤外線信号が送信されます。

また、POST /api/receive_ir を叩いて、赤外線ユニットに向けてリモコンノボタンを押すと以下のようにレスポンスします。

{
  "success": true,
  "data": {
    "pulses": [
      2403, 615, 594, 618, 1194, 621, 588, 621, 591, 621, 588, 621, 588, 624,
      612, 618, 1170, 645, 567, 645, 564, 645, 564, 645, 567, 27126, 2400, 621,
      588, 642, 1170, 645, 564, 645, 567, 645, 588, 621, 588, 621, 588, 624,
      1191, 621, 591, 621, 588, 621, 588, 621, 588, 27105, 2397, 645, 564, 645,
      1170, 645, 588, 621, 564, 645, 567, 645, 564, 645, 588, 621, 1170, 645,
      588, 621, 567, 642, 591, 621, 588, 27105, 2421, 621, 567, 642, 1170, 645,
      564, 645, 564, 645, 567, 645, 588, 621, 564, 645, 1194, 621, 588, 621,
      591, 618, 591, 621, 588
    ]
  }
}

これで、パソコンからでもHTTP操作で、リモコンの操作ができるようになりました。

M5StampC3 に CircuitPython をインストールする

今回の工作には、お安めの M5StampC3 (¥1,188)を使ってみることにしました。

M5Stamp C3 Matewww.switch-science.com

そしてファームウェアのランタイムには CircuitPython を選びました。これは、単にのちほど M5StampS3 を使って赤外線機能を持ったTV横USBキーボードデバイスを作ろうとしていて、同じCircuitPython上で実装したコードが使えることを狙ったためです。CircuitPythonのファームウェアは以下からダウンロードできます。

circuitpython.org

インストール方法は下記にあります。私は別途 ESP32 の開発環境が合ったため、 esptool.py を使いましたが、ここにはWebブラウザ経由でインストールする方法も書かれています。

learn.adafruit.com

CircuitPython はインストールすると、通常は USBメモリとして認識するドライブに、ファイルをコピーすることで使うことができます。M5StampC3 で使われている ESP32-C3 にはUSBデバイス機能はありません。では、どうやってファイルを送るかというと、M5StampC3 をUSBで繋ぐと出てくるシリアルコンソール上で、WiFi の設定を入力してM5StampC3 をWiFi につなぎ、すると WebServer が自動で立ち上がるようになるので、HTTP経由で送ります。コンソールに以下のようなPythonのコマンドを打ち込んで、設定ファイルを作ります。

learn.adafruit.com

f = open('settings.toml', 'w')
f.write('CIRCUITPY_WIFI_SSID = "wifissid"\n')
f.write('CIRCUITPY_WIFI_PASSWORD = "wifipassword"\n')
f.write('CIRCUITPY_WEB_API_PASSWORD = "webpassword"\n')
f.close()

そしてUSBの再接続すると、IP アドレスが書かれているので、そのIPにブラウザでアクセスすることができます。

私の場合は、固定IPにしたかったので、お家のWiFiルータの設定でDHCPでこのIPを割り当てた MACアドレスに固定IPを割り振るようにしました。

これで、curl でファイルを転送することができるようになりました。

curl -v -u :webpassword -T main.py -L --location-trusted http://192.168.1.19/fs/code.py

CircuitPython で WebServer を立てる

WiFi の設定をしただけで WebServer が立っている訳ですが、adafruit_httpserver を使うと、Flask と似た感じに API を実装することができます。

import wifi
import json
import socketpool
from adafruit_httpserver import Server, Request, Response

pool = socketpool.SocketPool(wifi.radio)
server = Server(pool, "/static", debug=True)

@server.route("/api/send_ir", methods="POST")
def send_ir_api(request: Request):
    print("access /api/send_ir")
    print(request.body)
    data = json.loads(request.body)
    pulse = data["pulse"]

    print(pulse)
    send_ir(pulse)

    res = {"success": True}
    return Response(request, json.dumps(res))

server.serve_forever(str(wifi.radio.ipv4_address))

adafruit_httpserver は、Adafruit CircuitPython Library Bundle に含まれています。以下のリポジトリからダウンロードし、libs/adafruit_httpserver ディレクトリを、CircuitPythonのlib ディレクトリに転送して、使えるようにしておきます。

github.com

M5Stack IR Unit を接続する

M5StampC3 には Grove ポートが付いており、Grove ポートで使えるようになる 赤外線モジュールがM5Stackから出ています。

M5Stack用赤外線送受信ユニット [U002]www.switch-science.com

お値段も ¥649 とお手頃です。M5Stack のモジュールはケースに入っているので、そのままお家にデプロイしても良さそうです。

CircuitPython の pulseio を使って、赤外線の信号を送受信する

CircuitPython にはこのような信号をやりとりする仕組みとして、pulseio があります。その裏は、MCUごとの機能を使って実装されているようです。

docs.circuitpython.org

まず受信の関数は以下のようにしました。len(pulsein) で受信したかどうかわかり、pulsein.popleft() で値を取ります。これを受信できるまでループさせるようにします。

import board
import pulseio
import time

pulsein = pulseio.PulseIn(board.G0, maxlen=120, idle_state=True)
pulsein.pause()

def receive_ir() -> list[int]:
    timeout = 10

    pulses = []

    start = time.monotonic()
    is_received = False
    pulsein.clear()
    pulsein.resume()
    while not is_received and (time.monotonic() - start) < timeout:
        # 値が取れるか、タイムアウトまでループ
        time.sleep(0.1)
        while pulsein:
            pulse = pulsein.popleft()
            pulses.append(pulse)
            is_received = True
    pulsein.pause()

    if pulses is None:
        print("cannot receive pulses")
    else:
        print("Heard", len(pulses), "Pulses:", pulses)

    return pulses

なお、pulseio.PulseIn に関しては、一度 pulseio.PulseIn() でインスタンスを作り deinit() でインスタンスを解除しても、再度 pulseio.PulseIn() でインスタンスを作ったとしても、使えなくなる問題が発生しました。これは最初の一度だけ初期化するようにします。また、一度初期化してしまうと CircuitPython のリセットを行っても再度使える状態には鳴りませんでした。その時は仕方なく、MCU レベルでリセットするようにしました。

import microcontroller; microcontroller.reset()

次に送信は以下のようにします。こちらは単純です。

def send_ir(pulse: list[int]):
    with pulseio.PulseOut(IR_SEND_PIN_NO) as pulseout:
        print("send")
        for _ in range(3):
            pulseout.send(array.array("H", data))
            time.sleep(0.025)
        print("send done")

こんな感じに配列を渡すだけで送信ができます。

この 2つの関数を WebServer から呼べるように実装しました。

これで赤外線の IO となる WebServer ができた

ほんの100行に満たないコードで、赤外線リモコンの IO となる Web API を構築することができました。

ここから更にやりたいことは以下のようなことです。

  • TVの電源オン、チェンネル1に変更、入力切り替えでTV横PCのHDMI に、1ボタンで切り替える
  • PS5 の起動後のAmazon Prime アプリを起動するまでの、シーケンスを、TVリモコン越しに行う

これらには複数のボタンを、時間間隔と一緒に制御する必要があります。

ですが、M5StampC3 で宅内 Web API になったことで、既にある Raspberry Pi 4 上に操作盤となる Webアプリを作り込めば、CircuitPython の小さな世界ではなく、Pythonなどの機能を存分に使って実装することができます。これに気づいたことから、マイコンでは、センサとの IO だけやるようにしようと思うようになりました。

今回作成したコードは以下のリポジトリで公開しています。

github.com

Appendix: CircutPython のプロジェクトで Python のコード補完を効かせる

VS Code で実装していると、コード補完機能には助けられます。CircuitPython用のコード補完用のモジュールは以下で公開されているため、これをインストールします。しかし、これを使っても完全に補完できる状態にはならないようです。

pypi.org

また、adafruit_httpserver のようなモジュールでは、libディレクトリに配置するのは mpy というバイナリ変換ファイルになります。バイナリファイルでは補完が効かないため、プロジェクトディレクトリにgit submodule で変換前のファイルを取得して参照するようにしました。

git submodule add https://github.com/adafruit/Adafruit_CircuitPython_HTTPServer.git ./imports/Adafruit_Circui
tPython_HTTPServer
ln -s imports/Adafruit_CircuitPython_HTTPServer/adafruit_httpserver ./