ESP32 Bluetooth入門:Bluetooth ClassicとBLEの違い、LED制御とセンサー
ESP32のBluetoothガイド:BluetoothSerialでスマートフォンからLEDを操作し、サービスとUUIDを持つBLEサーバーを作り、nRF Connectでテストしてセンサーデータを送ります。
ESP32のBluetoothは、チップに内蔵された無線機能を使って、追加のモジュールなしでESP32をスマートフォンやタブレット、パソコンにつなぎます。無印ESP32は2種類のBluetoothに同時に対応しています。シリアルポートのように動作するBluetooth Classicと、消費電力がとても少ないBluetooth Low Energy(BLE)です。Arduino Unoでスマートフォンと通信するには別途HC-05モジュールが必要ですが、ESP32なら数行のコードで済みます。このガイドでは両方の方法をステップごとに設定し、スマートフォンからLEDを点灯し、自作のBLEサービスを書いて、センサーデータをスマートフォンに送ります。
どちらの方法を選ぶかは、スマートフォンとボードによって決まります。Androidスマートフォンと無印のESP32 DevKitなら、いちばん手早く始められるのはBluetoothSerialライブラリです。iPhoneを使う場合や、ボードがESP32-S3やESP32-C3の場合はBluetooth Classicは使えず、BLEが必要です。終わりの方では、同じBLEプロジェクトをRaspberry Pi Pico W向けのMicroPythonでも紹介します。ESP32を初めて使う場合は、先にESP32とは?でArduino IDEのセットアップを済ませてください。

ESP32のBluetoothとは?Bluetooth ClassicとBLEの違い
Bluetooth Classicは、ワイヤレスイヤホンやHC-05のようなモジュールで使われている、昔からおなじみの方式です。ESP32ではSPP(Serial Port Profile)で使います。スマートフォンとボードの間に見えないシリアルケーブルがあるかのように、バイトが次々に流れます。まずスマートフォンのBluetooth設定でデバイスをペアリングし、それからターミナルアプリで接続します。一方BLEは、2010年にBluetooth 4.0で導入された別の仕組みで、電池で何か月も動くデバイス向けに設計されています。BLEには連続したデータの流れがなく、ESP32は値の小さな表(GATT)を持ち、スマートフォンはその値を読んだり書いたり、変化したときに知らせてもらうよう登録(サブスクライブ)したりします。名前は似ていますが、この2つは互いに通信できません。BLE用に作られたアプリは、Bluetooth Classicのデバイスには接続できません。
| 項目 | Bluetooth Classic(SPP) | Bluetooth Low Energy(BLE) |
|---|---|---|
| Arduinoライブラリ | BluetoothSerial(ESP32パッケージに付属) | BLEDevice(ESP32パッケージに付属)またはNimBLE-Arduino |
| データモデル | シリアルポートのような連続したバイトの流れ | サービスとキャラクタリスティックからなるGATTテーブル |
| データ量 | 多い。連続したストリームに向く | 小さなパケット。標準では1回の通知につき20バイト |
| 消費電力 | 接続中はずっと高い | とても低い。電池で何か月も動ける |
| ペアリング | スマートフォンのBluetooth設定で行う | 不要。接続はアプリの中で行う |
| Android | 対応 | 対応 |
| iPhone(iOS) | 非対応 | 対応 |
| 対応するESP32 | 無印ESP32のみ(WROOM-32、WROVER) | ESP32、S3、C3、C6、H2(S2は非対応) |
| 主な用途 | シリアルターミナル、ロボットカー、HC-05の代わり | センサー、ウェアラブル、スマートホーム、ビーコン |
実用的な目安はこうです。Androidスマートフォンと文字を送受信してすぐに結果を見たいならClassic、iPhone対応や低消費電力、新しいESP32ボードが必要ならBLEを使います。新しいプロジェクトを始めるなら、BLEは覚えておく価値があります。どちらのスマートフォンでも動き、Bluetoothを持つEspressifの新しいチップにはすべて搭載されているからです。HC-05を使ったことがあれば、HC-05 Bluetoothモジュールのコマンドの考え方がBluetoothSerialでもまったく同じだとわかるはずです。
どのESP32ボードにどのBluetoothがある?
ESP32ファミリーのチップがすべて同じBluetoothハードウェアを持っているわけではありません。いちばん多い失敗は、ネットで見つけたBluetoothSerialのサンプルをESP32-S3やESP32-C3のボードに書き込もうとすることです。EspressifはBluetooth Classicを無印ESP32チップにだけ搭載し、その後のBluetooth付きチップはBLEだけに対応しています。ボードにどのチップが載っているかは、モジュールの金属シールドに書かれた文字(たとえばESP32-WROOM-32やESP32-S3-WROOM-1)でわかります。
| ボード / チップ | Bluetooth Classic | BLE | 備考 |
|---|---|---|---|
| ESP32(WROOM-32、WROVER、DevKit V1) | 対応 | 対応(4.2) | BluetoothSerialが動く唯一のESP32ファミリー |
| ESP32-S2 | 非対応 | 非対応 | Wi-Fiのみ |
| ESP32-S3 | 非対応 | 対応(Bluetooth 5) | BluetoothSerialはコンパイルできないのでBLEを使う |
| ESP32-C3 | 非対応 | 対応(Bluetooth 5) | 小型で安価なBLEボード |
| ESP32-C6 | 非対応 | 対応(Bluetooth 5) | Wi-Fi 6、Zigbee、Threadにも対応 |
| ESP32-H2 | 非対応 | 対応(Bluetooth 5) | Wi-Fiなし。ZigbeeとThreadが中心 |
| ESP8266(NodeMCU、Wemos D1) | 非対応 | 非対応 | BluetoothにはHC-05かHM-10が必要 |
| Raspberry Pi Pico W / Pico 2 W | ハードウェアは対応 | 対応 | MicroPythonはBLEのみ対応 |
| Raspberry Pi Pico / Pico 2 | 非対応 | 非対応 | 外付けのBluetoothモジュールが必要 |
Bluetooth Classic:BluetoothSerialでスマートフォンからLEDを操作する
BluetoothSerialライブラリは、ESP32をスマートフォンに対してHC-05モジュールのように見せ、Serialオブジェクトと同じ命令を使います。available()、read()、print()、println()です。ライブラリはESP32のボードパッケージに含まれているので、追加でインストールするものはありません。回路は、ほとんどのDevKitボードでGPIO 2につながっている青い内蔵LEDで十分です。外付けのLEDを使う場合は、220 Ωの抵抗を通してGPIO 2につなぎます。抵抗値はLED抵抗計算ツールで確認でき、ESP32のほかの安全なピンはピン配置ツールで見られます。「ツール」>「ボード」でESP32 Dev Moduleが選ばれていることを確認して、次のコードを書き込みます。
1#include "BluetoothSerial.h"
2
3// Klasik Bluetooth yalnızca ilk ESP32'de var / Classic Bluetooth exists only on the original ESP32
4#if !defined(CONFIG_BT_ENABLED) || !defined(CONFIG_BLUEDROID_ENABLED)
5#error Bluetooth is not enabled! Please run `make menuconfig` to and enable it
6#endif
7#if !defined(CONFIG_BT_SPP_ENABLED)
8#error Serial Bluetooth not available or not enabled. It is only available for the ESP32 chip.
9#endif
10
11BluetoothSerial SerialBT;
12const int LED_PIN = 2; // DevKit dahili LED / DevKit onboard LED
13bool ledAcik = false;
14
15void ledAyarla(bool acik) {
16 ledAcik = acik;
17 digitalWrite(LED_PIN, acik ? HIGH : LOW);
18 SerialBT.println(acik ? "LED acik / LED on" : "LED kapali / LED off");
19}
20
21void setup() {
22 Serial.begin(115200);
23 pinMode(LED_PIN, OUTPUT);
24 SerialBT.begin("BlueGrays-ESP32"); // telefonda görünecek ad / name shown on the phone
25 Serial.println("Eslesmeye hazir / Ready to pair");
26}
27
28void loop() {
29 if (SerialBT.available()) {
30 String komut = SerialBT.readStringUntil('\n'); // satır sonuna kadar oku / read until newline
31 komut.trim(); // CR ve boşlukları sil / strip CR and spaces
32 komut.toLowerCase();
33
34 if (komut == "1" || komut == "ac") {
35 ledAyarla(true);
36 } else if (komut == "0" || komut == "kapat") {
37 ledAyarla(false);
38 } else if (komut == "durum") {
39 SerialBT.println(ledAcik ? "LED acik / LED on" : "LED kapali / LED off");
40 } else if (komut.length() > 0) {
41 SerialBT.print("Bilinmeyen komut / Unknown command: ");
42 SerialBT.println(komut);
43 }
44 }
45
46 if (Serial.available()) { // Seri Monitör'den telefona / Serial Monitor to phone
47 SerialBT.write(Serial.read());
48 }
49}このコードは、スマートフォンから届く1行ずつを命令として読みます。readStringUntil()は改行文字が届くまで待ち、trim()はスマートフォンが付け加える復帰文字(CR)と空白を取り除きます。この行を忘れると、「1」を送ったつもりでも実際には「1」と見えないCR文字が届き、どの命令にも一致しません。命令は小文字に変換されるので、「AC」と「ac」は同じ動作をします。下の2行は、シリアルモニタに入力したものをすべてスマートフォンに転送するので、パソコンとスマートフォンの間でワイヤレスのチャットができます。先頭の#errorチェックは、間違ったボードが選ばれているときにわかりやすいメッセージでビルドを止めます。
Serial Bluetooth Terminalでスマートフォンから接続する
- 書き込んだら、シリアルモニタを115200ボーで開き、「Eslesmeye hazir / Ready to pair」(ペアリング準備完了)のメッセージを確認します。
- Androidスマートフォンの「設定」>「Bluetooth」で新しいデバイスをスキャンし、BlueGrays-ESP32とペアリングします。ESP32は標準ではPINを聞かずにペアリングします。スマートフォンに確認コードが表示されたら承認してください。
- Google PlayからKai Morich作のSerial Bluetooth Terminalアプリをインストールします。
- アプリのサイドメニューからDevices(デバイス)を開き、Bluetooth ClassicタブでBlueGrays-ESP32を選びます。
- 上部バーのプラグのアイコンをタップします。画面に「Connected」と表示されたら接続完了です。
- 下の入力欄に1と入力して送信すると、LEDが点灯し「LED acik / LED on」(LEDオン)という返事が表示されます。0で消灯し、durum(トルコ語で「状態」)で今の状態がわかります。
BLEのしくみ:サービス、キャラクタリスティック、UUID
BLEには2つの役割があります。ESP32はサーバー(ペリフェラル)の役割で、値を持ち、最大31バイトの小さなアドバタイズパケットを短い間隔で送って(ESP32の標準では20〜40 msごと)「ここにいます」と知らせます。スマートフォンはクライアント(セントラル)の役割で、周囲をスキャンしてデバイスを見つけ、接続します。接続すると、スマートフォンはESP32のGATT(Generic Attribute Profile)テーブルを読み込みます。このテーブルは書類棚のようなものと考えてください。引き出し1つがサービスで、引き出しの中のファイル1つ1つがキャラクタリスティックです。キャラクタリスティックは1つの値(温度やLEDの命令など)と、その値で何ができるかを示すプロパティを持っています。
| 用語 | 意味 | このガイドでの例 |
|---|---|---|
| アドバタイズ | デバイスが名前とサービスを知らせる短いパケット | 「BlueGrays-BLE」という名前でアドバタイズ |
| サービス | 関連するキャラクタリスティックをまとめるグループ | Nordic UARTサービス、Environmental Sensing(0x181A) |
| キャラクタリスティック | 読み書きされる1つの値 | LEDの命令、温度(0x2A6E) |
| READ(読み取り) | スマートフォンが好きなときに値を読む | 最後に測った温度 |
| WRITE / WRITE_NR(書き込み) | スマートフォンが値を書き込む(応答あり / なし) | 「1」を書くとLEDが点灯 |
| NOTIFY(通知) | 値が変わるとESP32が自分からスマートフォンに知らせる | 2秒ごとに新しい温度 |
| ディスクリプタ0x2902(CCCD) | 通知のオン / オフを切り替えるスイッチ | nRF Connectの三重矢印ボタン |
| UUID | サービスとキャラクタリスティックの識別番号 | 6E400001-B5A3-F393-E0A9-E50E24DCCA9E |
サービスとキャラクタリスティックは、それぞれUUIDで識別されます。Bluetooth SIGが定めた標準の機能は、バッテリーサービスの0x180Fや温度キャラクタリスティックの0x2A6Eのように、短い16ビットの番号を使います。標準に対応するものがない独自のデータには、ランダムな128ビットのUUIDを生成します。Pythonならimport uuid; print(uuid.uuid4())だけで作れます。独自のUUIDを選ぶときは、xxxxxxxx-0000-1000-8000-00805F9B34FBのパターンに一致する値は使わないでください。このパターンはBluetooth SIGの16ビット番号用に予約されているからです。既存のアプリとスムーズに連携したいなら、誰もが知っているUUIDを使う方がさらに実用的です。
| サービス / キャラクタリスティック | UUID | データ形式 |
|---|---|---|
| Battery Service / Battery Level(電池残量) | 0x180F / 0x2A19 | 1バイト、パーセント(0〜100) |
| Environmental Sensing / Temperature(温度) | 0x181A / 0x2A6E | 2バイトの符号付き整数、0.01 °C単位 |
| Environmental Sensing / Humidity(湿度) | 0x181A / 0x2A6F | 2バイトの符号なし整数、0.01 %単位 |
| Heart Rate / Heart Rate Measurement(心拍数) | 0x180D / 0x2A37 | フラグのバイト+心拍数の値 |
| Nordic UART Service(NUS) | 6E400001-B5A3-F393-E0A9-E50E24DCCA9E | RX 6E400002(書き込み)、TX 6E400003(通知) |
ESP32のBLEサーバー:スマートフォンからLEDを操作する(Nordic UART)
最初のBLEプロジェクトでは、ESP32をNordic UART Service(NUS)のサーバーにします。NUSはNordic Semiconductorが定めた「BLE上のシリアルポート」のサービスで、事実上の標準になっています。スマートフォンがRXキャラクタリスティックに書き込み、ESP32はTXキャラクタリスティックから通知を送ります。多くの既製アプリが認識してくれるのが利点で、nRF Connect、Serial Bluetooth TerminalのBluetooth LEタブ、iPhoneのBluefruit Connectがすべて同じコードで動きます。そのため、このセクションのコードはAndroidとiPhoneの両方で使え、ESP32-S3やC3のようなBLE専用のボードでも動きます。BLEライブラリはESP32のボードパッケージの一部なので、追加でインストールするものはありません。
1#include <BLEDevice.h>
2#include <BLEServer.h>
3#include <BLEUtils.h>
4#include <BLE2902.h>
5
6// Nordic UART Service UUID'leri / Nordic UART Service UUIDs
7#define SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
8#define RX_UUID "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" // telefon -> ESP32 / phone -> ESP32
9#define TX_UUID "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" // ESP32 -> telefon / ESP32 -> phone
10
11const int LED_PIN = 2;
12
13BLEServer *sunucu = nullptr;
14BLECharacteristic *txKarakteristik = nullptr;
15volatile bool bagli = false;
16bool oncekiBagli = false;
17volatile int bekleyenKomut = -1; // -1: yeni komut yok / no new command
18
19class SunucuOlaylari : public BLEServerCallbacks {
20 void onConnect(BLEServer *s) { bagli = true; }
21 void onDisconnect(BLEServer *s) { bagli = false; }
22};
23
24class RxOlaylari : public BLECharacteristicCallbacks {
25 void onWrite(BLECharacteristic *c) {
26 String deger = c->getValue().c_str(); // çekirdek 2.x ve 3.x ile uyumlu / works on core 2.x and 3.x
27 deger.trim();
28 if (deger == "1") bekleyenKomut = 1;
29 else if (deger == "0") bekleyenKomut = 0;
30 }
31};
32
33void cevapGonder(const char *mesaj) {
34 txKarakteristik->setValue((uint8_t *)mesaj, strlen(mesaj));
35 txKarakteristik->notify(); // telefona bildirim / notify the phone
36}
37
38void setup() {
39 Serial.begin(115200);
40 pinMode(LED_PIN, OUTPUT);
41
42 BLEDevice::init("BlueGrays-BLE"); // yayında görünecek ad / advertised name
43 sunucu = BLEDevice::createServer();
44 sunucu->setCallbacks(new SunucuOlaylari());
45
46 BLEService *servis = sunucu->createService(SERVICE_UUID);
47
48 txKarakteristik = servis->createCharacteristic(TX_UUID, BLECharacteristic::PROPERTY_NOTIFY);
49 txKarakteristik->addDescriptor(new BLE2902()); // bildirim anahtarı (CCCD) / notification switch (CCCD)
50
51 BLECharacteristic *rx = servis->createCharacteristic(
52 RX_UUID, BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_WRITE_NR);
53 rx->setCallbacks(new RxOlaylari());
54
55 servis->start();
56
57 BLEAdvertising *yayin = BLEDevice::getAdvertising();
58 yayin->addServiceUUID(SERVICE_UUID);
59 yayin->setScanResponse(true);
60 yayin->setMinPreferred(0x06); // iPhone bağlantısına yardımcı / helps iPhone connections
61 yayin->setMaxPreferred(0x12);
62 BLEDevice::startAdvertising();
63 Serial.println("BLE yayini basladi / BLE advertising started");
64}
65
66void loop() {
67 if (bekleyenKomut != -1) { // telefondan komut geldi / command received
68 int komut = bekleyenKomut;
69 bekleyenKomut = -1;
70 digitalWrite(LED_PIN, komut == 1 ? HIGH : LOW);
71 if (bagli) cevapGonder(komut == 1 ? "LED acik" : "LED kapali");
72 }
73
74 if (!bagli && oncekiBagli) { // bağlantı koptu / disconnected
75 delay(500); // yığına zaman tanı / give the stack time
76 sunucu->startAdvertising(); // yeniden görünür ol / advertise again
77 Serial.println("Yeniden yayin / Advertising again");
78 oncekiBagli = false;
79 }
80 if (bagli && !oncekiBagli) { // yeni bağlantı / new connection
81 Serial.println("Telefon baglandi / Phone connected");
82 oncekiBagli = true;
83 }
84}コードでいちばん大事なのはコールバッククラスです。onConnectとonDisconnectは接続状態を追跡し、onWriteはスマートフォンがRXキャラクタリスティックに書いた値を受け取ります。これらの関数はBluetoothスタック自身のタスクで実行されるので、中で時間のかかる処理はせず、変数を変えるだけにしています。実際の処理はloop()が行います。TXキャラクタリスティックに追加したBLE2902ディスクリプタは、スマートフォンが通知をオンにするために必要です。接続が切れても、ESP32は自分ではアドバタイズを再開しません。loop()のstartAdvertising()の行がないと、最初の接続のあとはデバイスがもう見つかりません。setMinPreferredとsetMaxPreferredの行はアドバタイズに希望の接続間隔を加え、iPhoneとの接続をしやすくします。
nRF ConnectでBLEをステップごとにテストする
- Nordic Semiconductorの無料アプリnRF Connect for Mobileをスマートフォンにインストールします。AndroidとiPhoneの両方に対応しています。
- Androidではアプリが、Bluetoothと付近のデバイスの権限を求めます(Android 11以前では、位置情報サービスをオンにしたうえでの位置情報の権限)。これがないとスキャン一覧は空のままです。
- ScannerタブでSCANを押し、一覧からBlueGrays-BLEを探します。横にあるRSSIの値は信号の強さで、-50 dBmならとても近く、-90 dBmなら届く範囲のぎりぎりです。
- CONNECTをタップします。開いた画面にNordic UART Serviceが表示されます。自分で生成したUUIDを使っていた場合は、ここに「Unknown Service」(不明なサービス)と表示されます。
- TXキャラクタリスティック(6E400003)の横にある下向き三重矢印のアイコンをタップして、通知をオンにします。
- RXキャラクタリスティック(6E400002)の横にある上向き矢印のアイコンをタップし、データの種類にText(UTF-8)を選び、1と入力して送信します。LEDが点灯し、TXの行に「LED acik」(LEDオン)と表示されます。
このテストは、スマートフォンのアプリを書く前にESP32側が動いていることを確かめるいちばん手早い方法です。うまくいかないときは、まずnRF Connectで試します。デバイスが一覧にないならアドバタイズの問題、接続できてもサービスが表示されないならサービスのstart()呼び出しが抜けています。値を書いてもLEDが点かないなら、onWriteの中の比較を確認してください。同じコードはSerial Bluetooth TerminalのBluetooth LEタブからも試せます。アプリがNUSサービスを認識し、Bluetooth Classicと同じようにチャットできます。自分でAndroidアプリを作りたいなら、App InventorでArduinoとBluetoothをご覧ください。
BLEでセンサーデータを送る(DHT22)

| DHT22のピン | ESP32 | 備考 |
|---|---|---|
| VCC(+) | 3V3 | 3.3〜5 Vで動作 |
| DATA(OUT) | GPIO 4 | センサー単体の場合はDATAと3V3の間に10 kΩの抵抗を入れる |
| GND(−) | GND | 共通グラウンド |
この例では、標準のEnvironmental Sensingサービス(0x181A)と、その温度(0x2A6E)と湿度(0x2A6F)のキャラクタリスティックを使います。標準UUIDのよいところは、nRF Connectのようなアプリが値を認識して「23.45 °C」のようにそのまま表示してくれることです。標準では、値を文字ではなく100倍した整数で送ることになっています。23.45 °Cの温度は2345という数になり、下位バイトを先にした2バイト(リトルエンディアン)で送られます。センサーの読み取りには、DHT11ガイドで紹介したAdafruitのDHT sensor libraryを使います。ライブラリマネージャからインストールできます。
1#include <BLEDevice.h>
2#include <BLEServer.h>
3#include <BLE2902.h>
4#include <DHT.h>
5
6#define DHT_PIN 4
7DHT dht(DHT_PIN, DHT22);
8
9BLEServer *sunucu = nullptr;
10BLECharacteristic *sicaklikKar = nullptr;
11BLECharacteristic *nemKar = nullptr;
12volatile bool bagli = false;
13bool oncekiBagli = false;
14unsigned long sonOlcum = 0;
15
16class SunucuOlaylari : public BLEServerCallbacks {
17 void onConnect(BLEServer *s) { bagli = true; }
18 void onDisconnect(BLEServer *s) { bagli = false; }
19};
20
21BLECharacteristic *karakteristikEkle(BLEService *servis, uint16_t uuid) {
22 BLECharacteristic *k = servis->createCharacteristic(
23 BLEUUID(uuid), BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY);
24 k->addDescriptor(new BLE2902()); // bildirim için gerekli / needed for notify
25 return k;
26}
27
28void setup() {
29 Serial.begin(115200);
30 dht.begin();
31
32 BLEDevice::init("BlueGrays-Sensor");
33 sunucu = BLEDevice::createServer();
34 sunucu->setCallbacks(new SunucuOlaylari());
35
36 BLEService *servis = sunucu->createService(BLEUUID((uint16_t)0x181A)); // Environmental Sensing
37 sicaklikKar = karakteristikEkle(servis, 0x2A6E); // sıcaklık, 0,01 °C / temperature, 0.01 °C
38 nemKar = karakteristikEkle(servis, 0x2A6F); // nem, %0,01 / humidity, 0.01 %
39 servis->start();
40
41 BLEAdvertising *yayin = BLEDevice::getAdvertising();
42 yayin->addServiceUUID(BLEUUID((uint16_t)0x181A));
43 yayin->setScanResponse(true);
44 BLEDevice::startAdvertising();
45}
46
47void loop() {
48 if (millis() - sonOlcum >= 2000) { // DHT22 en fazla 2 sn'de bir / DHT22 at most every 2 s
49 sonOlcum = millis();
50 float t = dht.readTemperature();
51 float h = dht.readHumidity();
52 if (!isnan(t) && !isnan(h)) {
53 int16_t t100 = (int16_t)lroundf(t * 100); // 23,45 °C -> 2345
54 uint16_t h100 = (uint16_t)lroundf(h * 100); // %41,2 -> 4120
55 sicaklikKar->setValue((uint8_t *)&t100, 2); // 2 bayt, düşük bayt önce / 2 bytes, little-endian
56 nemKar->setValue((uint8_t *)&h100, 2);
57 if (bagli) {
58 sicaklikKar->notify();
59 nemKar->notify();
60 }
61 Serial.printf("%.2f C %.2f %%\n", t, h);
62 }
63 }
64
65 if (!bagli && oncekiBagli) { // bağlantı koptu / disconnected
66 delay(500);
67 sunucu->startAdvertising(); // yeniden yayın / advertise again
68 oncekiBagli = false;
69 }
70 if (bagli && !oncekiBagli) oncekiBagli = true;
71}DHT22はそれより頻繁に読めないので、コードは2秒ごとに測定します。値はまずsetValue()でキャラクタリスティックに書き込まれるので、スマートフォンが通知を待たずに読み取り(READ)をしたときも最新の測定値が得られます。スマートフォンが接続されていれば、notify()が新しい値をすぐに送ります。nRF Connectが値を解読できない場合は、生のバイトが表示されます。23.45 °Cなら画面には(0x) 29-09と表示されます。バイトを逆にすると0x0929になり、これは10進数で2345です。100で割れば温度になります。自分でアプリを書くときも同じ計算が必要です。値を文字で送る方が簡単に見えるかもしれませんが、標準の形式の方がバイト数が少なく、既存のアプリでも使えます。
iPhone(iOS)でESP32のBluetoothを使う
iPhoneユーザーからいちばん多い質問は、ESP32がBluetooth設定に表示されないのはなぜか、というものです。AppleはBluetooth Classicのシリアルポートプロファイル(SPP)をMFi認証のアクセサリーにしか開放していないため、BluetoothSerialを動かしているESP32にはiPhoneから接続できません。解決策はBLEで、上のNUSとセンサーの例はiPhoneでも問題なく動きます。いくつか気をつける点があります。
- 設定での検索:BLEデバイスはiOSのBluetooth設定には一覧表示されません。nRF Connect、LightBlue、Bluefruit Connectなどのアプリの中で接続してください。
- キャッシュ:iOSはデバイスの名前とサービス一覧を覚えています。コードでサービスを変えたあとも古い一覧が表示される場合は、スマートフォンのBluetoothをいったんオフにしてからオンにしてください。
- MACアドレスがない:iOSはアプリにデバイスのMACアドレスを見せず、デバイスごとに独自の識別子を割り当てます。ESP32を何台か使う場合は、それぞれに違う名前を付けてください。
- 接続間隔:setMinPreferred(0x06)とsetMaxPreferred(0x12)の行は消さないでください。Espressifのサンプルでも、iPhoneでの接続トラブル対策として推奨されています。
- 自作アプリ:iOSではSwiftとCore Bluetoothを使います。ペアリングは不要で、アプリはサービスのUUIDを直接スキャンします。
Raspberry Pi Pico WとMicroPythonでBLE

Raspberry Pi Picoファミリーで、Bluetoothが使えるのはPico WとPico 2 Wだけです。ボードに載っているCYW43439チップはハードウェアとしてはBluetooth ClassicとBLEの両方に対応していますが、MicroPythonが使うのはBLEだけです(バージョン1.20以降)。下のコードはESP32のNUSの例をMicroPythonにしたもので、同じUUIDを使っているので、nRF ConnectやSerial Bluetooth Terminalで同じようにテストできます。Pico W用に作られたMicroPythonのファームウェアを書き込んでおく必要があります。このコードはボードの内蔵温度センサーも読み、2秒ごとに接続中のスマートフォンに送ります。
1import bluetooth
2import struct
3import time
4from machine import Pin, ADC
5from micropython import const
6
7_IRQ_CENTRAL_CONNECT = const(1)
8_IRQ_CENTRAL_DISCONNECT = const(2)
9_IRQ_GATTS_WRITE = const(3)
10_FLAG_READ = const(0x0002)
11_FLAG_WRITE_NO_RESPONSE = const(0x0004)
12_FLAG_WRITE = const(0x0008)
13_FLAG_NOTIFY = const(0x0010)
14
15# Nordic UART Service: ESP32 örneğiyle aynı UUID'ler / same UUIDs as the ESP32 example
16NUS_UUID = bluetooth.UUID("6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
17TX = (bluetooth.UUID("6E400003-B5A3-F393-E0A9-E50E24DCCA9E"), _FLAG_READ | _FLAG_NOTIFY)
18RX = (bluetooth.UUID("6E400002-B5A3-F393-E0A9-E50E24DCCA9E"), _FLAG_WRITE | _FLAG_WRITE_NO_RESPONSE)
19
20led = Pin("LED", Pin.OUT) # Pico W dahili LED / Pico W onboard LED
21sensor = ADC(4) # dahili sıcaklık sensörü / internal temperature sensor
22
23ble = bluetooth.BLE()
24ble.active(True)
25((tx_h, rx_h),) = ble.gatts_register_services(((NUS_UUID, (TX, RX)),))
26baglanti = None
27
28
29def alan(tur, veri): # uzunluk + tür + veri / length + type + data
30 return struct.pack("BB", len(veri) + 1, tur) + veri
31
32
33YAYIN = alan(0x01, b"\x06") + alan(0x09, b"BlueGrays-Pico") # bayraklar + ad / flags + name
34YANIT = alan(0x07, bytes(NUS_UUID)) # 128 bit UUID / 128-bit UUID
35
36
37def yayinla():
38 ble.gap_advertise(100000, adv_data=YAYIN, resp_data=YANIT) # 100 ms aralık / interval
39
40
41def olay(event, data):
42 global baglanti
43 if event == _IRQ_CENTRAL_CONNECT:
44 baglanti = data[0]
45 elif event == _IRQ_CENTRAL_DISCONNECT:
46 baglanti = None
47 yayinla() # yeniden görünür ol / advertise again
48 elif event == _IRQ_GATTS_WRITE and data[1] == rx_h:
49 komut = ble.gatts_read(rx_h).decode().strip()
50 if komut == "1":
51 led.on()
52 elif komut == "0":
53 led.off()
54
55
56ble.irq(olay)
57yayinla()
58
59while True:
60 if baglanti is not None:
61 volt = sensor.read_u16() * 3.3 / 65535
62 sicaklik = 27 - (volt - 0.706) / 0.001721 # veri sayfası formülü / datasheet formula
63 try:
64 ble.gatts_notify(baglanti, tx_h, "T:{:.1f}\n".format(sicaklik).encode())
65 except OSError:
66 pass # bağlantı yeni koptu / just disconnected
67 time.sleep(2)MicroPythonのbluetoothモジュールは低レベルなので、アドバタイズパケットは自分で組み立てます。各フィールドは長さのバイト、種類のバイト、データからなります。0x01はフラグ、0x09はデバイスの完全な名前、0x07は128ビットのサービスUUIDです。アドバタイズパケットには最大31バイトしか入らないので、18バイトのUUIDのフィールドはスキャン応答(resp_data)に入れます。gatts_register_services()はキャラクタリスティックのハンドルを返します。このハンドルを使って、スマートフォンが書いた値を読んだり通知を送ったりします。コードを短くしたいなら、asyncioベースのaiobleライブラリをmipでインストールできます。ESP32のMicroPythonでこのコードを動かすには、LEDの行をPin(2, Pin.OUT)に変え、内蔵温度センサーの代わりにたとえばDHT11を読むだけです。
Wi-FiとBluetoothの同時使用:メモリ、通信距離、電源
ESP32では、Wi-FiとBluetoothが同じ2.4 GHzの無線部を共有しています(日本でも範囲はほぼ同じですが、技適マークのない無線モジュールの使用は電波法で制限されています)。チップは時分割で両方を動かすので同時に使えますが、それぞれ少し遅くなり、Bluetooth Classicと大量のWi-Fi通信の組み合わせがいちばん厳しい状況です。ただ、本当の問題はたいていメモリです。Bluetoothスタックは大きく、Wi-Fiと合わせるとプログラムが標準の1.2 MB(1,310,720バイト)のアプリ用パーティションを超えることがあります。このガイドのBLEの例だけでも、その領域の約80%を使います。ビルドが"Sketch too big"(スケッチが大きすぎる)や"text section exceeds available space in board"(ボードの空き容量を超えた)で終わり、無線アップデート(OTA)を使わないなら、「ツール」>「Partition Scheme」でHuge APP (3MB No OTA/1MB SPIFFS)を選びます。フラッシュとRAMの使用量が少ないNimBLE-Arduinoライブラリもよい選択肢です。
通信距離はボードのアンテナ設計と環境によって変わります。基板上にPCBアンテナがあるESP32なら、屋内では壁の状況によって10〜20メートル、開けた場所ならそれ以上と考えてください。外部アンテナ端子付きのモジュール(たとえばESP32-WROOM-32U)なら、さらに遠くまで届きます。アンテナを金属ケースや電池、ケーブルで覆うと、通信距離は大きく落ちます。無線部がオンになるときに電流が一瞬上がるので、弱いUSBケーブルだとボードが「Brownout detector was triggered」(ブラウンアウト検出)というメッセージで再起動することがあります。対策はESP32のブラウンアウトとGuru Meditationにあります。
ESP32のBluetoothプロジェクトのアイデア
- Bluetoothロボットカー:BluetoothSerialで受け取ったF、B、L、Rの命令をL298Nモータードライバに渡します。機構についてはArduinoロボットカーの作り方をご覧ください。
- スマートコンセント:NUSの例のLEDをリレーモジュールに置き換えて、iPhoneからランプを点けます。
- ワイヤレス温度計:DHT22の例にOLEDディスプレイを加えて、値を画面とスマートフォンの両方に表示します。
- カラフルなムードライト:スマートフォンから送ったR、G、Bの値でWS2812B NeoPixelのLEDテープの色を変えます。
- 自作のAndroidアプリ:App Inventorなら、ボタンとスライダーのあるリモコンアプリを数時間で作れます。
- ESP32同士の通信:スマートフォンなしで2枚のボードを通信させるなら、Bluetoothの代わりにもっと簡単で速いESP-NOWを試してください。
ESP32のBluetoothでよくある問題と対策
| 症状 / エラー | 原因 | 対策 |
|---|---|---|
| エラー:「Serial Bluetooth not available or not enabled」(Bluetooth Classicが使えない) | 選んだボードがBluetooth ClassicのないESP32-S3、C3、C6、H2 | 無印ESP32を使うか、BLE(NUS)の例に切り替える |
| エラー:「conversion from 'String' to non-scalar type 'std::string' requested」(型の変換エラー) | コア2.x(std::string)用のコードを3.xのコアでコンパイルした | getValue().c_str()をString変数に代入する |
| エラー:「Sketch too big」(スケッチが大きすぎる) | BluetoothとWi-Fiでアプリ用パーティションを超えた | Partition SchemeでHuge APPを選ぶか、NimBLE-Arduinoを使う |
| iPhoneがESP32を見つけられない | Bluetooth Classic(SPP)はiOSで非対応 | BLEを使い、アプリの中から接続する |
| 最初の接続のあとデバイスが見えなくなる | 切断後にアドバタイズを再開していない | 切断後にstartAdvertising()を呼ぶ |
| 通知が届かない | スマートフォンで通知がオンになっていない、またはBLE2902がない | nRF Connectで三重矢印をタップする。キャラクタリスティックにBLE2902を追加する |
| エラー:「read failed, socket might closed or timeout」(読み取り失敗) | 古いペアリング情報が残っている、または別のスマートフォンがすでに接続している | スマートフォンでデバイスの登録を解除してペアリングし直す。SPPは同時に1つの接続しか受け付けない |
| 長いメッセージの最後が切れる | 標準のMTUは23バイト、つまり1回の通知につきデータは20バイト | メッセージを20バイトずつに分けるか、BLEDevice::setMTU(185)でより大きなMTUを許可する |
| サービスの変更がスマートフォンに反映されない | スマートフォンが古いGATTテーブルをキャッシュに持っている | スマートフォンのBluetoothをオフにしてからオンにする。Androidではデバイスの登録を解除する |
ESP32のBluetoothを使えば、スマートフォンをリモコンに、ESP32をワイヤレスのセンサーやスマートデバイスにするのは数行のコードでできます。Androidで手早く始めるならBluetoothSerial、iPhone対応や低消費電力が必要ならBLEが正しい選択です。ESP32のワイヤレスプロジェクトをさらに続けたいなら、BlueGraysアプリには、LED、RGB LED、DHT22での温度測定、Bluetoothで操作する車などのプロジェクトがあり、それぞれステップごとの回路図とすぐに使えるコードが付いています。
よくある質問
ESP32にはBluetoothがありますか?
はい。無印ESP32チップはBluetooth 4.2でBluetooth Classic(BR/EDR)とBLEの両方に対応しており、追加のモジュールは要りません。ESP32-S3、C3、C6、H2はBLEだけに対応し、ESP32-S2にはBluetoothがまったくありません。
ESP32のBluetoothの通信距離はどのくらいですか?
基板上にPCBアンテナがあるESP32なら、屋内ではふつう10〜20メートルです。壁や金属の物、アンテナの向きがこの距離に影響します。開けた場所では距離が延び、外部アンテナ付きのモジュール(たとえばWROOM-32U)ならさらに遠くまで届きます。
ESP32はiPhoneとBluetoothで接続できますか?
BLEならできますが、Bluetooth Classic(BluetoothSerial)ではできません。iOSはシリアルポートプロファイル(SPP)をMFi認証のアクセサリーにしか開放していないからです。iPhoneでは、nRF Connect、LightBlue、Bluefruit ConnectなどのアプリでESP32のBLEサービスに接続できます。
BluetoothSerialはESP32-S3やESP32-C3で動きますか?
いいえ。これらのチップにはBluetooth Classicのハードウェアがないので、BluetoothSerialはコンパイルできません。同じシリアルポートの考え方をBLEで実現するには、Nordic UART Service(NUS)を使えます。Serial Bluetooth Terminalのようなアプリが認識してくれます。
ESP32はWi-FiとBluetoothを同時に使えますか?
はい。同じ2.4 GHzの無線部を時分割で使い、両方が一緒に動きます。ただし速度は少し落ち、プログラムは大きくなります。「Sketch too big」(スケッチが大きすぎる)のエラーが出たら、Partition SchemeでHuge APPを選ぶか、軽いNimBLE-Arduinoライブラリを使ってください。
ESP32にHC-05は必要ですか?
ふつうは要りません。無印ESP32のBluetoothSerialライブラリがHC-05の役割を内部で果たし、同じターミナルアプリで使えます。HC-05が必要なのは、Bluetoothのないボード(Arduino Uno、ESP8266、標準のPico、ESP32-S2)の場合か、プロジェクトにBluetooth Classicが必要なのにBLE専用のESP32を使っている場合だけです。
この記事を読んだ後に作れるプロジェクト
全プロジェクトを見る
ESP32の内蔵LEDを点滅
ESP32 DevKit V1に内蔵された青色LEDを点滅させます。外付け部品は不要です!ESP32を購入したら、最初に試すプロジェクトです。GPIO 2が内蔵LEDにつながっています。

ESP32でLEDを点滅
ESP32のGPIO出力の基本を学びます。デジタルピンからLEDを点灯・消灯させて、点滅させます。Arduino UNOの定番のLED点滅プロジェクトのESP32版です。Arduino環境で使う基本的な点滅コマンドは同じですが、ESP32のピン配置と3.3 VのGPIO電圧レベルはArduino UNOとは異なります。

ESP32でボタンによるLED制御(トグル)
ボタンを押すたびにLEDの状態が切り替わり、点灯中なら消灯し、消灯中なら点灯します。基本的なトグル動作です。チャタリング対策でボタンの機械的なノイズを除去し、1回の操作で1回だけ切り替わるようにします。

BluetoothでスマートフォンからLEDを操作
スマートフォンで部屋の照明をつけたり消したりするための、小さな練習です。Bluetoothモジュールがスマートフォンから送られた文字をArduinoに伝えます。ある文字で赤いLEDが、別の文字で緑のLEDが点灯し、同じ文字を小文字で送ると消灯します。無線操作の仕組みを、ごくシンプルな形で学べます。

L298Nで作るBluetooth操作の車
スマートフォンをリモコンにして、自作のロボットカーを操作できます。ArduinoはBluetoothモジュールを通じてスマートフォンのリモコンアプリから指令を受け取り、モータードライバーで車輪を回します。前進、後退、左右や斜めへの移動ができ、速度もスマートフォンから調整できます。停止ボタンを押すと、短いブレーキ動作でモーターがすぐに止まります。

ESP32でRGB LEDを制御
3個の個別のLEDの代わりに、1個のRGB LEDで無数の色を作ります。Arduinoと同じようにanalogWrite()でR/G/Bの値を調整し、虹色に変化させます。赤と緑を混ぜると黄色になるなど、色の仕組みを実際に確認できます。

ESP32 DHT22温湿度計(LCDディスプレイ付き)
DHT22センサーで周囲の温度と湿度を測定し、16x2 I2C LCDにリアルタイムで表示するプロジェクトです。Adafruit DHTライブラリを使い、1本の信号線を使うプロトコルでセンサーからデータを読み取ります。IoT気象観測装置やスマートホームの自動化、温室の環境監視システムの基礎となります。ESP32のGPIO 4ピンを介してDHT22と通信します。

ESP32 WS2812B NeoPixel LEDテープの制御
WS2812B(NeoPixel)は、1本のデータピンで連結したLEDを個別に制御できるスマートなRGB LEDです。このプロジェクトでは、8個のLEDを備えたWS2812BテープをAdafruit_NeoPixelライブラリで制御し、滑らかな虹色のアニメーションを作ります。ESP32の3.3VロジックレベルはほとんどのWS2812Bのロットで問題なく動作しますが、不安定な場合に備え、470Ωの直列抵抗と1000µFのコンデンサを使う対策も紹介します。アニメーションは、HSV色空間の色相(hue)をずらして作ります。
ほかの記事
BlueGraysアプリ
この回路をスマホでステップごとに作る
回路図、コード、部品リストを1つの画面で確認できます。Arduino、ESP32、Picoのプロジェクトを200件以上用意しています。

