0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Gemini Live API(無料)を使って、会話型AIロボットを自作するための基礎知識

0
Last updated at Posted at 2026-10-02

きっかけ

私の携帯はAndroidなんですが、いつの間にかGeminiが入ってて、
ホームボタンを長押しすると、Geminiが起動され、声で何か質問すると、音声で答えてくれます。
とても便利な世の中になったもんだと感心しました。
これをマイコンで再現したくなってしまったのです。

Geminiと会話しながら、動くロボットを作りたい

4年ぐらい前にM5stackを使った「スタックちゃん」が流行ってて、私も作って遊んでいました。
当時は確か、Whisper + ChatGPT + VOICEVOXを使って会話してたと思います。
これは、まずマイク音声をテキストに変換してもらい、そのテキストをAIに送って、帰ってきたテキストを音声合成してスピーカーから出す流れでした。
これだと、返事の音声が帰って来るまで結構時間がかかってました。

でも GoogleのGemini Liveを使えば直接音声を送って、音声で帰って来るので、返事が早いんです。
しかも無料で使える枠が結構あります。
これは、いい環境が整ったと思い、作りたくなりました。

色々調べていたら、もうすでに作られてました。
AI_StackChan_Ex です。参考にさせていただきます。
これを参考にカスタマズするにはどうしたらいいのか、プログラムの量が多すぎて難しそう。
そこで、機能をGeminiとのやり取りのところだけを抜き出して、加工しやすい状態にしてみたので、
これから、自作して見たい方の参考になればと思います。

マイコンの用意

使うマイコンは、M5Stack ATOMS3R + Atomic Voise Base(旧名Atomic Echo Base)です。

バラで買うより、キットで買ったほうが、ちょっと安いですね。
SWITCH SCIENCEで購入できます。
それと、もし別に買うときは、AtomS3R(PSRAMあり)とAtomS3(PSRAMなし)は別物なので、必ずAtomS3Rのほう買ってください。Gemini Liveの音声をストリーミングするときに結構メモリーを必要としますので。

開発環境は、Vscode+PlatformIOです。(Aruduino IDE はコンパイルが遅いし、使いずらいので、もう戻れません。)

Gemini API キーの取得

GoogleのGemini Live を使うためには、APIキー(無料)が必要です。
まずはGoogleアカウントを作ってください。あとはGoogleアカウントにログインした状態で、Google AI Studio 公式サイト にアクセスします。
APIの取得の方法は、次のサイトが大変わかりやすいです。
日本AIコンサルティング を参考に、取得して下さい。

Gemini Liveを無料で使える枠が結構あります

RPM(Requests Per Minute)は、1分当たりのリクエスト数(APIを呼び出せる回数)

TPM(Tokens Per Minute)は、1分当たりのトークン数(処理できる文字やデータの総数)

RPD(Requests Per Day)は、1日当たりのリクエスト数

バージョンによってTPMの量が違いますが、その他は無制限で使えそうです。すごくないですか?
今回は、一番容量の多い Gemini 2.5 Flash Native Audio Dialog を使おうと思います。

Gemini Live とのやり取りはどうするの?

Geminiとのやり取りは、Websocketという通信方法をとります。
一度Websocketで接続した後は、繋ぎっぱなしになり送受信できます。
文字のやり取りも、音声のやり取りも、Jsonドキュメントをバイナリ形式で行います。
音声の場合は、Base64形式の文字列に変換して行います。

Websocketで接続した後の流れはつぎのようになります。

マイコン ーーーー "setup"メッセージ ーーーーーーー> Gemini Live
     <ーーー "setupComplete"メッセージ ーーー

"setup"メッセージの中には、Jsonドキュメントのフォーマットに従って、
Gemini APIのモデル名や、声の種類を指定したり、Geminiの性格や、して欲しいことのリストを指定します。
Geminiの話す声は、voiceName に "Puck"(男), "Charon"(男), "Kore"(女), "Fenrir"(男), "Aoede"(女) などの定義された音声名を指定できます。
最低限必要なのは、以下のようなメッセージです。

{
  "setup": {
    "model": "models/gemini-2.5-flash-native-audio-latest",
    "generationConfig": {
      "responseModalities": ["AUDIO"],
      "speechConfig": {
        "voiceConfig": {
          "prebuiltVoiceConfig": {
            "voiceName": "Aoede"
          }
        }
      }			
    },
    "systemInstruction": {
      "parts": [
        {"text": "あなたはAIアシスタントで、名前は「ジェミニ」です。どーぞ日本語で話してください。"}
	  ],
      "role": "user"
    }
  }
}

さらに、関数宣言を追加することが出来ます。"tootls":[{"functionDeclarereatins":[{}]
"現在のあなたの感情に合わせて、アバターの表情を変更してください。"
とか、"ユーザーが家電(電気やエアコンなど)の操作(つける・消す)を求めたときに呼び出して下さい"を指定しておくと、それに適した関数呼び出しが来るようになります。
それも追加すると以下のようになります。

{
  "setup": {
    "model": "models/gemini-2.5-flash-native-audio-latest",
    "generationConfig": {
      "responseModalities": ["AUDIO"],
      "speechConfig": {
        "voiceConfig": {
          "prebuiltVoiceConfig": {
            "voiceName": "Aoede"
          }
        }
      }			
    },
    "tools": [
      {
        "functionDeclarations": [
          {
            "name": "set_avatar_expression",
            "description": "現在のあなたの感情に合わせて、アバターの表情を変更してください。",
            "parameters": {
              "type": "object",
              "properties": {
                "expression": {
                  "type": "string",
                  "description": "設定する表情です。",
                  "enum": [
                    "neutral",
                    "happy",
                    "angry",
                    "sad",
                    "doubt",
                    "sleepy"
                  ]
                }
              },
              "required": [
                "expression"
              ]
            }
          },
		  {
            "name": "control_home_appliance",
            "description": "ユーザーが家電(電気やエアコンなど)の操作(つける・消す)を求めたときに呼び出します。",
            "parameters": {
              "type": "OBJECT",
              "properties": {
                "device": {
                  "type": "STRING",
                  "enum": ["light", "aircon"],
                  "description": "操作対象の家電。電気なら'light'、エアコンなら'aircon'。"
                },
                "action": {
                  "type": "STRING",
                  "enum": ["on", "off"],
                  "description": "家電に対する操作。電源を入れる/つけるなら'on'、電源を切る/消すなら'off'。"
                }
              },
              "required": ["device", "action"]
            }
          }
        ]
      }
    ],
    "systemInstruction": {
      "parts": [
        {"text": "あなたはAIアシスタントで、名前は「ジェミニ」です。どーぞ日本語で話してください。"},
        {"text": "会話の感情的なトーンに合わせてアバターの表情を変化させるため、`set_avatar_expression` ツールを積極的に活用してください。自身の感情が変化した際や、ユーザーの感情から適切な反応が示唆される場面でこのツールを呼び出します。表情の指定には、neutral、happy、angry、sad、doubt、sleepy のいずれかを使用してください。"},
        {"text": "時間は日本時間を使用してください。"},
		{"text": "ユーザーが家電(電気やエアコンなど)の操作(つける・消す)を求めたときは、`control_home_appliance` ツールを呼び出してください。"}
	 ],
      "role": "user"
    }
  }
}

"setup"メッセージが無事にGeminiに届くと、"setupComplete"メッセージが送られてきます。

その後は、音声でのやり取りや、メッセージのやり取りを行えるようになります。

音声を送る場合は、細かいチャンクに区切って送ります。
例えば、1024ワード分のデータをrecoadしたら、
"realtimeInput"という名前のJsonドキュメントの中の"data"のところに、
Base64にエンコードした音声データを入れて送ります。
話終わるまでこれを繰り返し送ります。

{
  "realtimeInput": {
    "audio":{
      "mime_type": "audio/pcm;rate=16000",
      "data": "REPLACE_TO_AUDIO_BASE64"
    }
  }
}

こちらが、話終わったら、Geminiからの返事を待ちます。
音声データは、"serverContent"という名前のJsonドキュメントの中の"data"にBase64形式で送られてきます。
これも細かいチャンクで繰り返し送られてくるので、その都度スピーカーから再生します。

{
  "serverContent": {
    "modelTurn": {
      "parts": [
        {
          "inlineData": {
            "mimeType": "audio/pcm;rate=24000",
            "data": ""
          }
        }
      ]
    }
  }
}

あと、"toolCall"と言う名前のJsonメッセージが送られて来ることがあります。
その中身の"name"や"args"を解析して、それにそった行動をすることができます。

{
  "toolCall": {
    "functionCalls": [
      {
        "name": "",
        "args": {
        },
        "id": ""
      }
    ]
  }
}

例えば、アバターの表情をhappyに変えるときは、次のようなJsonメッセージが来ます。

{
  "toolCall": {
    "functionCalls": [
      {
        "name": "set_avatar_expression",
        "args": {
          "expression": "happy"
        },
        "id": ""
      }
    ]
  }
}

これは、最初に"setup"の中で指定したことによって、呼ばれるようになります。
nameの文字列で、どのファンクションを呼べばいいのか判断します。
これで、ロボットを声で操作できるようになるんです。

プログラムを作りましょう

PlatfomIOで新規のプロジェクトを作って下さい。
ボードは、[m5stack-atoms3]です。
言語は、Arduinoです。
platfomiio.iniの中身を次の通りにして下さい。

platformio.ini
[env:m5stack-atoms3r]
platform = espressif32@6.11.0
board = m5stack-atoms3
framework = arduino
upload_speed = 1500000
monitor_speed = 115200
;monitor_filters = default
board_build.filesystem = spiffs
board_build.partitions = no_ota.csv
; PSRAM 有効化
board_build.arduino.memory_type = qio_opi  ; ← PSRAM(8MB)を使う指定
board_build.flash_mode         = qio
board_build.psram_type         = opi
build_flags = 
  -DESP32S3
  -DARDUINO_M5STACK_ATOMS3R
  -DARDUINO_USB_CDC_ON_BOOT=1
  -DARDUINO_USB_MODE=1
  -DBOARD_HAS_PSRAM							; ← PSRAM有効化フラグ
  -mfix-esp32-psram-cache-issue
  -DREALTIME_API
  -DCONFIG_MBEDTLS_DYNAMIC_BUFFER=1	; ← SSL通信のメモリ(RAM)消費を抑える設定 
  -DCORE_DEBUG_LEVEL=3
  -D_WS_CONFIG_H_=1							; WebSockets2_Genericの内部エラー回避フラグ
  -DWEBSOCKETS_USE_INCLUDE=1				; ESP32用の設定を有効化
; -DENABLE_WAKEWORD							; ウェイクワードは未対応
  -DARDUINOJSON_DEFAULT_NESTING_LIMIT=100
  -DARDUINOJSON_STRING_LENGTH_SIZE=4	;デフォルトの2(バイト)だと扱えるJSONの文字数が64KBまでとなるが、RealtimeAPIが返してくる音声データは64KBを超えることがあるため、サイズ上限を拡張
lib_deps = 
  m5stack/M5Unified ; @ 0.2.24
; earlephilhower/ESP8266Audio @ ^1.9.7
  bblanchon/ArduinoJson @ ^7
  arduino-libraries/ArduinoBLE@^1.3.4
  meganetaaan/M5Stack-Avatar@^0.10.0
  ESP32WebServer
; links2004/WebSockets   ; ←こちらのWebSocketは、何故か通信が切れるので不採用
  https://github.com/ronron-gh/arduinoWebSockets.git#fix/max_data_size
lib_ldf_mode = deep

main.cppに以下をコピペしてください。右上にコピーアイコンがあります。

main.cpp
#include <Arduino.h>
#include <M5Unified.h>
#include <esp_heap_caps.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <WebSocketsClient.h>
#include <time.h>
#include <Wire.h>
#include <base64.h>
#include <ArduinoJson.h>
#include <HTTPClient.h> 
#include <SPIFFS.h>

//#define USE_AVATAR			// もし表示にアバターを使いたい場合は、左のコメントアウトを外してください。

#ifdef USE_AVATAR
#include <Avatar.h>
#endif

// Wi-FiとGemini APIの設定(ご自身の環境に合わせて書き換えてください)
const char* ssid		= "あなたのWifiの名前をここに入力";		// 使用するWiFiルーター名を指定してください 
const char* password	= "あなたのWifiのパスワードをここに入力";	// WiFiのパスワードを設定してください
const String api_key	= "あなたの取得したAPIキー";              // ご自身のGoogle AI StudioのAPIキーを記入してください

// Google Geminiのパス
const char* ws_host	= "generativelanguage.googleapis.com";
const char* ws_url	= "/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent";
const int ws_port	= 443;

// EchoBaseの機能に割り当てられたピンをATOM_S3R用にマッピングします。
static constexpr const uint8_t PIN_I2S_BCLK = 8;  // Bit Clock
static constexpr const uint8_t PIN_I2S_WS   = 6;  // Word Select (LRCK)
static constexpr const uint8_t PIN_I2S_DOUT = 5;  // Data Out (Speaker)
static constexpr const uint8_t PIN_I2S_DIN  = 7;  // Data In (Mic)
static constexpr const uint8_t PIN_I2C_SDA  = 38;
static constexpr const uint8_t PIN_I2C_SCL  = 39;

WebSocketsClient webSocket;					// Websocketクライアントのインスタンスを作成

#ifdef USE_AVATAR
	using namespace m5avatar;
	Avatar avatar;
#endif

#define MIC_BUFF_WORDS 1024					// 録音バッファサイズ(16bit数)
int16_t mic_buffer[MIC_BUFF_WORDS];	// 録音バッファ

#define SPEAKER_VOLUME 100					// スピーカーのボリューム
#define STREAM_BUFF_SIZE (1024*100)	// ストリーミング再生用バッファサイズ
uint8_t* stream_buff[2];					// Base64をデコードして得た音声データを格納するバッファ。再生直後に更新すると音が切れたのでダブルバッファとした
volatile int nextBufIdx = 0;				// 次回データを格納するダブルバッファの面(0 or 1)


// 自動録音の判定パラメータ
const int THRES_VOLUME = 200;			// 録音を開始する音量のしきい値(環境に合わせて調整)
const int SILENCE_TIMEOUT_MS = 1200;	// 音が鳴り止んでから録音を終了するまでの時間(ミリ秒)
const int MAX_RECORD_TIME_SEC = 10;		// 最大録音時間(秒)
#define MIC_METER_MAX 1000				// 録音中の音量メーターの最大値(環境に合わせて調整)
#define SPEAKER_METER_MAX 32767 		// スピーカー再生中の音量メーターの最大値(環境に合わせて調整)

// 状態管理
volatile bool is_connected = false;		// WebSocketが接続されているかどうかのフラグ

enum State { STATE_IDLE, STATE_RECORDING, STATE_SENDING, STATE_RECEIVING, STATE_PLAYBACK };
static volatile State currentState = STATE_IDLE;	// 現在の状態を管理する変数

enum I2Smode { MODE_MIC, MODE_SPEAKER};
static volatile I2Smode i2Smode = MODE_MIC;			// 現在のI2Sモードを管理する変数

uint32_t last_sound_time = 0;						// 録音時間管理用タイマー
const uint32_t RESEIVE_WAIT_TIME_LIMIT = 10*1000;	// 受信待ちタイムアウト時間 10秒
unsigned long reseive_wait_time = 0;				// 返答受信待ちタイマー

// Google Gemini の 証明書
static const char* root_ca_google_gemini = R"(
-----BEGIN CERTIFICATE-----
MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw
CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU
MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw
MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp
Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEBAQUA
A4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaMf/vo
27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7w
Cl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjw
TcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0Pfybl
qAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaH
szVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4Zor8
Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUspzBmk
MiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92
wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70p
aDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrN
VjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQID
AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E
FgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBAJ+qQibb
C5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmmWJyq+bXmYOfg6LEe
QkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuy
h6f88/qBVRRiClmpIgUxPoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM4
7HLwEXWdyzRSjeZ2axfG34arJ45JK3VmgRAhpuo+9K4l/3wV3s6MJT/KYnAK9y8J
ZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYciNuaCp+0KueIHoI17eko8cdLiA6Ef
MgfdG+RCzgwARWGAtQsgWSl4vflVy2PFPEz0tv/bal8xa5meLMFrUKTX5hgUvYU/
Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJFfbdT
6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ
0E6yove+7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm
2tIMPNuzjsmhDYAPexZ3FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bb
bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIICCTCCAY6gAwIBAgINAgPlwGjvYxqccpBQUjAKBggqhkjOPQQDAzBHMQswCQYD
VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG
A1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw
WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz
IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjOPQIBBgUrgQQAIgNi
AATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzuhXyi
QHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvR
HYqjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW
BBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNpADBmAjEA6ED/g94D
9J+uHXqnLrmvT/aDHQ4thQEd0dlq7A/Cr8deVl5c1RxYIigL9zC2L7F8AjEA8GE8
p/SgguMh1YQdc4acLa/KNJvxn7kjNuK8YAOdgLOaVsjh4rsUecrNIdSUtUlD
-----END CERTIFICATE-----
)";

// PSRAM上にJSONデータを置くためのアロケーター
struct SpiRamAllocator : ArduinoJson::Allocator {
	void* allocate(size_t size) override {
		return ps_malloc(size);
	}
	void deallocate(void* ptr) override {
		free(ptr);
	}
	void* reallocate(void* ptr, size_t new_size) {
		return ps_realloc(ptr, new_size);
	}
};
static SpiRamAllocator allocator;
JsonDocument msgDoc(&allocator);	// Websocketメッセージを入れるPSRAM上のJSONドキュメント

// 外部ライブラリに依存しない安全な自前Base64デコード関数
size_t customBase64Decode(uint8_t* output, const char* input, size_t inputLength) {
	static const int lookup[] = {
		-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
		-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,62,-1,-1,-1,63,52,53,54,55,56,57,58,59,60,61,-1,-1,-1,-1,-1,-1,
		-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1,-1,
		-1,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,-1,-1,-1,-1,-1
	};  
	size_t outLen = 0;
	uint32_t buffer = 0;
	int bitsLeft = 0;

	for (size_t i = 0; i < inputLength; i++) {
		char c = input[i];
		if (c == '=') break;
		if (c < 0 || c > 127) continue;

		int val = lookup[(int)c];
		if (val == -1) continue;

		buffer = (buffer << 6) | val;
		bitsLeft += 6;
		if (bitsLeft >= 8) {
			bitsLeft -= 8;
			output[outLen++] = (buffer >> bitsLeft) & 0xFF;
		}
	}
	return outLen;
}

// NTPによる時刻同期関数
void syncTime() {
	Serial.print("Initializing NTP Sync ");
	// 日本標準時(JST-9)として、複数の信頼できるNTPサーバーを指定
	configTime(9 * 3600, 0, "://google.com", "pool.ntp.org", "ntp.nict.jp");  
	// 最大15秒間、NTPでの同期を試みる
	int retry = 0;
	while (time(nullptr) < 1000000000l && retry < 30) {
		delay(500);
		Serial.print(".");
		retry++;
	}  
	// 【超強力なバックアップ】NTP(UDP)がネットワークで遮断されている場合
	// Googleに普通のHTTPリクエストを送り、レスポンスヘッダーの「Date:」から現在時刻を強制取得する
	if (time(nullptr) < 1000000000l) {
		Serial.println("\nNTP failed. Trying HTTP Time Sync backup...");
		HTTPClient http;
		http.begin("http://google.com"); // SSL不要の普通のHTTP
		
		const char* headerKeys[] = {"Date"};
		http.collectHeaders(headerKeys, 1);    
		int httpCode = http.GET();
		if (httpCode > 0) {
			String dateStr = http.header("Date"); // 例: "Sun, 20 Sep 2026 01:23:45 GMT"
			Serial.print("HTTP Server Date: ");
			Serial.println(dateStr);      
			// HTTPヘッダーの文字列から秒数を解析してESP32のシステム時計に直接セット
			struct tm tm;
			if (strptime(dateStr.c_str(), "%a, %d %b %Y %H:%M:%S GMT", &tm)) {
				time_t t = mktime(&tm);
				struct timeval tv = {.tv_sec = t};
				settimeofday(&tv, NULL);
				Serial.println("Time alternative sync success via HTTP!");
			}
		}
		http.end();
	}
	// 最終結果の表示確認
	time_t now = time(nullptr);
	struct tm timeinfo;
	localtime_r(&now, &timeinfo); // 日本時間に変換して表示
	Serial.print("\n[Current Time (JST)]: ");
	Serial.println(asctime(&timeinfo));
}

// LCDに状態を表示する
void updateDisplay(State state, const char* extraMsg = "") {
#ifndef USE_AVATAR 
	M5.Lcd.clear();
	M5.Lcd.setCursor(0, 20);
	M5.Lcd.setTextSize(1);
#endif

	switch (state) {
		case STATE_IDLE:
			Serial.println("【 待機中 】");
		#ifndef USE_AVATAR
			M5.Lcd.setTextColor(GREEN); M5.Lcd.println("【 待機中 】");
		#else
			avatar.setSpeechText("");
			avatar.setExpression(Expression::Neutral);
		#endif
			break;

		case STATE_RECORDING:
			Serial.println("● 録音中...");
		#ifndef USE_AVATAR
			M5.Lcd.setTextColor(RED); M5.Lcd.println("● 録音中...");
		#else	
			avatar.setSpeechText("聞いてるよ");
			avatar.setExpression(Expression::Happy);
		#endif
			break;
		
		case STATE_SENDING:
			Serial.println("▲ 送信中...");
		#ifndef USE_AVATAR
			M5.Lcd.setTextColor(BLUE); M5.Lcd.println("▲ 送信中...");
			if (strlen(extraMsg) > 0) {
				M5.Lcd.setTextColor(WHITE); M5.Lcd.println(extraMsg);
			}
		#else	
			avatar.setSpeechText("送信中...");
			avatar.setExpression(Expression::Neutral);
		#endif	
			break;

			case STATE_RECEIVING:
				Serial.println("▼ 返答待ち...");			
			#ifndef USE_AVATAR
				M5.Lcd.setTextColor(MAGENTA); M5.Lcd.println("▼ 返答待ち...");
				if (strlen(extraMsg) > 0) {
				M5.Lcd.setTextColor(WHITE); M5.Lcd.println(extraMsg);
				}
			#else
				avatar.setSpeechText("えーっと...");
				avatar.setExpression(Expression::Doubt);
			#endif
			break;

		case STATE_PLAYBACK:
			Serial.println("▶ 再生中...");
		#ifndef USE_AVATAR
			M5.Lcd.setTextColor(CYAN); M5.Lcd.println("▶ 再生中...");
		#else
			avatar.setSpeechText("");
			avatar.setExpression(Expression::Neutral);
		#endif
			break;
	}
	currentState = state;
}
// 音量をグラフ表示する
void dispVolume(uint16_t volume, uint16_t max_vol) {
#ifndef USE_AVATAR	
	uint16_t barWidth = map(volume, 0, max_vol, 0, 128);		// 音量をバーの幅にマッピング
	M5.Lcd.fillRect(0, 70, 200, 10, BLACK);						// 前のバーを消去
	M5.Lcd.fillRect(0, 70, barWidth, 10, GREEN);				// 音量バーを描画

	M5.Lcd.setCursor(0, 50);
	M5.Lcd.setTextColor(YELLOW, BLACK);
	M5.Lcd.printf("Vol:%5d", volume);							// 音量を数値で描画
#endif
}

#ifdef USE_AVATAR
int get_vol_average(int16_t* buffer, int length) {	// 再生バッファの音量の平均値を計算する
	long sum = 0;
	for (int i = 0; i < length; i++) {
		sum += abs(buffer[i]);
	}
	return sum / length;
}
void lipSync(void *args){		// Avatarの口の大きさを、スピーカーの音に連動させる。
	float gazeX, gazeY;
	// FreeRTOSのタスクは絶対に終了させてはいけないため、無限ループにする
	while (1) {
		int level = get_vol_average((int16_t*)stream_buff[nextBufIdx ^ 1], 50);
 		if(level > 5000) level = 5000;
		float open = (float)level/5000.0;
  	    avatar.setMouthOpenRatio(open);
  	    avatar.getGaze(&gazeY, &gazeX);
  	    avatar.setRotation(gazeX * 5);
        // 【重要】他のタスク(描画など)にCPUの処理権限を譲るために、必ずディレイを入れる
        delay(100);
	}
}
#endif
// ----------------------------------------------------
//  Gemini Live API用 JSONフォーマット
// ----------------------------------------------------
// 初期セットアップメッセージ
// voiceName に "Puck"(男), "Charon"(男), "Kore"(女), "Fenrir"(男), "Aoede"(女) などの定義された音声名を指定できます。
static const char* setupMessage = R"(
{
  "setup": {
    "model": "models/gemini-2.5-flash-native-audio-latest",
    "generationConfig": {
      "responseModalities": ["AUDIO"],
      "speechConfig": {
        "voiceConfig": {
          "prebuiltVoiceConfig": {
            "voiceName": "Aoede"
          }
        }
      }			
    },
    "tools": [
      {
        "functionDeclarations": [
          {
            "name": "set_avatar_expression",
            "description": "現在のあなたの感情に合わせて、アバターの表情を変更してください。",
            "parameters": {
              "type": "object",
              "properties": {
                "expression": {
                  "type": "string",
                  "description": "設定する表情です。",
                  "enum": [
                    "neutral",
                    "happy",
                    "angry",
                    "sad",
                    "doubt",
                    "sleepy"
                  ]
                }
              },
              "required": [
                "expression"
              ]
            }
          },
					{
            "name": "control_home_appliance",
            "description": "ユーザーが家電(電気やエアコンなど)の操作(つける・消す)を求めたときに呼び出します。",
            "parameters": {
              "type": "OBJECT",
              "properties": {
                "device": {
                  "type": "STRING",
                  "enum": ["light", "aircon"],
                  "description": "操作対象の家電。電気なら'light'、エアコンなら'aircon'。"
                },
                "action": {
                  "type": "STRING",
                  "enum": ["on", "off"],
                  "description": "家電に対する操作。電源を入れる/つけるなら'on'、電源を切る/消すなら'off'。"
                }
              },
              "required": ["device", "action"]
            }
          }
        ]
      }
    ],
    "systemInstruction": {
      "parts": [
        {"text": "あなたはAIアシスタントで、名前は「ジェミニ」です。どーぞ日本語で話してください。"},
        {"text": "会話の感情的なトーンに合わせてアバターの表情を変化させるため、`set_avatar_expression` ツールを積極的に活用してください。自身の感情が変化した際や、ユーザーの感情から適切な反応が示唆される場面でこのツールを呼び出します。表情の指定には、neutral、happy、angry、sad、doubt、sleepy のいずれかを使用してください。"},
        {"text": "時間は日本時間を使用してください。"},
				{"text": "ユーザーが家電(電気やエアコンなど)の操作(つける・消す)を求めたときは、`control_home_appliance` ツールを呼び出してください。"}
			],
      "role": "user"
    }
  }
}
)";

// 音声を送るときのJSONフォーマット
static const char*  realtime_input = R"(
{
  "realtimeInput": {
    "audio":{
      "mime_type": "audio/pcm;rate=16000",
      "data": "REPLACE_TO_AUDIO_BASE64"
    }
  }
}
)";

// サーバーから送られてきた音声データのJSONフォーマット
static const char* serverContent = R"(
{
  "serverContent": {
    "modelTurn": {
      "parts": [
        {
          "inlineData": {
            "mimeType": "audio/pcm;rate=24000",
            "data": ""
          }
        }
      ]
    }
  }
}
)";

// Geminiからファンクションコールが来た時のJSONフォーマット
static const char* function_call = R"(
{
  "toolCall": {
    "functionCalls": [
      {
        "name": "set_avatar_expression",
        "args": {
          "expression": ""
        },
        "id": ""
      }
    ]
  }
}
)";

// ファンクションコールが来た後にレスポンスを返す時のJSONフォーマット
static const char* function_response = R"(
{
  "tool_response": {
    "function_responses": [{
      "name": "REPLACE_TO_TOOL_NAME",
      "response": {"result":"REPLACE_TO_OUTPUT"},
      "id": "REPLACE_TO_CALL_ID"
    }]
  }
}
)";

// マイクとスピーカーを初期化する
void init_mic_spk(){
	M5.In_I2C.begin(I2C_NUM_1, M5.In_I2C.getSDA(), M5.In_I2C.getSCL());	// 背面ポートのI2C
//	Wire.begin(PIN_I2C_SDA, PIN_I2C_SCL, 100000U);
	auto spk_cfg = M5.Speaker.config();
	spk_cfg.sample_rate = 24000;	// サンプリングレート 24000Hz
	spk_cfg.task_pinned_core = APP_CPU_NUM;
/*
	spk_cfg.pin_data_out	= PIN_I2S_DOUT;
	spk_cfg.pin_bck			= PIN_I2S_BCLK;
	spk_cfg.pin_ws 			= PIN_I2S_WS;
*/
	M5.Speaker.config(spk_cfg);
	M5.Speaker.setVolume(SPEAKER_VOLUME);

	auto mic_cfg = M5.Mic.config();
	mic_cfg.sample_rate = 16000;	// サンプリングレート 16000Hz
	mic_cfg.stereo      = false;
/*
	mic_cfg.pin_data_in = PIN_I2S_DIN;
	mic_cfg.pin_bck		= PIN_I2S_BCLK;
	mic_cfg.pin_ws 		= PIN_I2S_WS;
  mic_cfg.use_adc       = false;
*/
	M5.Mic.config(mic_cfg);
	Serial.println("マイクON");
	M5.Mic.begin();
	i2Smode = MODE_MIC;	
}
// スピーカに切り替え
void sw_speaker(){	
	while (M5.Mic.isRecording()) { vTaskDelay(1); }
	Serial.println("マイク終了");
	M5.Mic.end();
	Serial.println("スピーカー開始");	
	M5.Speaker.begin();
	i2Smode = MODE_SPEAKER;
}
// マイクに切り替え
void sw_mic(){			
	while (M5.Speaker.isPlaying()) { vTaskDelay(1); }
	Serial.println("スピーカー終了");
	M5.Speaker.end();
	for(int i=0; i<2; i++){
		memset(stream_buff[i], 0, STREAM_BUFF_SIZE);
	}
	Serial.println("マイク開始");
	M5.Mic.begin();
	i2Smode = MODE_MIC;
}
// Wi-Fi 接続
void connect_wifi(){	
	M5.Lcd.setCursor(0, 0);
	M5.Lcd.print("WiFi Connecting");	
	WiFi.begin(ssid, password);
	while (WiFi.status() != WL_CONNECTED) {
		M5.Lcd.print(".");
		delay(500);
	}
	if (WiFi.status() == WL_CONNECTED) {
		Serial.println("\n[Wi-Fi] Connected successfully!");
		Serial.printf("[Wi-Fi] ESP32 IP Address: %s\n", WiFi.localIP().toString().c_str());
		M5.Lcd.setCursor(0, 0);
		M5.Lcd.printf("WiFi Connected! IP:\n%s", WiFi.localIP().toString().c_str());	
	} else {
		Serial.println("\n[Wi-Fi] Connection Failed! Check SSID/Password.");
		return;
	}
}
// Geminiにセットアップメッセージを送信する
void send_setup() {									
	String jsonPayload(setupMessage);	// charをStringに変換	
	webSocket.sendTXT(jsonPayload);		// Websocketでセットアップメッセージを送信	
	Serial.printf("[Gemini] Setup message sent.\n%s\n", jsonPayload.c_str());
}
// 音声データをWebsocketで送る
void send_chank(int16_t* audio_buff, int bytes){
	if(is_connected){
		String base64Audio = base64::encode((uint8_t*)audio_buff, bytes);	// Base64にエンコード

		String jsonPayload(realtime_input);																// 音声送信用のJsonコマンド読み込み
		jsonPayload.replace("REPLACE_TO_AUDIO_BASE64", base64Audio);		// 音声データを再設定
		
		Serial.printf("%s\nsize=%d\n", jsonPayload.c_str(), jsonPayload.length());
		webSocket.sendTXT(jsonPayload);																		// 音声データをWebsocketで送信
	}
}
// 音声をストリーム再生
void streamAudioDelta(String& delta)
{
	int base64Size = delta.length();
	uint8_t* buf = stream_buff[nextBufIdx];
	int len = customBase64Decode(buf, delta.c_str(), base64Size);	// Base64データを解凍

	while (M5.Speaker.isPlaying()){ vTaskDelay(1); }				// スピーカー終了待ち
	M5.Speaker.playRaw((int16_t*)buf, len/2, 24000, false, 1);		// スピーカーで鳴らす
	uint16_t vol = abs(*(int16_t *)buf);
	dispVolume(vol, SPEAKER_METER_MAX);
	nextBufIdx ^= 1;	//ダブルバッファを切り替え
}
// 表情を描画
void change_expression(String& expressionName){
		Serial.printf("expresssion = %s\n", expressionName);
#ifndef USE_AVATAR		
		M5.Lcd.setCursor(0,100);
		M5.Lcd.setTextColor(YELLOW);
		M5.Lcd.printf("表情は %s", expressionName);
#else
		Expression avatarExpression = Expression::Neutral;
		if(expressionName == "neutral"){
    	avatarExpression = Expression::Neutral;
  	}
  	else if(expressionName == "happy"){
    	avatarExpression = Expression::Happy;
  	}
  	else if(expressionName == "angry"){
    	avatarExpression = Expression::Angry;
  	}
  	else if(expressionName == "sad"){
    	avatarExpression = Expression::Sad;
  	}
  	else if(expressionName == "doubt"){
    	avatarExpression = Expression::Doubt;
  	}
  	else if(expressionName == "sleepy"){
    	avatarExpression = Expression::Sleepy;
  	}
		avatar.setExpression(avatarExpression);
#endif
}

// 家電を制御する実際の処理
String controlAppliance(String& device, String& action) {
	String response = "";
	Serial.printf("\n【システム命令受信】 対象家電: %s, 操作: %s\n", device, action);
    
	if (device == "light") {
		if (action == "on") {
			response = "-> [実行] 部屋の電気をつけます。";
			// todo
		}else if (action == "off"){
			response = "-> [実行] 部屋の電気を消します。";
			// todo
		}
	}else if (device == "aircon") {
		if (action == "on"){
			response = "-> [実行] エアコンの電源を入れます。";
			// todo
		}else if (action == "off") {
			response = "-> [実行] エアコンの電源を切ります。";
			// todo
		}
	}
	Serial.printf("%s/n", response);
	return response;
}

// Websocketからのファンクションコール処理
String exec_calledFunc(String& name, String& args){		// Geminiからファンクション呼び出しが来た時の処理
	String response = "";
	JsonDocument argsDoc;

 	DeserializationError error = deserializeJson(argsDoc, args);	// argsをJsonデータに解凍
  if (error) {
    Serial.print(F("deserializeJson(arguments) failed: "));
    Serial.println(error.f_str());
		response = "エラーです";
		delay(1000);
	}

	if(name == "set_avatar_expression"){							// リクエスト名が、アバターの表情設定のとき
		String expressionName = argsDoc["expression"].as<String>();	// 表情を得る
		change_expression(expressionName);
		response = "アバターの表情を" + expressionName + "に変えました。";
	}
	else if(name == "control_home_appliance"){						// 家電の制御のとき
		String device = argsDoc["device"].as<String>();
		String action = argsDoc["action"].as<String>();
		response = controlAppliance(device, action);				// それぞれの処理を行う
	}

	return response;
}

// WebSocketのイベントハンドラ(Geminiからの返答を受け取る用)
void webSocketEvent(WStype_t type, uint8_t * payload, size_t length) {
	String delta;
	DeserializationError error;

	switch(type) {
		case WStype_DISCONNECTED:								// Websocketが切断された
			Serial.printf("[WS] Disconnected! Payload (Reason Code): %d\n", (payload != NULL) ? *payload : -1);
			is_connected = false;
			updateDisplay(STATE_IDLE);		// リスニングモードにする
			if(i2Smode == MODE_SPEAKER){
				sw_mic();		// マイクに切り替え
			}
			break;
		case WStype_CONNECTED:									// Websockt接続成功
			Serial.println("[WS] Websocket connected! ");
			is_connected = true;
			send_setup();										// 接続完了直後に初期セットアップを送信
			break;
		case WStype_TEXT:										// テキストデータが送られてきた
			Serial.printf("[WSc] get text: %s\n", payload);
			break;
		case WStype_BIN:										// バイナリーデータが送られてきた
			Serial.printf("[WSc] get binary length: %u\npayload: %s\n", length, payload);
			error = deserializeJson(msgDoc, payload);//, length);	// 受信データをJson形式に変換
			if (error) {
				Serial.printf("WebSocket Event: JSON deserialization error %d\n", error.code());
				break;
			}
			if(!msgDoc["setupComplete"].isNull()){		// セットアップが成功すると"setupComplete"が来る
				Serial.println("[WSc] setupComplete");
			}
			else if(!msgDoc["serverContent"]["modelTurn"]["parts"][0]["text"].isNull()){				// 音声データの前に送られてくる
				Serial.println("[WSc] modelTurn parts text 受信");
			}
			else if(!msgDoc["serverContent"]["modelTurn"]["parts"][0]["inlineData"]["data"].isNull()){	// 音声データ受信
				if(i2Smode == MODE_MIC){
					updateDisplay(STATE_PLAYBACK);	// 音声再生モードにする
					sw_speaker();					// スピーカに切り替え
				}	
				delta = msgDoc["serverContent"]["modelTurn"]["parts"][0]["inlineData"]["data"].as<String>();	// 音声データ文字列を取りだす
				streamAudioDelta(delta);	// 音声データ再生
			}
			else if(!msgDoc["serverContent"]["turnComplete"].isNull()){	// モデルの生成完了通知
				Serial.println("[WSc] turnComplete");
				if(i2Smode == MODE_SPEAKER){
					sw_mic();	// マイクに切り替え
				}
				updateDisplay(STATE_IDLE);			// リスニングモードにする
			}
			else if(!msgDoc["toolCall"]["functionCalls"][0].isNull()){	// ファンクションコールが来た
				Serial.println("[WSc] toolCall:");
				String name = msgDoc["toolCall"]["functionCalls"][0]["name"].as<String>();
				String args = msgDoc["toolCall"]["functionCalls"][0]["args"].as<String>();
				String call_id = msgDoc["toolCall"]["functionCalls"][0]["id"].as<String>();
				Serial.printf("name: %s, args: %s, id: %s\n", name.c_str(), args.c_str(), call_id.c_str());

				String response = exec_calledFunc(name, args);	// ファンクションを実行
				response.replace("\"", "\\\"");     //JSON内の文字列を囲む"にエスケープ(\)を付ける

				String json(function_response);
				json.replace("REPLACE_TO_TOOL_NAME", name.c_str());
				json.replace("REPLACE_TO_CALL_ID", call_id.c_str());
				json.replace("REPLACE_TO_OUTPUT", response.c_str());
				Serial.printf("[WSc] function output: %s\n", json.c_str());
				webSocket.sendTXT(json);																				// ちゃんと受信したことを送信
			}
			break;
		case WStype_ERROR:
			Serial.printf("[WS] Error occurred! Payload : %s\n", payload);
			break;	
		case WStype_FRAGMENT_TEXT_START:
		case WStype_FRAGMENT_BIN_START:
		case WStype_FRAGMENT:
		case WStype_FRAGMENT_FIN:
			Serial.printf("[WSc] payload: %s\n", payload);		
			break;
		default:
			Serial.printf("[WSc] Unknown event\n");
			break;
	}
}

void connect_Websocket(){								// GeminiのWebsocketに接続する
	webSocket.beginSslWithCA(ws_host, ws_port, ws_url, root_ca_google_gemini);	// 証明書付きで、GeminiのWebsocketに接続
	webSocket.onEvent(webSocketEvent);																					// Websocket割り込みイベント先を指定
	String auth = "x-goog-api-key: " + api_key;	
	webSocket.setExtraHeaders(auth.c_str());																		// APIキーをセット
	Serial.printf("Connecting to Gemini Live API...\n");
}

void setup() {
	Serial.begin(115200);
	auto cfg = M5.config();
	cfg.internal_spk = false; 
	cfg.internal_mic = false;
	cfg.external_speaker.atomic_echo = true;	// 外部の [Atomic Echo Base] を使う宣言。これを指定するとPIN番号などが自動で割り当てられる。が、何故かbigen()やend()でI2Cエラーになるが無視する。
	cfg.serial_baudrate = 115200;  				// M5Unified 0.1.17からデフォルトが0になったため設定
	M5.begin(cfg);
  
	if(!SPIFFS.begin(true)){
    M5.Lcd.print("Failed to mount SPIFFS. System reset after 5 seconds.");
    delay(5000);
    ESP.restart();
  }

	M5.Lcd.setRotation(0); 				// LCDの回転
	M5.Lcd.setFont(&fonts::efontJA_16); // 日本語フォントを指定
	M5.Lcd.setTextSize(1);				// テキストサイズを指定

	for(int i=0; i<2; i++){	// ストリーミング音声再生用のダブルバッファをPSRAM上に確保
		stream_buff[i] = (uint8_t*)ps_malloc(STREAM_BUFF_SIZE);
		if(stream_buff[i] == nullptr){
			Serial.println("PSRAM allocation failed for audiobuff[]. Please check PSRAM settings.");
			while (1);			
		}
		memset(stream_buff[i], 0, STREAM_BUFF_SIZE);
	}
	nextBufIdx = 0;			// stream_buffのインデックスを0にする
	init_mic_spk();			// マイクとスピーカを初期化
	connect_wifi();			// Wifiに接続
	syncTime();				// 時刻同期
	is_connected = false;	// Websocket接続フラグをクリア
	connect_Websocket();	// GeminiのWebSocketに接続

#ifdef USE_AVATAR	
  avatar.setScale(0.4);
  avatar.setPosition(-56, -96);
  Face* face = avatar.getFace();
  face->setMouth(new Mouth(30, 90, 6, 60));	// Mouth(uint16_t minWidth, uint16_t maxWidth, uint16_t minHeight)
  avatar.init();
  avatar.addTask(lipSync, "lipSync", 2048);
  avatar.setSpeechFont(&fonts::efontJA_16);
#endif
  updateDisplay(STATE_IDLE);	// リスニングモードにする
}

void loop() {
	M5.update();
	webSocket.loop(); // WebSocketの通信維持

	if (i2Smode == MODE_MIC) {
		if(!M5.Mic.record(mic_buffer, MIC_BUFF_WORDS, 16000)){	// 録音する
			Serial.println("Mic.record() returns false");
			delay(1000);
		}
		uint32_t v,sum = 0;
		for (size_t i = 0; i < MIC_BUFF_WORDS; i++) {			// 音量レベルの計算(絶対値平均)
			v = abs(mic_buffer[i]);
			sum += v;
		}
		uint16_t current_volume = sum / MIC_BUFF_WORDS;	// 平均音量
		dispVolume(current_volume, MIC_METER_MAX);		// 音量レベルを表示
		switch (currentState) {
			case STATE_IDLE: 
				if (current_volume > THRES_VOLUME) {			// しきい値を超えたら「録音状態」に移行
					send_chank(mic_buffer, sizeof(mic_buffer));	// データをwebsocketで送る
					updateDisplay(STATE_RECORDING);				// 録音モードにする
					last_sound_time = millis();
				}
				break;

			case STATE_RECORDING:
				send_chank(mic_buffer, sizeof(mic_buffer));	    // データをwebsocketで送る  
				if (current_volume > THRES_VOLUME) {			// 音が鳴っている(しきい値を超えている)間は、最終発話時刻を更新
					last_sound_time = millis();
				}
				// ★【話の終わりの検出ロジック】
				// 最後に音を検知(最終発話時刻)してから、設定した無音時間(1.2秒)が経過したか判定
				if (millis() - last_sound_time > SILENCE_TIMEOUT_MS) {
					Serial.println("話し終わりを検出しました。録音終了。");
					updateDisplay(STATE_RECEIVING);		    // 受信待ちモードにする
					reseive_wait_time = millis();			// 返答受信待ちの開始時間
				} 
				break;

			case STATE_RECEIVING:
				if( millis() - reseive_wait_time  > RESEIVE_WAIT_TIME_LIMIT){
					Serial.println("受信待ちタイムアウト");
					updateDisplay(STATE_IDLE);				// リスニングモードにする
				}
				break;
		}
	}
	delay(1);
}

プログラムの最初のほうのWifiの設定と、APIキーの設定はご自身のものを書き込んで下さい。

// Wi-FiとGemini APIの設定(ご自身の環境に合わせて書き換えてください)
const char* ssid		= "あなたのWifiの名前をここに入力";		// 使用するWiFiルーター名を指定してください 
const char* password	= "あなたのWifiのパスワードをここに入力";	// WiFiのパスワードを設定してください
const String api_key	= "あなたの取得したAPIキー";              // ご自身のGoogle AI StudioのAPIキーを記入してください

これを、AtomS3Rに書き込んでください。
書き込むときは、AtomS3rの脇のリセットボタンを長押しして、緑のLEDが点灯したら書き込みモードになります。
書き込みが完了したら。リセットボタンを押して再起動してください。

これをたたき台にして、いろいろやってみて下さい。

声でロボット操作したり、何かわからないことを質問したりできるでしょう。
特定のワードだけに反応するのではなく、似たようなニュアンスの言葉で操作できるのが、AIの良いところなのかな?

おまけ

あと、プログラムの4行目のコメント//を外すと、スタックちゃん見たいなAbatarの顔表示にもなるように、仕込んで置きましたので、やってみて下さい。

#define USE_AVATAR			// もし表示にアバターを使いたい場合は、左のコメントアウトを外してください。

あと、m5stack/M5Unifiedライブラリーのせいだと思うのですが、
マイクとスピーカーの切り替えの時にエラーメッセージがでます。
Geminiに聞いて色々やって見ましたが解決できず、このエラーは見て見ぬ振りをすることにしました。動作には影響ありません。

今回プログラムを作るにあたり結構、Google Ai Geminiのお世話になりました。
自分で検索するより楽になったよ。

こんなの作っちゃいましたw

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?