AIによるキーボード&マウス自動操作:ESP32-S3を自作USB HIDにしてマインスイーパーを解かせる

ESP32-S3の開発ボードを、USBキーボード&マウスとして動かしました。さらに、AI(Claude Code)がこのボードに命令を送ってRaspberry Pi 5を操作し、画面を見ながらマインスイーパーを解くところまで試しました。
結果を先にまとめます。ESP32-S3のUSB(OTG)機能を使うと、PCからは普通のUSBキーボードとマウスに見える機器を作れました。操作される側のRaspberry Pi 5には、何もインストールしていません。AIはSSHで画面を撮り、盤面を読み、絶対座標でマスをクリックして、マインスイーパー(9×9、地雷10個)を1分19秒、ノーミスでクリアしました。狙ったマスを正確にクリックするには、普通の「相対移動」のマウスではなく、画面の座標を直接指定する「絶対座標マウス」を自作する必要がありました。
作業日:2026年10月2日(日本時間)。
やったこと
- ボタンで決まった動作をする:基板のBOOTボタンを押すと、文字を入力し、マウスを動かす
- AIから操作する:Claude CodeがシリアルでESP32に命令を送り、キーボードとマウスを操作する
- 画面を見ながら操作する:SSHと
grimで相手の画面を撮り、AIが状況を見て次の手を決める。最後にマインスイーパーを解かせる
使ったもの
| 項目 | 内容 |
|---|---|
| ボード | Freenove ESP32-S3 開発ボードキット(フラッシュ 8MB、PSRAM 8MB。ESP32-S3-WROOM-1 N8R8 相当と推定) |
| 開発PC | Windows 11、VS Code、PlatformIO 拡張 |
| フレームワーク | Arduino(PlatformIO の espressif32 プラットフォーム 7.1.3) |
| 操作される側 | Raspberry Pi 5(Raspberry Pi OS Bookworm、Wayland のデスクトップ) |
| AI | Claude Code(VS Code 拡張) |
このボードにはUSB-Cのポートが2つあります。今回の構成は、この2つの役割を分けたことがポイントです。
| ポート | 中身 | 用途 |
|---|---|---|
| UART | USB-シリアル変換チップ(WCH CH343) | プログラムの書き込みと、AIからの命令の受け取り。Windows に接続(COM5) |
| USB | ESP32-S3 本体の USB(OTG) | キーボード&マウスとして Raspberry Pi 5 に接続 |
全体の構成
Claude Code(Windows)
│ hid_send.py が "CLICK 960 566" のような1行の命令を送る
▼
[UART ポート / COM5]
▼
ESP32-S3 … 命令を解釈して、USB HID のレポートを作る
▼
[USB ポート(OTG)]
▼
Raspberry Pi 5 … 「キーボードとマウスが操作された」と受け取る
│
└ SSH + grim で画面を撮り、Claude Code が画像を見て次の手を決める
ESP32自体にAIの機能は入っていません。UARTから届いた文字列の命令を、キーボードやマウスの操作に変えるだけです。考えるのはPC側のAIです。
セットアップ
ボードの中身を確かめる
届いたボードが本当にESP32-S3なのかも分からなかったので、まず esptool で調べました。
esptool --port COM5 chip-id
esptool --port COM5 flash-id
esptool --port COM5 get-security-info
- チップは ESP32-S3(QFN56)リビジョン v0.2、デュアルコア 240MHz、PSRAM 8MB
- フラッシュは 8MB
- Secure Boot と Flash Encryption は無効で、自由に書き込める状態
- 出荷時には、Arduino で作られたテスト用のプログラムが入っていた
USB を OTG モードにする
ESP32-S3のUSBは、標準ではデバッグ用のシリアル(USB-Serial/JTAG)として動きます。キーボードやマウスとして使うには、PlatformIOの設定でTinyUSB(OTG)モードに切り替えます。
[env:freenove_esp32_s3_wroom]
platform = espressif32 @ ^7.1.3
board = freenove_esp32_s3_wroom
framework = arduino
build_unflags =
-DARDUINO_USB_MODE=1
-DARDUINO_USB_CDC_ON_BOOT=1
build_flags =
-DARDUINO_USB_MODE=0 ; 本体の USB を TinyUSB (OTG) で使う
-DARDUINO_USB_CDC_ON_BOOT=0 ; Serial は UART 側 (CH343) に出す
キーボードには、Arduino標準の USBHIDKeyboard を使いました。
ボタンで決まった動作をする
最初の動作確認として、BOOTボタン(GPIO0)を押すと、Hello from ESP32 S3 と入力してEnterを押し、マウスカーソルで100ピクセル四方の四角を描くようにしました。
USBを挿しただけでは何も入力しないようにしています。また、テストはメインのWindows PCではなく、Raspberry Pi 5で行いました。間違ってメインのPCに入力が飛ぶと困るからです。
AI から操作できるようにする
ESP32が、UARTから1行に1つの命令を受け取って実行するようにしました。主な命令は次のとおりです。座標は画面のピクセル(左上が0, 0)で指定します。
| 命令 | 例 | 動作 |
|---|---|---|
TYPE <文字列> | TYPE hello | 文字を入力する |
KEY <キー> | KEY ctrl+s | キーを押す。同時押しもできる |
CLICK [ボタン] [x y] | CLICK right 1020 606 | 指定した座標をクリックする |
ABS <x> <y> | ABS 300 200 | マウスを座標へ移動する |
DRAG x1 y1 x2 y2 | DRAG 100 100 400 300 | ドラッグする |
SCROLL <n> | SCROLL -3 | ホイールを回す |
PC側は、短いPythonスクリプト(hid_send.py)で命令を送ります。ESP32は1命令ごとに OK か ERR ... を返すので、AIは失敗にすぐ気づけます。
python hid_send.py "TYPE Hello from Claude Code" "KEY enter" "CLICK 960 566"
相手の画面を見る(SSH + grim)
画面を見ずに操作するだけでは、決まった手順を流すことしかできません。そこで、Raspberry Piのデスクトップ画面をSSH経由で撮るようにしました。
ssh pi 'XDG_RUNTIME_DIR=/run/user/1000 WAYLAND_DISPLAY=wayland-1 grim /tmp/screen.png'
scp pi:/tmp/screen.png .
- Raspberry Pi OS Bookworm のデスクトップは Wayland なので、昔ながらの
scrotではなくgrimを使う - SSH のセッションにはデスクトップの環境変数がないので、
XDG_RUNTIME_DIRとWAYLAND_DISPLAYを指定する grim -g "x,y 幅x高さ"で、画面の一部だけを切り出すこともできる
なお、画面を撮るのにSSHを使っているので、「相手に何もインストールしていない」のは操作の部分だけです。
結果:AI がマインスイーパーをクリア
Raspberry Piに sgt-puzzles(Simon Tatham のパズル集)を入れ、Mines(9×9、地雷10個)を起動しました。

Claude Codeは、次のループを繰り返しました。
- grim で盤面を撮る
- 画像から盤面を読み取り、数字から地雷のマスと安全なマスを推理する
- 地雷には右クリックで旗を立て、安全なマスは左クリックで開ける
sgt-puzzlesは最初の1手で地雷を踏まない作りです。その1手目を除いて、勘で開けたマスはありませんでした。結果は1分19秒でクリア(COMPLETED!)です。
![地雷10個すべてに旗が立ち、下に [1:19] COMPLETED! と表示されたマインスイーパーの盤面](https://www.ban.com/wp20200526/wp-content/uploads/2026/10/esp32-s3-hid-mines-completed.png)
つまずいた点
相対移動のマウスでは、狙った場所をクリックできない
今回いちばん大きな問題でした。普通のUSBマウスは「右に10動いた」という相対移動を送ります。OSのマウス加速がかかるので、同じ量を送っても実際の移動量が変わり、マインスイーパーのマス(約20ピクセル)を正確に狙えませんでした。
そこで、画面の座標を直接指定する絶対座標マウスを自作しました。仮想マシンのUSBタブレット(QEMU の usb-tablet)と同じ方式で、XとYを0〜32767の範囲で送ると、OSが画面全体に割り当ててくれます。HIDレポートディスクリプタの要点は次のとおりです。
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x30, 0x09, 0x31, // Usage (X), Usage (Y)
0x16, 0x00, 0x00, // Logical Minimum (0)
0x26, 0xFF, 0x7F, // Logical Maximum (32767)
0x75, 0x10, 0x95, 0x02, // 16bit × 2
0x81, 0x02, // Input (Data, Var, Abs) ← Abs が絶対座標
ESP32側では、ピクセル座標をこの0〜32767の値に変換して送ります。画面の解像度(既定は1920×1080)は命令で変えられるようにしました。ABS 300 200 を送ると、カーソルの先端がぴったり (300, 200) に来ました。

相対マウスと絶対マウスを同じUSB機器に同居させると、Linux側で1台のマウスにまとめられて誤動作するおそれがあります。そのため、相対マウスは外して絶対マウスだけにしました。
シリアルポートを開くと ESP32 がリセットされる
このボードは、DTRとRTSの信号線でESP32を自動リセットする回路を持っています。何もしないと、命令を送ろうとポートを開くたびにESP32が再起動し、Raspberry Piから見るとUSBが抜き挿しされたことになります。ポートを開く前にDTRとRTSを下げておくことで回避しました。
s = serial.Serial()
s.port = "COM5"
s.baudrate = 115200
s.dtr = False
s.rts = False
s.open()
PlatformIO にボードの定義がない
pio boards freenove の一覧には freenove_esp32_s3_wroom が出てくるのに、ビルドすると Unknown board ID になりました。一覧はレジストリ上の最新版を見ていて、インストール済みのプラットフォーム(6.5.0)は古かったのが原因です。プロジェクトの設定で platform = espressif32 @ ^7.1.3 と指定して解決しました。
esptool が intelhex がないと言って止まる
新しいプラットフォームに付いてくる esptool v4.11 は、Pythonのモジュール intelhex を必要とします。PlatformIOのPython環境(penv)にpipで入れると一度は直りますが、VS CodeのPlatformIO拡張がpenvを作り直したときに消えてしまいました。最終的に、esptoolを依存の少ない v4.5.1 に固定して解決しました。
platform_packages =
platformio/tool-esptoolpy @ ~1.40501.0
できないこと・注意点
- 文字入力は US 配列が前提です。
USBHIDKeyboardは US 配列のキーコードを送るので、相手の OS が日本語キーボードの設定だと記号がずれます。日本語も直接は入力できません - 画面を見るには、相手に SSH で入れることが前提です。SSH のない機械を操作するには、HDMI キャプチャなど別の手段が必要です
- マインスイーパーは 9×9・地雷10個の1回だけです。もっと大きな盤面や、勘に頼るしかない局面での成績は試していません
- これは「何でも打ち込めるキーボード」なので、間違った操作もそのまま実行されます。動作確認は、何が起きても困らない機械で行うのが安心です
次に試したいこと
- Untangle(点をドラッグして線の交差をほどくパズル)で、ドラッグ操作を試す
- HDMI キャプチャで画面を取り込み、相手に SSH がなくても画面を見られるようにする
- Wi-Fi で命令を受け取り、ケーブルなしで操作する
- 日本語入力(IME の切り替えとローマ字入力の組み合わせ)
参照資料
Arduino-ESP32 USB API / PlatformIO Espressif 32 / esptool ドキュメント / USB HID 仕様(USB-IF) / Simon Tatham’s Portable Puzzle Collection
