第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アプリを使う場合

  1. タスクトレイからOllamaを終了します。
  2. Windowsの「環境変数」でユーザー環境変数 OLLAMA_HOST を作り、値を 0.0.0.0:11434 にします。
  3. 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にクライアントを書く

完成コード:ollama_client.pyをダウンロード

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行の削除とサービス再読み込み・再起動で戻せます。

やってみよう

  1. PROMPT を変え、短い説明、クイズ、アイデア出しを試しましょう。
  2. センサーの値を質問に含め、「この値を短く説明して」と送ってみましょう。
  3. PC上で別のローカルモデルを導入し、同じ質問の回答時間と内容を比べましょう。

XiaoZhiの音声処理とは別の構成です。OllamaのChat APIだけでは音声の認識・合成は行わないため、音声化にはそれらの処理をつなぐ仕組みが必要です。