第6章
M5StackからOllamaのローカルAIを使う
パソコンにOllamaを導入し、UIFlow 2.0でCoreS3-Liteから質問を送って回答を表示します。
この章で学ぶこと
- パソコンにOllamaとローカルモデルを導入する
- 同じLAN内のM5StackからHTTP APIを呼ぶ
- AIの回答を本体画面とWebTerminalに表示する
- 通信とモデル実行の問題を切り分ける
作るものと役割分担
CoreS3-Liteの画面で「ASK」をタップすると、パソコンで動くAIへ質問し、回答を表示する端末を作ります。
Ollamaをインストールするのはパソコンです。 この教材のCoreS3-LiteはESP32-S3のマイコンで、PC用のOllamaや大規模言語モデルを本体にインストールして実行する構成にはしません。本体にはUIFlow 2.0のPythonプログラムを書き込みます。
CoreS3-Lite(タッチ・画面)
↓ Wi-Fi / HTTPで質問を送信
同じLAN内のPC(Ollama + gemma3:1b)
↓ PCで回答を生成
CoreS3-Lite(回答を表示)
今回はテキストの質問・回答を扱います。マイクによる音声入力や読み上げは含みません。音声アシスタントの導入は次の章「XiaoZhi」で扱います。
1. 準備する
- UIFlow 2.0のファームウェアが入ったCoreS3-Lite
- データ通信できるUSB Type-Cケーブル
- Ollamaに対応したmacOS・Windows・Linuxのパソコン
- PCとM5Stackが相互に通信できるLAN。M5Stackは2.4GHzのWi-Fiへ接続する
- Ollamaとモデルをダウンロードするためのインターネット接続
PCは有線LANでも構いません。同じルーターに接続していても、ゲストWi-Fiや端末間通信の制限があると通信できません。必要なメモリや処理速度はモデルとPCによって異なります。まず小さいテキストモデルで動作を確かめます。
2. PCにOllamaとモデルを入れる
Ollamaの公式ダウンロードページから、自分のOS用の手順に従ってインストールします。OSの対応条件はmacOSの案内・Windowsの案内・Linuxの案内を確認してください。
Ollamaを起動し、ターミナルまたはPowerShellで次を実行します。
ollama --version
ollama pull gemma3:1b
ollama run gemma3:1b
Linuxでサーバーが起動していない場合は、先に別のターミナルで ollama serve を起動します。すでにアプリやサービスが起動している場合、同じポートで二重に起動しません。
対話画面で What is a sensor? と入力して、回答が返ることを確認します。終了は /bye です。ダウンロード済みモデルは ollama list で確認できます。
ここではテキスト用の gemma3:1b を使います。モデルを変える場合は、後のAPIリクエストとPythonの MODEL も、ollama list に表示される名前にそろえます。ローカルモデルでの実行にはOllamaのAPIキーは不要です。Ollama Quickstart
3. PC上でAPIを試す
macOS・Linux
curl http://localhost:11434/api/chat \
-H 'Content-Type: application/json' \
-d '{"model":"gemma3:1b","messages":[{"role":"user","content":"What is a sensor? Answer in one short sentence."}],"stream":false}'
Windows PowerShell
$body = @{
model = "gemma3:1b"
messages = @(@{ role = "user"; content = "What is a sensor? Answer in one short sentence." })
stream = $false
} | ConvertTo-Json -Depth 5
$result = Invoke-RestMethod -Uri "http://localhost:11434/api/chat" -Method Post -ContentType "application/json" -Body $body
$result.message.content
/api/chat は会話形式の生成APIです。回答は message.content に入ります。stream: false を指定すると1つのJSONとして受け取れるので、小さい端末から扱いやすくなります。この例では毎回1件の質問だけを送り、会話履歴は引き継ぎません。Chat API
PCだけで回答が返らない場合は、先にモデル名、Ollamaの起動状態、PCのメモリを確認してください。M5Stackとの接続はその後で行います。
4. LAN内から接続できるようにする
Ollamaは標準では 127.0.0.1:11434 で待ち受けます。このままではPC自身からしか接続できないため、OLLAMA_HOST を変更して再起動します。Ollamaのネットワーク設定
macOSでOllamaアプリを使う場合
ターミナルで実行し、Ollamaアプリを終了してから起動し直します。
launchctl setenv OLLAMA_HOST "0.0.0.0:11434"
WindowsでOllamaアプリを使う場合
- タスクトレイからOllamaを終了します。
- Windowsの「環境変数」でユーザー環境変数
OLLAMA_HOSTを作り、値を0.0.0.0:11434にします。 - Ollamaをスタートメニューから起動し直します。
Linuxでsystemdサービスを使う場合
sudo systemctl edit ollama.service
エディタに以下を設定して保存します。
[Service]
Environment="OLLAMA_HOST=0.0.0.0:11434"
sudo systemctl daemon-reload
sudo systemctl restart ollama
OS別の設定はOllamaのサーバー設定も参照してください。
0.0.0.0 は待ち受けるための設定です。M5Stackに書く接続先はPCのLAN内IPアドレスです。たとえば 192.168.1.100 なら、接続先は http://192.168.1.100:11434 になります。
PCのネットワーク設定でIPアドレスを確認し、同じLANの別のPCやスマートフォンのブラウザから次の形式のURLを開きます。
http://192.168.1.100:11434/api/tags
自分のPCのIPへ置き換えてください。モデル一覧のJSONが返れば、LANからOllamaへの到達を確認できます。モデル一覧API
ファイアウォールで必要な場合は、信頼するプライベートネットワークからのTCP 11434への接続を許可します。ファイアウォール全体は無効にしません。この構成ではAPIに認証を付けていないため、ルーターのポート開放や公開トンネルは使わず、信頼するLAN内で利用します。
5. M5Stackにクライアントを書く
UIFlow 2.0で本体へ接続し、Python編集画面に次のコード全体を貼り付けます。先頭の WIFI_SSID・WIFI_PASSWORD・OLLAMA_URL を自分の環境に変更してください。M5Stackにとって localhost はM5Stack自身を指すので、PCのIPアドレスを指定します。
ollama_client.pyの完成コード
# CoreS3-Lite / UIFlow 2.0: PC上のOllamaへ質問する
import M5
import network
import requests2
import time
WIFI_SSID = "YOUR_WIFI_SSID"
WIFI_PASSWORD = "YOUR_WIFI_PASSWORD"
OLLAMA_URL = "http://192.168.1.100:11434/api/chat"
MODEL = "gemma3:1b"
PROMPT = "What is a sensor? Answer in simple English in at most 40 words."
def connect_wifi():
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
if not wlan.isconnected():
wlan.connect(WIFI_SSID, WIFI_PASSWORD)
started = time.ticks_ms()
while not wlan.isconnected():
M5.update()
if time.ticks_diff(time.ticks_ms(), started) > 30000:
raise RuntimeError("Wi-Fi timeout")
time.sleep_ms(100)
return wlan
def ask_ollama():
response = None
try:
response = requests2.post(
OLLAMA_URL,
json={
"model": MODEL,
"messages": [{"role": "user", "content": PROMPT}],
"stream": False,
"options": {"num_predict": 100, "num_ctx": 2048}
},
headers={"Content-Type": "application/json"},
timeout=120
)
if response.status_code != 200:
print("Server response:", response.text)
raise RuntimeError("HTTP " + str(response.status_code))
data = response.json()
if not isinstance(data, dict):
raise RuntimeError("Invalid JSON response")
if data.get("error"):
raise RuntimeError(str(data["error"]))
message = data.get("message")
if not isinstance(message, dict):
raise RuntimeError("Missing message")
answer = message.get("content")
if not isinstance(answer, str) or not answer.strip():
raise RuntimeError("Empty answer")
return answer
finally:
if response is not None:
response.close()
def show(title, text, button="ASK"):
lcd = M5.Lcd
lcd.fillScreen(0x101820)
lcd.setTextColor(0xFFFFFF, 0x101820)
lcd.setTextSize(1)
lcd.drawString(title, 8, 8)
lcd.drawString("Full response: WebTerminal", 8, 25)
# 標準フォントで表示できるASCIIに限定。全文はターミナルへ。
safe = "".join(c if 32 <= ord(c) <= 126 else " " for c in text)
lines = [safe[i:i + 48] for i in range(0, len(safe), 48)]
for index, line in enumerate(lines[:8]):
lcd.drawString(line, 8, 48 + index * 17)
if len(lines) > 8:
lcd.drawString("... see WebTerminal", 8, 187)
lcd.fillRect(8, 207, 304, 27, 0x285880)
lcd.setTextColor(0xFFFFFF, 0x285880)
lcd.drawString(button, 16, 216)
def main():
M5.begin()
M5.Lcd.setRotation(1)
if (M5.Lcd.width(), M5.Lcd.height()) != (320, 240):
raise RuntimeError("Expected 320x240 display")
show("Ollama client", "Tap ASK to connect and send the question.")
was_down = False
while True:
M5.update()
down = M5.Touch.getCount() > 0
if down and not was_down and M5.Touch.getY() >= 207:
try:
show("Connecting...", "Connecting to Wi-Fi", "WAIT")
connect_wifi()
show("Thinking...", PROMPT, "WAIT")
print("Question:", PROMPT)
answer = ask_ollama()
print("Answer:", answer)
show("Answer", answer)
except Exception as error:
print("Error:", error)
show("Error", str(error), "RETRY")
# 押しっぱなしでは再送しない。次は指を離してからタップ。
was_down = down
time.sleep_ms(30)
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
pass
except Exception as error:
import sys
sys.print_exception(error)
「Run Once」で実行し、「ASK」をタップします。Connecting...、Thinking... の後に回答が出れば成功です。初回はモデルの読み込みに時間がかかることがあります。
標準フォントのまま試せるよう、サンプルは英語で短く質問し、画面にはASCII文字だけを表示します。長い回答は画面で省略し、全文をWebTerminalへ出します。日本語で使う場合は PROMPT を変更してWebTerminalで確認し、本体表示には日本語フォントを別途用意してください。
このコードは同期通信のため、回答待ちの間はタッチ操作を処理しません。通信失敗時はエラーを表示し、自動再送せず、指を離して「RETRY」を押すと再実行します。実行できたらPCを起動したまま「Run Always」で本体へ保存し、再起動後の接続も確認します。
6. コードを理解する
| 設定・処理 | 役割 |
|---|---|
MODEL |
PCにダウンロードしたモデルの名前 |
PROMPT |
ASKを押したときに送る質問 |
stream: False |
生成結果を1つのJSONで受け取る |
num_predict: 100 |
生成トークン数を抑える。100文字という意味ではない |
num_ctx: 2048 |
このリクエストで使うコンテキスト長の設定 |
response.close() |
成功・失敗のどちらでも通信リソースを解放する |
モデルを大きくすると、必要なメモリや生成時間も変わります。回答が長すぎる場合は、質問に短く答える条件を付け、生成上限も調整します。M5Stack側で大きな応答全体を保持するとメモリ不足につながります。
HTTP処理にはUIFlow 2.0の requests2 を使います。OllamaのJSONに error があった場合やHTTPエラーも確認し、エラーを回答として表示しないようにしています。Ollamaのエラー形式
7. 動作確認とトラブル対処
| 段階・症状 | 確認すること |
|---|---|
ollama run で回答が出ない |
Ollamaの起動、モデルのダウンロード、メモリ、PC側のログ |
| PCのlocalhostには接続できるがLANからは不可 | OLLAMA_HOST と再起動、PCのIP、ファイアウォール、ゲストWi-Fiの分離 |
| Wi-Fi timeout | SSID・パスワード、2.4GHz、電波状態 |
| Connection refused | PCでOllamaが起動しているか、11434でLAN待ち受けしているか |
| タイムアウト | PCのスリープ、ネットワーク、モデル読み込み・生成時間。まずPC側で短い質問を試す |
| HTTP 404 / model not found | ollama list のモデル名と MODEL、/api/chat のパス |
unexpected keyword argument |
UIFlow 2.0のrequests2の版と timeout 対応。対応ファームウェアへ更新して試す |
| 日本語が本体画面に出ない | サンプルのASCII制限。全文はWebTerminalで確認する |
| 昨日まで動いたのに接続できない | PCのIPが変わっていないか。必要ならルーターでDHCP予約を設定する |
作業後にLANからの接続を止める場合は、OLLAMA_HOST を削除するか 127.0.0.1:11434 に戻してOllamaを再起動します。macOSでは launchctl unsetenv OLLAMA_HOST、Windowsでは追加したユーザー環境変数の削除、Linuxでは追加したEnvironment行の削除とサービス再読み込み・再起動で戻せます。
やってみよう
PROMPTを変え、短い説明、クイズ、アイデア出しを試しましょう。- センサーの値を質問に含め、「この値を短く説明して」と送ってみましょう。
- PC上で別のローカルモデルを導入し、同じ質問の回答時間と内容を比べましょう。
XiaoZhiの音声処理とは別の構成です。OllamaのChat APIだけでは音声の認識・合成は行わないため、音声化にはそれらの処理をつなぐ仕組みが必要です。