参考にしたもの

  • 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制御だけを切り分けて確認できます。


実際のデモ

demo

動かすとこの動画のように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_blinkgpio_blink_many を追加して、

GPIO22を5回点滅させて
GPIO23を0.2秒間隔で3秒間点滅させて

のような指示を、より安定して実行できるようにしたいです。