ガイド · 読了まで21分

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のセットアップを済ませてください。

BluetoothとBLEを搭載したESP32開発ボード
ESP32開発ボード:Wi-Fi、Bluetooth Classic、BLEがすべて同じチップに入っているので、スマートフォンにつなぐのに外付けのBluetoothモジュールは要りません。

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 ClassicBLE備考
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モジュールが必要
ボードメニューでESP32-S3、C3、C6、H2を選んだままBluetoothSerialのコードをコンパイルすると、Espressifのサンプル(と下のコード)にあるチェック行が"Serial Bluetooth not available or not enabled. It is only available for the ESP32 chip."(Bluetooth Classicが使えない、無印ESP32専用)というメッセージでビルドを止めます。これはインストールやライブラリの問題ではなく、単にチップにBluetooth Classicのハードウェアがないということです。これらのボードを持っている場合は、下のBLE(Nordic UART)のサンプルに直接進んでください。

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が選ばれていることを確認して、次のコードを書き込みます。

C++ (Arduino IDE)
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でスマートフォンから接続する

  1. 書き込んだら、シリアルモニタを115200ボーで開き、「Eslesmeye hazir / Ready to pair」(ペアリング準備完了)のメッセージを確認します。
  2. Androidスマートフォンの「設定」>「Bluetooth」で新しいデバイスをスキャンし、BlueGrays-ESP32とペアリングします。ESP32は標準ではPINを聞かずにペアリングします。スマートフォンに確認コードが表示されたら承認してください。
  3. Google PlayからKai Morich作のSerial Bluetooth Terminalアプリをインストールします。
  4. アプリのサイドメニューからDevices(デバイス)を開き、Bluetooth ClassicタブでBlueGrays-ESP32を選びます。
  5. 上部バーのプラグのアイコンをタップします。画面に「Connected」と表示されたら接続完了です。
  6. 下の入力欄に1と入力して送信すると、LEDが点灯し「LED acik / LED on」(LEDオン)という返事が表示されます。0で消灯し、durum(トルコ語で「状態」)で今の状態がわかります。
Serial Bluetooth Terminalの下にあるM1とM2のマクロボタンを長押しして「1」と「0」の命令を保存すると、1回のタップでLEDを切り替えられます。アプリ設定のSend > Newlineの項目は標準でCR+LFですが、コードのtrim()が余分なCRをすでに取り除いています。LEDの代わりにリレーをつなげば、同じコードでランプやコンセントを切り替えられます。詳しくはリレーモジュールとは?をご覧ください。

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 / 0x2A191バイト、パーセント(0〜100)
Environmental Sensing / Temperature(温度)0x181A / 0x2A6E2バイトの符号付き整数、0.01 °C単位
Environmental Sensing / Humidity(湿度)0x181A / 0x2A6F2バイトの符号なし整数、0.01 %単位
Heart Rate / Heart Rate Measurement(心拍数)0x180D / 0x2A37フラグのバイト+心拍数の値
Nordic UART Service(NUS)6E400001-B5A3-F393-E0A9-E50E24DCCA9ERX 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のボードパッケージの一部なので、追加でインストールするものはありません。

C++ (Arduino IDE)
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との接続をしやすくします。

この記事のコードは、ESP32 Arduinoコア(ボードパッケージ)のバージョン2.0.11でコンパイルしました。このバージョンではgetValue()などのBLE関数はstd::stringを返し、ネット上のサンプルの多くもこの型を使っています。コア3.0からBLE関数はArduinoのString型に変わったため、std::stringで書かれたコードは3.xで「conversion from 'String' to non-scalar type 'std::string' requested」(String型からstd::string型への変換エラー)で失敗します。私たちのコードのように結果をString変数に代入し、末尾に.c_str()を付ければ、その行は2.xと3.xの両方のコアでコンパイルできます。

nRF ConnectでBLEをステップごとにテストする

  1. Nordic Semiconductorの無料アプリnRF Connect for Mobileをスマートフォンにインストールします。AndroidとiPhoneの両方に対応しています。
  2. Androidではアプリが、Bluetoothと付近のデバイスの権限を求めます(Android 11以前では、位置情報サービスをオンにしたうえでの位置情報の権限)。これがないとスキャン一覧は空のままです。
  3. ScannerタブでSCANを押し、一覧からBlueGrays-BLEを探します。横にあるRSSIの値は信号の強さで、-50 dBmならとても近く、-90 dBmなら届く範囲のぎりぎりです。
  4. CONNECTをタップします。開いた画面にNordic UART Serviceが表示されます。自分で生成したUUIDを使っていた場合は、ここに「Unknown Service」(不明なサービス)と表示されます。
  5. TXキャラクタリスティック(6E400003)の横にある下向き三重矢印のアイコンをタップして、通知をオンにします。
  6. 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温湿度センサー
DHT22は0.1 °Cの分解能で温度を測ります。3.3 Vで動くので、ESP32に直接つなげます。
DHT22のピンESP32備考
VCC(+)3V33.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を使います。ライブラリマネージャからインストールできます。

C++ (Arduino IDE)
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で割れば温度になります。自分でアプリを書くときも同じ計算が必要です。値を文字で送る方が簡単に見えるかもしれませんが、標準の形式の方がバイト数が少なく、既存のアプリでも使えます。

接続せずにデータを送ることもできます。ESP32は測定値をアドバタイズパケットのメーカーデータ欄に直接入れられ、近くにあるスマートフォンやゲートウェイなら接続しなくても読み取れます。この方法をビーコンと呼び、Home Assistantが認識するオープンなBTHome形式も同じしくみです。ESP32が測定のたびにディープスリープに入れば、電池寿命は何か月にも延びます。方法はESP32のディープスリープをご覧ください。

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ボード
Raspberry Pi Picoファミリーで、Bluetoothがあるのは名前にWが付くボード(Pico W、Pico 2 W)だけです。標準のPicoには無線接続がありません。

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秒ごとに接続中のスマートフォンに送ります。

MicroPython
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ではデバイスの登録を解除する
接続トラブルを調べるときは、「ツール」>「Core Debug Level」でDebugかVerboseを選びます。するとシリアルモニタにBluetoothスタックの接続、切断、エラーのログが表示され、問題がスマートフォン側かESP32側かがすぐにわかります。スマートフォンでnRF Connectを使って同じテストを繰り返せば、自作のアプリが原因かどうかもわかります。

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を点滅
#106初級

ESP32の内蔵LEDを点滅

ESP32 DevKit V1に内蔵された青色LEDを点滅させます。外付け部品は不要です!ESP32を購入したら、最初に試すプロジェクトです。GPIO 2が内蔵LEDにつながっています。

4.8(29)
53
ESP32でLEDを点滅
#103初級

ESP32でLEDを点滅

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

4.7(37)
99
ESP32でボタンによるLED制御(トグル)
#105初級

ESP32でボタンによるLED制御(トグル)

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

4.6(39)
57
BluetoothでスマートフォンからLEDを操作
#34初級
プレミアム

BluetoothでスマートフォンからLEDを操作

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

4.8(68)
89
L298Nで作るBluetooth操作の車
#80上級
プレミアム

L298Nで作るBluetooth操作の車

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

5.0(38)
65
ESP32でRGB LEDを制御
#111初級

ESP32でRGB LEDを制御

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

4.6(29)
51
ESP32 DHT22温湿度計(LCDディスプレイ付き)
#117中級

ESP32 DHT22温湿度計(LCDディスプレイ付き)

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

4.8(38)
56
ESP32 WS2812B NeoPixel LEDテープの制御
#126中級
プレミアム

ESP32 WS2812B NeoPixel LEDテープの制御

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

4.9(19)
30