参考にしたもの
-
tnm/zclaw ESP32上で動く小型AIアシスタントです。C言語で実装されており、GPIO制御、スケジュール実行、永続メモリ、自然言語によるカスタムツール構成などを扱えるプロジェクトです。公式READMEでは、ESP32上で動く小さなAIパーソナルアシスタントとして説明されています。
-
espressif/esp-claw EspressifによるIoTデバイス向けのAIエージェントフレームワークです。会話によってデバイスの振る舞いを定義し、センシング、判断、実行のループをEspressifチップ上で扱えます。OpenClawの考え方に影響を受け、Cで再実装された軽量な仕組みとして説明されています。
-
今回使ったコード 通常版ESP32でGPIO22とGPIO23のLEDをOllama経由の自然言語で操作するために使った fork / branch です。
Contents
- はじめに
- 今回できたこと
- 実際のデモ
- 全体構成
- なぜZClawを使ったのか
- ZClawに追加・調整したこと
- ZClaw内部では何が起きているか
- Ollamaへ送っているリクエスト
- 引数はどこで作られるのか
- ビルドと書き込み
- OllamaをESP32から見えるようにする
- ESP32へWi-FiとOllama設定を入れる
- Wi-Fi状態の確認
- 自然言語でLEDを操作する
- ハマったところ
- まとめ
はじめに
ESP32を、ただのマイコンではなく、自然言語で指示を受け取り、GPIOなどのツールを実行する小さなエージェントとして使えないか試してみました。
実行したコードはこちらになります。 https://github.com/sekihan02/zclaw/tree/esp32-gpio22-23-ollama-led-demo
PCから自然言語で指示する
↓
ESP32がその指示を受け取る
↓
PC上のOllamaに問い合わせる
↓
Ollamaが「どのツールをどの引数で呼ぶか」を返す
↓
ESP32がGPIOを操作する
↓
LEDが点く
これをできるだけ無料で試したかったので、LLM部分にはクラウドAPIではなく ローカルOllama を使うことにしました。
今回試したのは、ESP32通常版、いわゆるESP32-WROOM-32系のボードです。 ZClawがこれに対応していたのでベースにしました。 実施内容はGPIO22とGPIO23につないだLEDを自然言語でON/OFFできるところまで確認しました。
今回できたこと
ただのLチカです。
「GPIO23を点けて、GPIO22を消して」
とPCから入力すると、ESP32上のZClawがPC上のOllamaへ問い合わせ、Ollamaが返したtool callに従って、ESP32がGPIOを操作します。
最終的には、次のような操作ができました。
GPIO23を点けて、GPIO22を消して
GPIO23を消して、GPIO22を点けて
GPIO23を点けて、GPIO22を点けて
GPIO23を消して、GPIO22を消して
2つのLEDの状態を読んで
また、LLMを通さない直接確認用として、次のようなコマンドも使えます。
/gpio all
/gpio 23 high
/gpio 22 low
この /gpio 直接コマンドがあることで、自然言語処理やOllama接続に問題があるときも、ESP32側のGPIO制御だけを切り分けて確認できます。
実際のデモ

動かすとこの動画のようにPCから自然言語で指示を送り、 その結果としてESP32につないだLEDが指示通りに点灯・消灯します。
全体構成
今回の構成は次のようになっています。
PC
├─ PowerShell
├─ zclaw_chat.py
└─ Ollama
↑
│ Wi-Fi / HTTP
│
ESP32
├─ ZClaw firmware
├─ GPIO tool
├─ gpio_write
├─ gpio_write_many
└─ GPIO22 / GPIO23 LED
入力の流れはこうです。
PCからCOM6経由で自然言語を送る
↓
ESP32上のZClawが受け取る
↓
ZClawがOllama用のリクエストを作る
↓
ESP32がWi-Fi経由でPC上のOllama APIへHTTPリクエストを送る
↓
Ollamaがtool callを返す
↓
ESP32上のZClawがtool callを解析する
↓
ZClawがGPIOツールを実行する
↓
LEDが点灯・消灯する
Ollamaの役割は、自然言語を読んで、
{
"tool": "gpio_write_many",
"arguments": {
"writes": [
{"pin": 23, "state": 1},
{"pin": 22, "state": 0}
]
}
}
のような「どのツールをどの引数で呼ぶか」を返すことです。
実際にGPIOを操作するのは、ESP32上で動いているZClawです。
なぜZClawを使ったのか
ESP32を自然言語で操作する仕組みとしては、ESP-Clawのようなプロジェクトもあります。ESP-Clawは、会話でデバイスの振る舞いを定義し、センシング、判断、実行までを扱うIoT向けAIエージェントフレームワークとして公開されています。
ただ、今回使いたかったのは通常版ESP32です。 そのため、通常版ESP32でも動かしやすく、GPIO制御やツール実行の仕組みがすでにあるZClawを使うことにしました。
ZClawは、ESP32上で動く小型AIアシスタントとして作られており、GPIO制御、スケジュール、永続メモリ、カスタムツール構成などをサポートしています。
ZClawをforkし、ESP32通常版でGPIO22/23を操作するための実験用ファームウェアとして調整しました。
公開用のforkにブランチを切り、今回使用したコードを格納しています。
https://github.com/sekihan02/zclaw/tree/esp32-gpio22-23-ollama-led-demo
ZClawに追加・調整したこと
今回の改造の中心は、次の3つです。
1. 使用するGPIOをGPIO22とGPIO23に限定する
2. GPIOをON/OFFするツールを使えるようにする
3. 複数GPIOをまとめて変更できる gpio_write_many を追加する
最初 3 はなく、自然言語で
GPIO23を点けて、GPIO22を消して
と入力すると、LLMが次のように2回に分けてツールを呼んでいました。
gpio_write(pin=23, state=1)
gpio_write(pin=22, state=0)
結果は動き、一回の指示で二回動作できるのかと分かりましたが、Ollamaの処理結果待ちが長いことと、ツールを増やしても意図通り動くかを見たかったため、
複数GPIOをまとめて変更するツールとして、gpio_write_many を追加しました。
イメージとしてはこうです。
{
"writes": [
{"pin": 23, "state": 1},
{"pin": 22, "state": 0}
]
}
これにより、LLMは1回のtool callで複数のGPIO状態を指定できます。
ZClaw内部では何が起きているか
今回のプロジェクトでは、ZClawの中に今回の処理を追加しています。
つまり、外部アプリからZClawライブラリを呼んでいるというより、ZClawというファームウェア自体を自分の用途向けに改造してESP32へ書き込んでいます。
ざっくり分けると、ZClaw内部はこうなっています。
ZClaw firmware
├─ channel.c
│ └─ COM6経由の入力を受け取る
│
├─ agent.c
│ └─ 入力を処理し、必要ならLLMへ送る
│
├─ llm.c
│ └─ OllamaへHTTPリクエストを送る
│
├─ tools.c
│ └─ tool callを実行する
│
├─ tools_gpio.c
│ └─ gpio_write / gpio_write_many / gpio_read_all
│
├─ builtin_tools.def
│ └─ LLMに見せるツール一覧
│
└─ gpio_policy.c
└─ 操作可能なGPIOを制限する
今回の変更は主に、GPIOツールとエージェントの終了処理まわりです。
Ollamaへ送っているリクエスト
ESP32がOllamaへ送るのは、
GPIO23を点けて、GPIO22を消して
という本文だけではありません。
実際には、次のような情報を含むリクエストを送っています。
system prompt
会話履歴
ユーザー入力
ツール一覧
各ツールの説明
各ツールの引数スキーマ
そのため、Ollama側のLLMは、
gpio_write_many というツールがある
writes という配列を渡す
各要素には pin と state が必要
複数GPIOを変えるなら gpio_write_many を使う
という情報を見た上で回答します。
今回のシステムプロンプトには、複数GPIOを変更する場合は gpio_write_many を使うように指示を入れました。
引数はどこで作られるのか
たとえば、PCから次のように入力します。
GPIO23を点けて、GPIO22を消して
このとき、引数を作るのはOllama側のLLMです。
LLMは、ツール定義を見て、
GPIO23を点ける = pin 23, state 1
GPIO22を消す = pin 22, state 0
と解釈し、次のようなtool callを返します。
{
"name": "gpio_write_many",
"arguments": {
"writes": [
{"pin": 23, "state": 1},
{"pin": 22, "state": 0}
]
}
}
ESP32上のZClawは、このtool callを受け取り、gpio_write_many のhandlerへ渡します。
ただし、LLMの出力をそのまま信用してGPIOを操作するのは危険です。 そのため、ZClaw側ではGPIOポリシーを使って、操作できるGPIOを制限しています。
今回の設定では、操作できるGPIOはGPIO22とGPIO23だけです。
ビルドと書き込み
ESP-IDFプロジェクトとしてビルドし、Windows側のPython/esptoolを使ってESP32へ書き込みました。
流れはこうです。
ZClawのCコード
↓
ESP-IDFでビルド
↓
build/zclaw.bin などが生成される
↓
esptool.pyでESP32へ書き込み
↓
ESP32がZClawファームウェアとして起動
PowerShellからWSL経由で実行する場合は、次のようにしました。
wsl bash -lc 'cd /mnt/c/Users/user/Desktop/esp32-zclaw && ./scripts/build.sh && ./scripts/flash_windows_com.sh COM6'
ビルドとprovisionは分けて考えています。
build / flash
→ ファームウェア本体を書き込む
provision
→ Wi-Fi SSID、Ollama URL、モデル名などの設定をNVSへ書き込む
コードを変更した場合は、provisionだけでは反映されません。 必ずbuildしてflashする必要があります。
OllamaをESP32から見えるようにする
OllamaはPC上で動いています。 ESP32からアクセスするために、Ollamaをlocalhost専用ではなくLANから見えるように起動しています。
PowerShellでは次のように起動しました。
$env:OLLAMA_HOST = "0.0.0.0:11434"
ollama serve
その後、PCのLAN IPを使って疎通確認します。
curl.exe http://192.168.0.6:11434/api/tags
ESP32からは、次のOpenAI互換APIエンドポイントへアクセスするように設定しました。
http://192.168.0.6:11434/v1/chat/completions
ESP32へWi-FiとOllama設定を入れる
Wi-FiやOllama接続先は、ESP32のNVS領域へprovisionします。
実行例は次のような形です。
./scripts/provision_windows_com.sh \
--port COM6 \
--ssid "<YOUR_WIFI_SSID>" \
--backend ollama \
--model "qwen3:4b" \
--api-url "http://<YOUR_PC_LAN_IP>:11434/v1/chat/completions"
パスワードをコマンド履歴に残したくないため、--pass を書かずに実行しています。
その場合、実行中にWi-Fiパスワードを入力します。
Wi-Fi状態の確認
ESP32がWi-Fiへ接続できているかは、次のコマンドで確認しました。
cmd.exe /C "C:\Users\user\.platformio\penv\Scripts\python.exe scripts\zclaw_chat.py --timeout 60 --raw COM6 /wifi status"
成功時のログは次のような形です。
WiFi status: provisioned=yes safe_mode=no driver=started link=connected ssid=<YOUR_WIFI_SSID> ip=192.168.0.14 rssi=-43 last_reason=none
重要なのは次の2つです。
link=connected
ip=192.168.0.xx
この状態になっていれば、ESP32がLANに参加できています。
自然言語でLEDを操作する
Wi-FiとOllamaの準備ができたら、自然言語を送ります。
cmd.exe /C "C:\Users\user\.platformio\penv\Scripts\python.exe scripts\zclaw_chat.py --timeout 2400 --raw COM6 GPIO23を点けて、GPIO22を消して"
成功すると、ログには次のような内容が出ます。
agent: Processing: GPIO23を点けて、GPIO22を消して
llm: Sending request to Ollama...
llm: Response: 200
agent: Tool call: gpio_write_many
tools: Exec: gpio_write_many
agent: Tool result: GPIO writes: 23=HIGH, 22=LOW
Done: GPIO writes: 23=HIGH, 22=LOW
これで、PCから入力した自然言語が、Ollamaによってtool callへ変換され、ESP32上のZClawがGPIOを操作したことが確認できます。
ハマったところ
Wi-Fiがつながらない
途中で、Wi-Fi設定は入っているのに接続できない状態がありました。
link=idle
ip=(none)
last_reason=4WAY_HANDSHAKE_TIMEOUT
この場合、ESP32はSSIDを見つけて接続を試みていますが、WPAの鍵交換で失敗しています。
よくある原因は次です。
Wi-Fiパスワード違い
2.4GHzではなく5GHz側を見ている
WPA3専用設定になっている
通常版ESP32は2.4GHz Wi-Fiのみ対応です。 そのため、ルーター側では2.4GHzが有効で、WPA2-PSK系の設定になっているかを確認しました。
自然言語を送るとESP32が落ちた
Wi-Fi未接続の状態で自然言語を送ると、OllamaへHTTP通信しようとしてESP32側で落ちることがありました。
ログには次のようなエラーが出ました。
assert failed: tcpip_send_msg_wait_sem ... (Invalid mbox)
これは、Wi-Fi/TCP/IPスタックが使える状態ではないのに、HTTP通信へ進もうとしたためだと考えられます。
そのため、自然言語を送る前には必ず次を確認するようにしました。
/wifi status
→ link=connected
→ ip=192.168.0.xx
まとめ
今回、ZClawとOllamaを使って、通常版ESP32に自然言語の指示でGPIO操作をさせることができました。
最終的な構成はこうです。
PCから自然言語を送る
↓
ESP32上のZClawが受け取る
↓
ESP32がWi-Fi経由でPC上のOllamaへ問い合わせる
↓
Ollamaがtool callを返す
↓
ZClawがgpio_write_manyを実行する
↓
GPIO22/23のLEDが点灯・消灯する
今回の実験で分かったのは、ESP32上でLLMそのものを動かさなくても、ESP32をtool実行側、PC上のOllamaを自然言語解釈側 に分ければ、かなり小さな構成で「自然言語で物理I/Oを操作する」体験が作れるということです。
まだ点滅のような時間を持つ動作には専用ツールが必要ですが、GPIOのON/OFFや状態確認のような基本操作は動きました。
次にやるなら、gpio_blink や gpio_blink_many を追加して、
GPIO22を5回点滅させて
GPIO23を0.2秒間隔で3秒間点滅させて
のような指示を、より安定して実行できるようにしたいです。