ガジェットコンパス

ガジェット探求の旅に終わりはない
🔍
M5Stack生成AIローカルLLMロボットESP32会話ロボットスタックチャンIoTマイコンAI実装

M5Stack × ローカルLLMで作る会話ロボット「スタックチャン」:初心者向け実装完全ガイド【2025年最新版】

👤 いわぶち 📅 2025-12-05 ⭐ 4.7点 ⏱️ 18m
M5Stack × ローカルLLMで作る会話ロボット「スタックチャン」:初心者向け実装完全ガイド【2025年最新版】

ポッドキャスト

🎙️ 音声: ずんだもん / 春日部つむぎ(VOICEVOX)

📌 1分で分かる記事要約

  • M5StackはAIを搭載した会話ロボット「スタックチャン」の理想的なベースデバイスで、初心者でも実装可能
  • ローカルLLMを使えば、クラウドなしでプライベートな会話が実現でき、レスポンス速度も高速
  • ハードウェアの選定からセットアップ、トラブル対応まで段階的に進めることが成功のカギ
  • 外部PC上にLLMを配置し、M5Stackはフロントエンド(顔・音声・ボタン)に集中させる設計が現実的
  • この記事を読むと、自分だけのAI相棒を数時間で完成させられる知識が身につきます

はじめに:机の上にAI相棒が欲しくなった時代

「机の上に小さなロボットを置いて、話しかけたら返事してくれたら…」そんな想いを実現できる時代がやってきました。

M5Stackという小型マイコンボードと、最近話題のローカルLLM(大規模言語モデル)を組み合わせることで、クラウドに頼らないプライベートな会話ロボット「スタックチャン」を自分で作ることができます

難しそうに聞こえるかもしれませんが、実は意外とシンプル。この記事では、初心者向けに「何を買って、どう組み立てて、どう動かすか」を、つまずきやすいポイントも含めて詳しく解説していきます。


M5Stackとスタックチャンって何?

M5Stackの基本を知ろう

M5Stackは、ESP32というWi-Fi対応のマイコンを搭載した、手のひらサイズのコンピュータです。スマートフォンくらいの大きさで、液晶画面、スピーカー、ボタンが付いています。

もともとはIoT(Internet of Things)プロトタイピング向けのツールでしたが、最近は生成AIと組み合わせて「AI相棒ロボット」を作る人が急増しています。理由は明確です:

  • 小さくて可愛い:机の上に置いても邪魔にならない
  • 拡張性が高い:マイク、スピーカー、カメラなど好きなパーツを足せる
  • プログラミングが簡単:Arduinoという初心者向けの開発環境で動く
  • コスト効率が良い:本体が数千円で手に入る

「スタックチャン」とは何か

スタックチャンは、M5Stackに生成AIを搭載した、会話できる小型ロボットの総称です。もともとは日本の技術者コミュニティで始まったプロジェクトで、かわいい顔のグラフィックが表示されて、あなたの質問に答えてくれます。

実装方法は複数ありますが、最も実用的なのが「ローカルLLMを使った構成」です。つまり、データをクラウドに送らずに、手元のコンピュータやM5Stack自体でAIが考えて返事をしてくれるということ。プライバシーも守られるし、インターネット接続がなくても動きます。


必要なハードウェア部品:何を買えばいい?

実装に必要なハードウェアを、型番と入手先を含めてリストアップしました。

M5Stack本体:どれを選ぶ?

推奨モデル:M5Stack CoreS3 または M5Stack Core2

項目CoreS3Core2
CPUESP32-S3ESP32
RAM512KB + PSRAM 8MB約520KB
フラッシュ16MB4-16MB
液晶2.0インチ(320×240)2.0インチ(320×240)
音声機能マイク内蔵別途追加が推奨
推奨用途音声対話重視シンプル実装

入手先: マルツオンライン、Switch Science、eleshop など

私からのアドバイス: 初めての購入なら CoreS3 一択をお勧めします。マイクが内蔵されているので、後から「あ、マイク買い忘れた」という失敗がありません。

マイク:音声入力の要

CoreS3を選んだ場合、マイクは内蔵されていますが、より高精度な音声認識を目指すなら、専用ユニットの追加も検討します。

推奨オプション:

  • M5Stack AV Unit(ES8388音声コーデック搭載、16bit ステレオ対応)
  • Atom Echo Base(マイク+スピーカー一体型、コンパクト)

入手先: Switch Science、マルツオンライン

注意点: 複数のマイクを接続する場合、I2Sインターフェースの設定が重複しないよう注意してください。

スピーカー:声を出す

M5Stack本体にはスピーカーが付いていますが、音量が小さいため、別途スピーカーユニットの追加が実用的です。

推奨モデル:

  • SoundDropMini16M UNIT(Grove接続対応、16MBフラッシュ搭載)
  • M5Stack SPK UNIT(アンプ内蔵、3W出力)

入手先: Switch Science、マルツオンライン

ベストプラクティス: スピーカーは3W程度の出力で十分です。大きすぎる音は歪みが出やすく、会話ロボットとしての聞き心地が悪くなります。

その他の周辺機器

部品名推奨仕様用途入手先
microSDカード32GB以上音声ログ、モデルファイル保存一般家電量販店
USB Type-Cケーブル標準品充電・プログラム書き込み100均から可
電源アダプタ5V/2A以上安定動作用マルツオンライン、eleshop
ジャンパーワイヤオスメス混合I2S接続用(拡張ユニット使用時)秋葉原、オンラインショップ

ハードウェア選定のチェックリスト

購入前に以下を確認してください:

  • M5Stack CoreS3またはCore2を選択した
  • マイク(内蔵またはユニット)の対応を確認した
  • スピーカーユニットの必要性を判断した
  • microSDカード(32GB以上)を用意した
  • USB Type-Cケーブルと電源アダプタを確保した
  • 各部品が同じGroveコネクタまたはI2Sインターフェースに対応していることを確認した

開発環境の構築:PCのセットアップ

M5Stackにプログラムを書き込むために、PCに開発環境を整える必要があります。

Arduino IDE 2.x系のインストール

ステップ1:Arduino IDEをダウンロード

公式サイト(https://www.arduino.cc/en/software)から最新版をダウンロードしてインストールします。Windows/macOS/Linuxすべてに対応しています。

ステップ2:ESP32ボード定義を追加

  1. Arduino IDEを開く
  2. メニューから「ファイル」→「環境設定」を選択
  3. 「追加のボードマネージャのURL」に以下を貼り付ける:
    https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
  4. 「OK」をクリック

ステップ3:ボードマネージャからESP32をインストール

  1. 「ツール」→「ボード」→「ボードマネージャ」を選択
  2. 検索欄に「esp32」と入力
  3. 「esp32 by Espressif Systems」を選択して「インストール」をクリック

※インストール完了まで数分かかります。コーヒーを飲んで待ちましょう。

ステップ4:ボードを選択

  1. 「ツール」→「ボード」から「M5Stack-Core2」(またはESP32-S3 Dev Module)を選択
  2. 「ツール」→「ボーレート」から「115200」を選択
  3. 「ツール」→「ポート」からM5Stackが接続されているポートを選択

M5Stackライブラリのインストール

M5Stackを簡単に扱うための公式ライブラリをインストールします。

ステップ1:ライブラリマネージャを開く

「スケッチ」→「ライブラリをインクルード」→「ライブラリを管理」

ステップ2:必要なライブラリをインストール

検索欄に以下を入力して、順番にインストールしてください:

  1. M5Unified(最新版推奨)

    • M5Stackシリーズ全体を統一的に扱えるライブラリ
    • 古い「M5Stack.h」より優れています
  2. M5GFX(M5Unifiedと一緒に入ることが多い)

    • グラフィック描画用
  3. ArduinoJson(バージョン6.x系)

    • LLMサーバとのJSON通信に必須
  4. WiFiClientSecure(標準ライブラリ)

    • HTTPS通信用

初めての書き込みテスト

セットアップが完了したら、動作確認をしましょう。

テストコード:

#include <M5Unified.h>

void setup() {
  auto cfg = M5.config();
  M5.begin(cfg);
  M5.Display.setTextSize(2);
  M5.Display.setCursor(10, 40);
  M5.Display.println("Setup OK!");
  M5.Display.println("M5Stack is ready!");
}

void loop() {
  M5.update();
  delay(100);
}

書き込み方法:

  1. 上記コードをArduino IDEにコピペ
  2. 「スケッチ」→「マイコンボードに書き込む」をクリック
  3. コンパイルと書き込みが自動で進む
  4. M5Stackの画面に「Setup OK!」と表示されたら成功

うまくいかない場合:

  • 「ポートが見つかりません」 → USB Type-Cケーブルの接続を確認。別のケーブルを試す
  • 「ボーレート115200に接続できない」 → ドライバをインストール。CH9102またはCP210xドライバをメーカーサイトからダウンロード
  • 「フラッシュに書き込めません」 → M5Stackの電源をいったん切って、もう一度つけ直す

サーバ側のローカルLLM環境構築

M5Stack単体では大規模な言語モデルを動かせないため、PC(またはRaspberry Pi)上にローカルLLMを配置します。M5Stackはそれにネットワーク経由で質問を送り、返答を受け取る構成です。

ローカルLLM実行環境の選択肢

Ollama(推奨・初心者向け)

OllamaはWindows/macOS/Linux対応の、最も簡単にローカルLLMを実行できるツールです。

  • インストールが簡単(インストーラをクリックするだけ)
  • REST APIが自動で立ち上がる(M5Stackから叩きやすい)
  • 日本語対応モデルも豊富
  • GPU/CPU自動選択

llama.cpp(軽量・Raspberry Pi向け)

C++製の軽量LLM実行エンジン。Raspberry Pi 4/5でも動きます。

  • メモリ効率が良い
  • ラズパイでも実用的な速度
  • セットアップは少し複雑

LM Studio(GUI重視)

GUIが充実しているため、コマンドラインが苦手な人向け。

Ollamaのセットアップ手順(Windows/macOS)

ステップ1:Ollamaをダウンロード・インストール

公式サイト(https://ollama.ai)から、あなたのOS用インストーラをダウンロード。通常のアプリケーションと同じように「次へ」をクリックしていくだけです。

ステップ2:ターミナル(コマンドプロンプト)を開く

  • Windows:スタートメニューから「cmd」または「PowerShell」を検索して開く
  • macOS:Spotlight検索(⌘+スペース)で「ターミナル」を検索

ステップ3:モデルをダウンロード・起動

以下のコマンドを実行します。初回は数分かかります(モデルサイズによる)。

ollama run llama2

または、より軽い日本語対応モデルなら:

ollama run neural-chat

完了すると、以下のようなプロンプトが表示されます:

>>> 

ステップ4:テスト実行

プロンプトに質問を入力してみます:

>>> こんにちは。あなたは誰ですか?

AIが日本語で返答したら成功です。

ステップ5:バックグラウンド起動の確認

Ollamaはバックグラウンドで常に起動している状態です。ブラウザで以下にアクセスして、APIが動作しているか確認:

http://localhost:11434/api/generate

Raspberry Piでのllama.cpp セットアップ

Raspberry Pi 5で軽量LLMを動かしたい場合:

ステップ1:必要なツールをインストール

sudo apt-get update
sudo apt-get install build-essential cmake git

ステップ2:llama.cppをクローン

git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp
make

ステップ3:モデルファイルをダウンロード

Hugging Face(https://huggingface.co)から、gguf形式の軽量モデルをダウンロード。例えば:

wget https://huggingface.co/...(モデルURL)

ステップ4:サーバを起動

./server -m model.gguf --host 0.0.0.0 --port 8000

これで、M5Stackから http://192.168.x.x:8000 にアクセスできるようになります。

ローカルLLM環境構築のチェックリスト

  • Ollamaをインストールした(またはllama.cppをビルドした)
  • モデルをダウンロード・起動できた
  • ローカルホストのポート(11434またはカスタムポート)にアクセスできた
  • LLMが日本語で返答することを確認した
  • ファイアウォール設定で、M5Stackが同じLAN内からアクセス可能か確認した

M5StackとLLMサーバの通信設計

M5StackとローカルLLMの間で、どのようにやりとりするかを設計します。

通信プロトコル:HTTP POSTが最もシンプル

M5StackからLLMサーバへ質問を送り、返答を受け取る最も簡単な方法は、HTTP POST リクエストです。

リクエストの例:

POST http://192.168.1.100:11434/api/generate
Content-Type: application/json

{
  "model": "llama2",
  "prompt": "こんにちは。今日の天気は?",
  "stream": false
}

レスポンスの例:

{
  "model": "llama2",
  "created_at": "2025-12-05T10:30:00.000000Z",
  "response": "こんにちは。申し訳ありませんが、リアルタイムの天気情報は持っていません。...",
  "done": true
}

M5Stack側は、このJSONを解析して、response フィールドのテキストを画面に表示します。

セッション管理:会話履歴を保持する

ロボットが「前の話題を覚えている」ような自然な会話をするには、会話履歴をサーバ側で管理するのが効果的です。

設計例:

M5Stack側:
1. ボタンを押して録音開始
2. 音声→テキスト変換(別プロセス)
3. ユーザーテキストをサーバに送信

サーバ側:
1. ユーザーテキストを受け取る
2. セッション履歴に追加
3. LLMに全履歴を含めてプロンプト生成
4. LLMから返答を得る
5. セッション履歴に返答を追加
6. M5Stackに返答を返す

M5Stack側:
1. 返答を受け取る
2. テキスト→音声変換
3. スピーカーから再生

この流れにより、ユーザーが「前に話したことを覚えてくれている」という体験が生まれます。

プロンプトエンジニアリング:ロボットのキャラを作る

LLMの返答は、与えるプロンプト(指示文)で大きく変わります。スタックチャンのキャラクターを作り込むには、システムプロンプトを工夫します。

例1:フレンドリーなアシスタント

あなたはM5Stackに搭載された、親切で楽しい小型ロボットです。
ユーザーの質問に対して、わかりやすく、ときどき冗談も交えながら答えてください。
返答は1-2文で簡潔にしてください。

例2:先生キャラ

あなたは小学生にもわかるように丁寧に説明する先生です。
難しい言葉は避けて、具体例を交えながら説明してください。
返答は2-3文程度にしてください。

例3:ツンデレキャラ

あなたはM5Stackに搭載された、ちょっとぶっきらぼうだけど実は優しいロボットです。
質問には答えてくれますが、ときどき照れくさそうな返事をします。
返答は1-2文で、時々「別に…」と前置きしてください。

このプロンプトをリクエストに含めることで、同じLLMでも性格が変わります。


実装コード例:最小限から実用的なレベルまで

ここからは、実際に動く最小限のコード例を3段階で提示します。

コード例1:超シンプル版「ボタンで返事」

M5Stackの画面にあらかじめ用意したセリフを表示するだけの、最も簡単な実装です。

目的: M5Stackの基本操作(画面表示、ボタン入力)を理解する

#include <M5Unified.h>

// ロボットのセリフ(あらかじめ用意)
const char* responses[] = {
  "こんにちは!スタックチャンです!",
  "今日はいい天気ですね",
  "何かお手伝いできることはありますか?",
  "ずっと君のそばにいるよ",
  "また明日ね!"
};

int currentIndex = 0;

void setup() {
  auto cfg = M5.config();
  M5.begin(cfg);
  
  M5.Display.setTextSize(2);
  M5.Display.setTextColor(TFT_WHITE, TFT_BLACK);
  M5.Display.setCursor(10, 20);
  M5.Display.println("=== StackChan ===");
  M5.Display.setCursor(10, 50);
  M5.Display.println("Button A: Next");
  M5.Display.println("Button B: Reset");
}

void loop() {
  M5.update();
  
  if (M5.BtnA.wasPressed()) {
    // ボタンAが押されたら次のセリフを表示
    M5.Display.clear();
    M5.Display.setCursor(10, 40);
    M5.Display.setTextSize(2);
    M5.Display.println(responses[currentIndex]);
    currentIndex = (currentIndex + 1) % (sizeof(responses) / sizeof(responses[0]));
  }
  
  if (M5.BtnB.wasPressed()) {
    // ボタンBが押されたらリセット
    currentIndex = 0;
    M5.Display.clear();
    M5.Display.setCursor(10, 50);
    M5.Display.println("Reset!");
  }
  
  delay(100);
}

使い方:

  1. このコードをArduino IDEにコピペ
  2. M5Stackに書き込む
  3. ボタンAを押すたびに、セリフが変わる

学べること:

  • M5.Display.println() で画面に文字を出す方法
  • M5.BtnA.wasPressed() でボタン入力を検出する方法
  • 配列を使ってセリフを管理する方法

コード例2:シリアル経由で「なんちゃって会話」

PC側でLLMを実行し、M5Stackはシリアル接続でやりとりする実装です。ここで初めてAIとの連携が出てきます。

目的: M5StackとPC間のシリアル通信を理解する

M5Stack側のコード:

#include <M5Unified.h>

String inputBuffer = "";
String responseBuffer = "";

void setup() {
  auto cfg = M5.config();
  M5.begin(cfg);
  Serial.begin(115200);
  
  M5.Display.setTextSize(2);
  M5.Display.setCursor(10, 20);
  M5.Display.println("=== AI Chat ===");
  M5.Display.setCursor(10, 60);
  M5.Display.println("Waiting for PC...");
}

void loop() {
  M5.update();
  
  // シリアルからデータを受け取る
  while (Serial.available() > 0) {
    char c = Serial.read();
    if (c == '\n') {
      // 改行で終了と判定
      displayResponse(responseBuffer);
      responseBuffer = "";
    } else {
      responseBuffer += c;
    }
  }
  
  // ボタンAで質問を送信(テスト用)
  if (M5.BtnA.wasPressed()) {
    String question = "What is 2+2?";
    Serial.println(question);
    M5.Display.clear();
    M5.Display.setCursor(10, 40);
    M5.Display.println("You: " + question);
  }
  
  delay(100);
}

void displayResponse(String response) {
  M5.Display.clear();
  M5.Display.setTextSize(1);
  M5.Display.setCursor(5, 20);
  M5.Display.println("AI Response:");
  M5.Display.setTextSize(2);
  M5.Display.setCursor(10, 60);
  
  // 長いテキストは折り返す
  if (response.length() > 20) {
    M5.Display.println(response.substring(0, 20));
    M5.Display.println(response.substring(20));
  } else {
    M5.Display.println(response);
  }
}

PC側のPythonコード(最小版):

import serial
import time

# M5Stackと接続(ポート番号はあなたの環境に合わせてください)
port = serial.Serial("COM3", 115200, timeout=1)
time.sleep(2)  # 接続安定化待ち

print("M5Stack connected. Type 'quit' to exit.")

while True:
    # M5Stackからデータを受け取る
    if port.in_waiting > 0:
        question = port.readline().decode('utf-8').strip()
        if question:
            print(f"M5Stack: {question}")
            
            # ここでLLMに質問を投げる
            # (簡略版:固定の返答を返す)
            response = f"You asked: {question}. Here's my answer!"
            
            # M5Stackに返答を送信
            port.write((response + "\n").encode('utf-8'))
            print(f"Response sent: {response}")
    
    time.sleep(0.1)

使い方:

  1. M5StackをUSBでPCに接続
  2. M5Stack側のコードを書き込む
  3. PCでPythonスクリプトを実行
  4. M5StackのボタンAを押すと、PCから返答が返ってくる

次のステップ: このPythonコードの「固定の返答」部分を、OllamaのAPIに置き換えると、本格的なLLM連携になります。


コード例3:本格版「Wi-Fi経由でLLMと会話」

M5StackがWi-Fi経由でローカルLLMサーバに直接質問を送り、返答を受け取る本格的な実装です。

目的: M5StackとローカルLLMサーバをネットワーク接続させる

#include <M5Unified.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>

// Wi-Fi設定
const char* SSID = "YOUR_SSID";
const char* PASSWORD = "YOUR_PASSWORD";

// LLMサーバ設定
const char* LLM_SERVER = "http://192.168.1.100:11434";
const char* MODEL_NAME = "llama2";

String conversationHistory = "";

void setup() {
  auto cfg = M5.config();
  M5.begin(cfg);
  
  M5.Display.setTextSize(2);
  M5.Display.setCursor(10, 20);
  M5.Display.println("Connecting WiFi...");
  
  // Wi-Fi接続
  WiFi.begin(SSID, PASSWORD);
  int attempts = 0;
  while (WiFi.status() != WL_CONNECTED && attempts < 20) {
    delay(500);
    attempts++;
  }
  
  if (WiFi.status() == WL_CONNECTED) {
    M5.Display.clear();
    M5.Display.setCursor(10, 20);
    M5.Display.println("WiFi: OK");
    M5.Display.setCursor(10, 60);
    M5.Display.println(WiFi.localIP().toString());
  } else {
    M5.Display.clear();
    M5.Display.setCursor(10, 20);
    M5.Display.println("WiFi: FAILED");
  }
  
  delay(2000);
}

void loop() {
  M5.update();
  
  if (M5.BtnA.wasPressed()) {
    // ボタンAで質問を送信
    askLLM("こんにちは。元気ですか?");
  }
  
  if (M5.BtnB.wasPressed()) {
    // ボタンBで会話をリセット
    conversationHistory = "";
    M5.Display.clear();
    M5.Display.setCursor(10, 40);
    M5.Display.println("Conversation reset");
    delay(1000);
  }
  
  delay(100);
}

void askLLM(String userMessage) {
  M5.Display.clear();
  M5.Display.setTextSize(2);
  M5.Display.setCursor(10, 20);
  M5.Display.println("Thinking...");
  
  // LLMサーバへのリクエスト作成
  HTTPClient http;
  String url = String(LLM_SERVER) + "/api/generate";
  
  // JSONペイロード作成
  StaticJsonDocument<1024> doc;
  doc["model"] = MODEL_NAME;
  doc["prompt"] = userMessage;
  doc["stream"] = false;
  
  String jsonString;
  serializeJson(doc, jsonString);
  
  // POST送信
  http.begin(url);
  http.addHeader("Content-Type", "application/json");
  int httpCode = http.POST(jsonString);
  
  if (httpCode == 200) {
    String response = http.getString();
    
    // JSON解析
    StaticJsonDocument<2048> responseDoc;
    DeserializationError error = deserializeJson(responseDoc, response);
    
    if (!error) {
      String aiResponse = responseDoc["response"].as<String>();
      
      // 画面に表示
      M5.Display.clear();
      M5.Display.setTextSize(1);
      M5.Display.setCursor(5, 10);
      M5.Display.println("You: " + userMessage);
      M5.Display.setCursor(5, 60);
      M5.Display.println("AI:");
      M5.Display.setTextSize(2);
      M5.Display.setCursor(10, 90);
      
      // テキストが長い場合は折り返す
      if (aiResponse.length() > 15) {
        M5.Display.println(aiResponse.substring(0, 15));
        M5.Display.println(aiResponse.substring(15, 30));
      } else {
        M5.Display.println(aiResponse);
      }
      
      // シリアルにも出力(デバッグ用)
      Serial.println("User: " + userMessage);
      Serial.println("AI: " + aiResponse);
    }
  } else {
    M5.Display.clear();
    M5.Display.setCursor(10, 40);
    M5.Display.println("Error: " + String(httpCode));
    Serial.println("HTTP Error: " + String(httpCode));
  }
  
  http.end();
}

セットアップ手順:

  1. コード内の SSIDPASSWORD をあなたのWi-Fi情報に変更
  2. LLM_SERVER のIPアドレスを、ローカルLLMが動作しているPCのアドレスに変更
  3. M5Stackに書き込む
  4. ボタンAを押すと、LLMに質問が送られる

重要な注意点:

  • ファイアウォール設定: ローカルLLMサーバが、M5Stackからのアクセスを許可しているか確認してください
  • タイムアウト: LLMの推論に時間がかかる場合、タイムアウトエラーが出ることがあります。その場合は、LLMサーバ側のタイムアウト設定を増やしてください
  • メモリ: このコードはメモリを多く使用するため、PSRAM付きのM5Stack(CoreS3など)の使用を推奨します

音声入出力の実装:マイクとスピーカーを活かす

会話ロボットの真価は、音声でやりとりできることです。

マイク入力の基本

M5Stack CoreS3には内蔵マイクがありますが、より高精度な音声認識には、I2S接続のマイクの追加を検討します。

I2S マイク接続の概要:

M5Stack Pin 配置(CoreS3の例)
- GPIO 1: I2S MCLK
- GPIO 2: I2S DOUT(データ出力)
- GPIO 42: I2S BCLK(ビットクロック)
- GPIO 41: I2S LRCK(左右チャンネルクロック)

実装コード例(マイク入力を取得):

#include <M5Unified.h>
#include <driver/i2s.h>

#define I2S_PORT I2S_NUM_0
#define I2S_SAMPLE_RATE 16000
#define I2S_BITS_PER_SAMPLE 16

void setupMicrophone() {
  i2s_config_t i2s_config = {
    .mode = I2S_MODE_MASTER | I2S_MODE_RX,
    .sample_rate = I2S_SAMPLE_RATE,
    .bits_per_sample = I2S_BITS_PER_SAMPLE,
    .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
    .communication_format = I2S_COMM_FORMAT_STAND_I2S,
    .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
    .dma_buf_count = 4,
    .dma_buf_len = 1024,
  };
  
  i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL);
  
  // ピン設定
  i2s_pin_config_t pin_config = {
    .bck_io_num = 42,
    .ws_io_num = 41,
    .data_out_num = I2S_PIN_NO_CHANGE,
    .data_in_num = 2,
  };
  i2s_set_pin(I2S_PORT, &pin_config);
}

void readMicrophone() {
  uint8_t buffer[1024];
  size_t bytes_read = 0;
  
  i2s_read(I2S_PORT, buffer, sizeof(buffer), &bytes_read, portMAX_DELAY);
  
  // バッファに音声データが入っている
  // これを音声認識APIに送信する
}

スピーカー出力の実装

テキストを音声に変換(TTS:Text-to-Speech)して再生します。

シンプルな方法: PC側でTTSを行い、MP3/WAVファイルをM5Stackにダウンロード+再生

#include <M5Unified.h>
#include <driver/i2s.h>

void playAudioFile(const char* filename) {
  // microSDカードからMP3ファイルを読み込んで再生
  // (詳細な実装は省略)
}

より本格的な方法: Google Cloud TTSなどのAPIを使用

void speakText(String text) {
  // 1. テキストをGoogle Cloud TTSに送信
  // 2. 返ってきた音声ファイルをダウンロード
  // 3. M5Stackのスピーカーで再生
}

トラブルシューティング:うまくいかない時の対処法

ネットワーク接続エラー

症状: 「WiFi connected」と出ているのに、LLMサーバに接続できない

チェックリスト:

  1. ルーターの2.4GHz帯に接続しているか

    • M5StackのESP32は5GHz非対応です
    • ルーター設定で2.4GHz帯を有効にしてください
  2. LLMサーバのIPアドレスが正しいか

    • PC側で ipconfig(Windows)または ifconfig(Mac/Linux)を実行
    • 表示されたIPアドレスをコードに貼り付けてください
  3. ファイアウォールがブロックしていないか

    • Windows Defender ファイアウォール設定を確認
    • ローカルLLMサーバのポート(11434など)を許可リストに追加
  4. LLMサーバが起動しているか

    • ターミナルで ollama serve を実行し、起動を確認
    • ブラウザで http://localhost:11434 にアクセスして確認

対処方法:

// デバッグ用にシリアル出力を追加
void setup() {
  Serial.begin(115200);
  WiFi.begin(SSID, PASSWORD);
  
  int attempts = 0;
  while (WiFi.status() != WL_CONNECTED && attempts < 20) {
    Serial.print(".");
    delay(500);
    attempts++;
  }
  
  if (WiFi.status() == WL_CONNECTED) {
    Serial.println("\nWiFi connected!");
    Serial.println("IP: " + WiFi.localIP().toString());
  } else {
    Serial.println("\nWiFi failed!");
  }
}

ビルド失敗エラー

症状: 「error: ‘M5Unified’ does not name a type」などのエラー

原因と対処:

エラーメッセージ原因対処方法
xxx was not declared in this scopeライブラリのインクルード漏れ#include <M5Unified.h> を追加
No such file or directoryライブラリがインストールされていないライブラリマネージャからインストール
flash size is not enoughフラッシュメモリ不足ツール→フラッシュサイズを「16MB」に変更

LLMサーバの応答が遅い

症状: ボタンを押してから返答が出るまで5秒以上かかる

原因:

  • LLMモデルが大きすぎる(7B以上)
  • LLMサーバのマシンのスペック不足
  • ネットワーク遅延

対処方法:

# より軽いモデルに切り替え
ollama run neural-chat  # llama2より軽い
ollama run orca-mini    # さらに軽い

または、M5Stack側でタイムアウト値を増やす:

http.setTimeout(30000);  // 30秒に設定

音声認識がうまくいかない

症状: マイクから音を拾っていない、または認識率が低い

チェックリスト:

  1. マイクが正しく接続されているか

    • I2Sピン配置を確認
    • ハンダ付けが甘くないか確認
  2. マイク入力レベルが適切か

    • 音量が小さすぎないか
    • ノイズが多すぎないか
  3. 音声認識APIが正しく動作しているか

    • PC側でWhisperなどを単体テスト

デバッグ用コード:

void debugMicrophone() {
  uint8_t buffer[512];
  size_t bytes_read = 0;
  
  i2s_read(I2S_PORT, buffer, sizeof(buffer), &bytes_read, portMAX_DELAY);
  
  // マイク入力レベルを計算
  int16_t* samples = (int16_t*)buffer;
  int maxLevel = 0;
  for (int i = 0; i < bytes_read / 2; i++) {
    maxLevel = max(maxLevel, abs(samples[i]));
  }
  
  Serial.print("Mic Level: ");
  Serial.println(maxLevel);
}

Cloudflare障害などの外部サービス依存エラー

症状: 「Cloudflare Tunnel経由でLLMに接続できない」などのエラー

背景知識: 日記にもあった通り、外部サービス(Cloudflare、Firebase など)は時々障害が発生します。

対処方法:

  1. 外部サービスのステータスページを確認

  2. 代替経路を用意しておく

    // メイン経路がダメなら、ローカルネットワーク直接アクセスに切り替え
    const char* PRIMARY_SERVER = "https://api.example.com/llm";  // Cloudflare経由
    const char* FALLBACK_SERVER = "http://192.168.1.100:11434";  // ローカル直接
    
    bool connectToLLM(const char* server) {
      HTTPClient http;
      http.begin(server);
      int code = http.GET();
      http.end();
      return (code == 200);
    }
    
    void setup() {
      if (!connectToLLM(PRIMARY_SERVER)) {
        // フォールバック
        Serial.println("Using fallback server");
      }
    }
  3. ローカルLLMをメインにする

    • 外部サービスに依存しない構成にすれば、障害の影響を受けない

応用例:さらに面白くする工夫

基本的な実装ができたら、以下のような応用も考えてみてください。

顔グラフィックのアニメーション

M5Stackの画面に、かわいい顔を表示してアニメーションさせます。

void drawFace(bool happy) {
  M5.Display.fillCircle(160, 120, 50, TFT_WHITE);  // 顔
  
  if (happy) {
    M5.Display.fillCircle(140, 110, 5, TFT_BLACK);   // 左目
    M5.Display.fillCircle(180, 110, 5, TFT_BLACK);   // 右目
    M5.Display.drawArc(160, 130, 20, 10, 0, 180, TFT_BLACK);  // 笑顔
  } else {
    M5.Display.fillCircle(140, 110, 5, TFT_BLACK);
    M5.Display.fillCircle(180, 110, 5, TFT_BLACK);
    M5.Display.drawLine(140, 135, 180, 135, TFT_BLACK);  // 真顔
  }
}

複数のキャラクター切り替え

ボタンで性格を切り替えられるようにします。

enum Personality {
  FRIENDLY,
  TEACHER,
  TSUNDERE
};

Personality currentPersonality = FRIENDLY;

String getSystemPrompt() {
  switch (currentPersonality) {
    case FRIENDLY:
      return "あなたは親切で楽しいロボットです。";
    case TEACHER:
      return "あなたは丁寧に説明する先生です。";
    case TSUNDERE:
      return "あなたはツンデレなロボットです。";
    default:
      return "";
  }
}

定期的な話しかけ

ロボットが時々、ユーザーに話しかけるようにします。

unsigned long lastSpeakTime = 0;
const unsigned long SPEAK_INTERVAL = 300000;  // 5分ごと

void loop() {
  if (millis() - lastSpeakTime > SPEAK_INTERVAL) {
    speakRandomMessage();
    lastSpeakTime = millis();
  }
}

void speakRandomMessage() {
  const char* messages[] = {
    "そろそろ休憩しない?",
    "何か手伝えることあるかな?",
    "ずっと君を見守ってるよ"
  };
  
  int idx = random(0, sizeof(messages) / sizeof(messages[0]));
  askLLM(messages[idx]);
}

まとめ:あなたのAI相棒が完成したら

ここまでで、M5StackとローカルLLMを組み合わせた会話ロボット「スタックチャン」の実装方法をすべて解説しました。

できるようになったこと:

  • ✅ M5Stackの基本的な使い方
  • ✅ ローカルLLMの環境構築
  • ✅ M5StackとLLMサーバの通信
  • ✅ 会話ロボットの実装
  • ✅ トラブルシューティング

次のステップ:

  1. まずは超シンプル版(コード例1)を試す → 環境が正しく動作しているか確認
  2. シリアル版(コード例2)に進む → PC連携の基本を理解
  3. Wi-Fi版(コード例3)に挑戦 → ローカルLLMとの本格連携
  4. 音声入出力を追加 → 完全な会話ロボットへ
  5. キャラクターを作り込む → あなただけのAI相棒に

最後に:

このプロジェクトは、「技術」と「創造性」の両方が必要です。ハードウェアを接続し、コードを書き、時にはうまくいかないことに悩みながら、少しずつ完成に近づいていく過程そのものが、最大の楽しみです。

あなたが作ったスタックチャンが、机の上であなたの質問に答えてくれる瞬間、きっと素敵な達成感が得られるはずです。

さあ、始めましょう。あなたのAI相棒との出会いが待っています!


参考資料とさらに学ぶために

疑問点やトラブルは、これらのコミュニティで質問するのも効果的です。多くの先人たちが同じ問題に直面し、解決方法を共有しています。

🗂️ 人気カテゴリ

記事数の多いカテゴリから探す