1. はじめに:なぜ「ESP32」と「BLE」なのか?
「自宅のドアが開いたら、離れた場所でもすぐにスマートフォンに通知が欲しい」 「既存のスマートホーム製品は高価で設定が複雑だ」
このような悩みを解決するのが、ESP32と Bluetooth Low Energy (BLE) を組み合わせた自作システムです。ESP32はWi-FiとBLEの両方を搭載した強力なマイコンであり、安価でプログラミングが容易です。
本記事では、ESP32を2台使用し、子機(ドアセンサー)と親機(通知ハブ)に役割を分け、BLEで確実な近距離通信を行い、親機からインターネット経由でGmailにアラートメールを送信するシステムを構築する手順を、詳しく解説します。
2. システムの全体像と役割分担
このシステムの核となるのは、BLEの GATT (Generic Attribute Profile) による通信モデルです。
役割 | ESP32の動作モード | 機能(役割) |
子機 (Door Sensor) | BLE GATT サーバー | ドアの開閉状態(磁気センサー)を監視し、状態が変化した際にデータをブロードキャスト(Notify)する。 |
親機 (Alert Hub) | BLE GATT クライアント | Wi-Fiに接続し、子機からの通知(Notify)を常時監視。通知を受け取ったらGmail送信処理を実行する。 |
利点: Wi-Fiよりも消費電力の少ないBLEを子機に使用することで、電池駆動が容易になり、センサーをドア付近に自由に設置できます。
3. 構築前の準備:必要なものとGmail設定
3.1. ハードウェアリスト
- ESP32 開発ボード (2台): Arduino IDEでプログラミングできるもの。
- 磁気リードスイッチ (ドアセンサー): ドア枠とドアに分けて取り付け、近づくと接点がON/OFFするタイプ。
- ジャンパーワイヤ、ブレッドボード
- 電源 (USBケーブル、または子機を電池駆動する場合はバッテリー)
3.2. ソフトウェアとライブラリ
- Arduino IDE
- ESP32 Mail Client ライブラリ (親機用): Arduinoライブラリマネージャからインストールします。
3.3. 最重要:Gmailのアプリパスワード設定
GmailのSMTPサーバーを利用してESP32からメールを送信する場合、セキュリティ上の理由で通常のパスワードは使用できません。以下の設定を完了し、16桁のアプリパスワードを控えておいてください。
- Googleアカウントで2段階認証を有効化します。
- Googleアカウントのセキュリティ設定へ移動し、「アプリ パスワード」を作成します。
- 生成された16桁のパスコードが、スケッチ内で使用する
APP_PASSWORD
となります。
4. ESP32 子機(BLEサーバー)の構築
子機は、ドアの開閉状態(磁気センサー)を監視し、状態が変化した際にBLEで親機に通知します。
4.1. ハードウェア接続
磁気リードスイッチをESP32の指定したGPIOピンとGND間に接続します。以下のスケッチでは、GPIO 34を使用し、INPUT_PULLUP
を設定しています。
接続例: GPIO 34 ← 磁気センサー → GND
- ドアが閉(磁石接近): LOW (0V)
- ドアが開(磁石離脱): HIGH (3.3V)
4.2. 子機(BLEサーバー)完全プログラム
以下のコードをESP32(子機)に書き込みます。DOOR_SENSOR_PIN
やDEVICE_NAME
を環境に合わせて変更してください。
C++
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
// ========== 【設定】定数設定 ==========
#define DEVICE_NAME "ESP32_Door_Sensor" // 親機がスキャンするデバイス名 (親機と一致させること)
#define DOOR_SENSOR_PIN 34 // 磁気センサーを接続するGPIOピン (例: 34)
// BLE UUID (親機と子機で一致させること)
const char* SERVICE_UUID = "4fafc201-1fb5-459e-8fcc-c5c9c331914b";
const char* CHARACTERISTIC_UUID = "beb5483e-36e1-4688-b7f5-ea07361b26a8";
// グローバル変数
BLECharacteristic *pCharacteristic;
bool deviceConnected = false;
int lastSensorState = -1; // 初期状態(-1は未初期化を示す)
// BLEサーバーコールバッククラス
class MyServerCallbacks: public BLEServerCallbacks {
void onConnect(BLEServer* pServer) {
deviceConnected = true;
Serial.println("Client connected.");
}
void onDisconnect(BLEServer* pServer) {
deviceConnected = false;
Serial.println("Client disconnected. Starting advertising...");
BLEDevice::startAdvertising(); // 再度アドバタイズ開始
}
};
// ===================================
void setup() {
Serial.begin(115200);
pinMode(DOOR_SENSOR_PIN, INPUT_PULLUP); // センサーピンをプルアップ入力に設定
// BLEデバイスの初期化
BLEDevice::init(DEVICE_NAME);
// サーバーの作成
BLEServer *pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyServerCallbacks());
// サービスの作成
BLEService *pService = pServer->createService(SERVICE_UUID);
// 特性 (Characteristic) の作成と設定
pCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_NOTIFY
);
// 特性にDescriptor (2902) を追加。Notifyを可能にするために必要。
pCharacteristic->addDescriptor(new BLE2902());
// サービス開始
pService->start();
// アドバタイズ開始
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
pAdvertising->addServiceUUID(SERVICE_UUID);
pAdvertising->setScanResponse(true);
pAdvertising->setMinPreferred(0x06); // スキャン時間を短縮
BLEDevice::startAdvertising();
Serial.println("BLE Server ready. Advertising...");
}
// ===================================
void loop() {
int currentSensorState = digitalRead(DOOR_SENSOR_PIN); // センサーの状態を読み取り
// 状態が変化し、かつクライアントが接続している場合のみ処理
if (currentSensorState != lastSensorState) {
// 状態を文字列化 (LOW: ドア閉鎖, HIGH: ドア開放と仮定)
String status = (currentSensorState == LOW) ? "CLOSED" : "OPEN";
// 接続している場合のみ通知
if (deviceConnected) {
Serial.print("Sensor state changed to: ");
Serial.println(status);
// BLE特性の値を更新し、クライアントに通知 (Notify)
pCharacteristic->setValue(status.c_str());
pCharacteristic->notify();
} else {
Serial.print("State changed, but no client connected. New state: ");
Serial.println(status);
}
lastSensorState = currentSensorState; // 状態を更新
}
delay(50); // 状態監視の間隔
}
5. ESP32 親機(BLEクライアント&Gmail送信)の構築
親機は、子機からのBLE通知をトリガーとして、Wi-Fiを経由してGmailにアラートメールを送信します。
5.1. 接続シーケンス
親機は以下のタスクを担います。特にBLE接続が切れた場合の自動再接続ロジックを組み込んでいます。
- Wi-Fi接続:インターネットアクセスを確保。
- BLEスキャン:子機のデバイス名 (
ESP32_Door_Sensor
) を検索。 - 接続確立:子機に接続し、サービスと特性を特定。
- 通知の購読 (Subscribe):子機からの状態変化通知を待ち受ける。
- Gmail送信:通知コールバック内で
sendAlertEmail()
を実行。
5.2. 親機(BLEクライアント&Gmail送信)完全プログラム
以下のコードの 【設定】定数設定セクションを、ご自身の環境に合わせて必ず 書き換えてからESP32(親機)に書き込んでください。
C++
#include <WiFi.h>
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEScan.h>
#include <BLEAdvertisedDevice.h>
#include <ESP_Mail_Client.h> // ESP32 Mail Client ライブラリ
// ========== 【設定】 Wi-Fi & Gmail 定数設定 ==========
// Wi-Fi設定
#define WIFI_SSID "YOUR_WIFI_SSID" // Wi-FiのSSID
#define WIFI_PASSWORD "YOUR_WIFI_PASSWORD" // Wi-Fiのパスワード
// Gmail SMTP設定
#define SMTP_HOST "smtp.gmail.com"
#define SMTP_PORT 465 // SSL/TLSポート
#define AUTHOR_EMAIL "YOUR_GMAIL_ADDRESS@gmail.com" // 送信元Gmailアドレス
#define APP_PASSWORD "YOUR_16_DIGIT_APP_PASSWORD" // Gmailのアプリパスワード (通常のパスワードではない)
#define RECIPIENT_EMAIL "RECIPIENT_EMAIL@example.com" // 通知を受け取るメールアドレス
// 通知間隔設定
#define EMAIL_COOLDOWN_SEC 10 // ドア状態変化後、次にメールを送信できるまでのクールダウン時間(秒)
// ========== 【設定】 BLE 定数設定 ==========
// 子機と一致させるUUIDs
const char* SERVICE_UUID = "4fafc201-1fb5-459e-8fcc-c5c9c331914b";
const char* CHARACTERISTIC_UUID = "beb5483e-36e1-4688-b7f5-ea07361b26a8";
// 子機と一致させるデバイス名
const char* TARGET_DEVICE_NAME = "ESP32_Door_Sensor";
// グローバル変数とインスタンス
BLEAdvertisedDevice *myDevice;
static bool doConnect = false;
static bool connected = false;
static BLEUUID serviceUUID(SERVICE_UUID);
static BLEUUID charUUID(CHARACTERISTIC_UUID);
BLEClient* pClient = nullptr;
unsigned long lastEmailTime = 0;
SMTPSession smtp;
// ===================================
// Gmailアラート送信関数
void sendAlertEmail(String status) {
// クールダウン期間のチェック
if (millis() - lastEmailTime < (EMAIL_COOLDOWN_SEC * 1000)) {
Serial.println("Email cooldown active. Skipping email.");
return;
}
// SMTPセッションの設定
Session_Config config;
config.server.host_name = SMTP_HOST;
config.server.port = SMTP_PORT;
config.login.email = AUTHOR_EMAIL;
config.login.password = APP_PASSWORD;
config.login.server_auth = true; // サーバー認証を有効にする (必須)
// メッセージの作成
SMTP_Message message;
message.sender.name = "ESP32 Door Alert";
message.sender.email = AUTHOR_EMAIL;
message.subject = "【🚨🚨 緊急アラート 🚨🚨】ドア開閉通知: " + status;
// 受信者の追加
message.addRecipient("User", RECIPIENT_EMAIL);
// メールの本文
String body = "日時: ";
body += String(millis() / 1000) + "秒後\n\n"; // 簡易的なタイムスタンプ
if (status == "OPEN") {
body += "【注意】玄関ドアが開きました!すぐに状況を確認してください。\n";
} else {
body += "ドアが閉じました(警戒モード再開)。\n";
}
message.text.content = body;
message.text.charSet = "UTF-8";
message.text.transfer_encoding = "base64";
Serial.print("Sending Email...");
if (!smtp.connect(&config)) {
Serial.println("Error connecting to SMTP server.");
return;
}
if (MailClient.sendMail(&smtp, &message, true)) {
Serial.println("Email sent successfully!");
lastEmailTime = millis();
} else {
Serial.print("Error sending email: ");
Serial.println(smtp.errorReason());
}
}
// BLE通知コールバック関数
void notifyCallback(BLERemoteCharacteristic* pBLERemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify) {
if (isNotify) {
String status = (char*)pData;
status.substring(0, length); // Null終端を削除
Serial.print("Received Status: ");
Serial.println(status);
// ドアが開いた場合にのみメールを送信
if (status.indexOf("OPEN") != -1) {
sendAlertEmail("OPEN");
} else if (status.indexOf("CLOSED") != -1) {
sendAlertEmail("CLOSED");
}
}
}
// BLE接続クラス
class MyClientCallback : public BLEClientCallbacks {
void onConnect(BLEClient* pclient) {
connected = true;
Serial.println("Successfully connected to the Server!");
}
void onDisconnect(BLEClient* pclient) {
connected = false;
Serial.println("Disconnected from Server. Will try to reconnect...");
doConnect = true; // 再接続フラグを立てる
}
};
// BLE接続処理
bool connectToServer() {
pClient->setClientCallbacks(new MyClientCallback());
// 接続を試みる
if (!pClient->connect(myDevice)) {
Serial.println("Failed to connect to the Server.");
return false;
}
// 接続成功: サービスと特性を取得
BLERemoteService* pRemoteService = pClient->getService(serviceUUID);
if (pRemoteService == nullptr) {
Serial.print("Failed to find service UUID: ");
Serial.println(SERVICE_UUID);
pClient->disconnect();
return false;
}
BLERemoteCharacteristic* pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID);
if (pRemoteCharacteristic == nullptr) {
Serial.print("Failed to find characteristic UUID: ");
Serial.println(CHARACTERISTIC_UUID);
pClient->disconnect();
return false;
}
// 通知 (Notify) を購読 (Subscribe)
if (pRemoteCharacteristic->canNotify()) {
pRemoteCharacteristic->subscribe(true, notifyCallback);
Serial.println("Successfully subscribed to notifications.");
} else {
Serial.println("Characteristic does not support Notify property.");
pClient->disconnect();
return false;
}
connected = true;
return true;
}
// BLEアドバタイズデバイススキャンクラス
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
void onResult(BLEAdvertisedDevice advertisedDevice) {
// ターゲットデバイス名のチェック
if (advertisedDevice.getName() == TARGET_DEVICE_NAME) {
Serial.print("Found target BLE Device: ");
Serial.println(advertisedDevice.toString().c_str());
// スキャンを停止し、接続フラグを立てる
BLEDevice::getScan()->stop();
myDevice = new BLEAdvertisedDevice(advertisedDevice);
doConnect = true;
}
}
};
// ===================================
void setup() {
Serial.begin(115200);
// 1. Wi-Fi接続
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("Connecting to WiFi...");
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.print(".");
}
Serial.println("\nWiFi connected.");
// 2. BLE初期化とスキャン設定
BLEDevice::init("");
pClient = BLEDevice::createClient();
BLEScan* pScan = BLEDevice::getScan();
pScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
pScan->setActiveScan(true); // アクティブスキャンで応答を取得しやすくする
pScan->setInterval(100);
pScan->setWindow(99);
}
// ===================================
void loop() {
// 接続が必要な場合(初期接続または切断後の再接続)
if (doConnect) {
if (connectToServer()) {
doConnect = false; // 接続成功
} else {
Serial.println("Re-connecting failed. Retrying scan in 5 seconds...");
delay(5000); // 接続失敗時は5秒待って再スキャン
BLEDevice::getScan()->start(10, false); // 10秒間スキャンを再開
}
}
// 接続が切断された場合
else if (!connected) {
Serial.println("Connection lost. Restarting BLE scan...");
BLEDevice::getScan()->start(10, false); // 10秒間スキャンを再開
doConnect = false;
}
// 接続が維持されている場合は特に何もしない(通知を待機)
delay(100);
}
6. まとめと次のステップ
本記事で解説したESP32×BLEシステムは、市販のスマートホーム製品に劣らない、確実で安価なホームセキュリティの基礎となります。このコードをベースに、より詳細な情報(バッテリー残量など)をBLEで送信したり、メール以外の通知方法(LINE通知など)を組み込むことも可能です。