🚪 ESP32で実現!ドア開閉を検知してGmailに即時通知するBLEスマートアラートシステム構築ガイド

【DIY】ホームセーフティ実践

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桁のアプリパスワードを控えておいてください。

  1. Googleアカウントで2段階認証を有効化します。
  2. Googleアカウントのセキュリティ設定へ移動し、「アプリ パスワード」を作成します。
  3. 生成された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_PINDEVICE_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接続が切れた場合の自動再接続ロジックを組み込んでいます。

  1. Wi-Fi接続:インターネットアクセスを確保。
  2. BLEスキャン:子機のデバイス名 (ESP32_Door_Sensor) を検索。
  3. 接続確立:子機に接続し、サービスと特性を特定。
  4. 通知の購読 (Subscribe):子機からの状態変化通知を待ち受ける。
  5. 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通知など)を組み込むことも可能です。

タイトルとURLをコピーしました