「ドアが開いた瞬間に、離れた部屋のブザーが鳴る」—そんなシンプルなIoTシステムを、 ESP32とESP-NOW(エスピー・ナウ) という強力なコンビで実現する方法を紹介します。
このシステムは、Wi-Fiルーターを経由せず、デバイス間で直接通信を行うため、超低遅延で省電力、しかも初期設定が非常に簡単なのが最大の魅力です。
この記事では、具体的な配線と、子機(ドアセンサー)と親機(ブザー)それぞれの完全なプログラムコードを提供します。
💡 プロジェクトの概要とESP-NOWのメリット
プロジェクトの目的:シンプルなワイヤレスドアベル
本プロジェクトは、以下の2つのESP32モジュールで構成されます。
- 子機(送信側):ドアに設置し、開閉状態を検知して親機へデータを送信します。
- 親機(受信側):ブザーを接続し、子機からのドア開信号を受け取ったらブザーを鳴らします。
ESP-NOWを選ぶ理由
なぜ通常のWi-Fi通信(MQTTやHTTP)ではなくESP-NOWを選ぶのでしょうか?
特徴 | ESP-NOW | 一般的なWi-Fi通信 (MQTT/HTTP) |
遅延 | 数ミリ秒(超高速) | 数百ミリ秒~数秒 |
ルーター | 不要(デバイス間直接通信) | 必要 |
消費電力 | 低 | 高 |
設定 | 相手のMACアドレスを登録するだけ | Wi-Fi SSID、パスワード、サーバーアドレスなどが必要 |
ESP-NOWは、非常に高速な応答が必要な「ドア開閉通知」や「ボタン入力」といった用途に最適なのです。
🛠 準備するものリストと配線図
必須コンポーネント
部品名 | 数量 | 備考 |
ESP32 開発ボード | 2個 | ESP-WROOM-32などが一般的です。 |
ブザーモジュール | 1個 | アクティブブザー(電源を入れると鳴るタイプ)が簡単です。 |
リードスイッチ/磁気センサー | 1個 | ドアの開閉検知に使用します。 |
ジャンパー線 | 数本 | 配線用。 |
ブレッドボード | 2枚 | 部品の仮固定と配線に使います。 |
USB ケーブル | 2本 | ESP32への書き込みと電源供給用。 |
🔌 配線ガイド
1. 親機(受信側/ブザー)の配線
ESP32ピン | 接続先 | 役割 |
GPIO 27 | ブザーの信号ピン | ブザーを鳴らす制御信号。 |
GND | ブザーの$\text{GND}$ | |
3.3V または 5V | ブザーの$\text{VCC}$ | (アクティブブザーの場合) |
2. 子機(送信側/ドアセンサー)の配線
今回は、シンプルなデジタル入力として INPUT_PULLUP を使用します。これにより、外付けの抵抗が不要になります。
ESP32ピン | 接続先 | 役割 |
GPIO 4 | リードスイッチの片側 | 信号入力ピン。 |
GND | リードスイッチのもう片側 |
🔑 センサーの動作原理 $\text{INPUT\_PULLUP}$を設定した$\text{GPIO}\ 4$は、センサーが開いている(磁石が離れている)ときは内部抵抗により$\text{HIGH}$(ドア閉)を示します。センサーが閉じている(磁石が近づいている)と、$\text{GND}$に接続され$\text{LOW}$(ドア開)を示します。この「$\text{LOW}$になったら開いた」という信号をプログラムで処理します。
💻 プログラム実装:親機の MAC アドレス取得から始める
ESP-NOW通信を行うには、子機が親機の MAC アドレスを知っている必要があります。
Step 1: 親機の MAC アドレスを確認
まずは親機側のESP32に以下のコードを書き込み、シリアルモニタで$\text{MAC}$アドレスを取得します。
C++
#include <WiFi.h>
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA); // ステーションモードに設定
Serial.print("==============================\n");
Serial.print("Receiver MAC Address: ");
Serial.println(WiFi.macAddress()); // MACアドレスを表示
Serial.print("==============================\n");
}
void loop() {
// 一度表示したらOK
}
表示された MAC アドレス(例:AA:BB:CC:DD:EE:FF)をメモしておいてください 。これは後の子機プログラムで必要になります。
3. 親機(受信側)の完全プログラム
親機は、子機から送信されるデータ構造体を共有し、データを受信するためのコールバック関数を定義します。
C++
#include <esp_now.h>
#include <WiFi.h>
// ブザーを接続するGPIOピン(例: GPIO 27)
const int BUZZER_PIN = 27;
// ★★★ 共有するデータ構造体 ★★★
// 送信側と受信側で完全に一致させる必要があります。
typedef struct struct_message {
bool door_open; // ドアが開いているかどうかのフラグ (true=開 / false=閉)
} struct_message;
struct_message myData; // 受信データ格納用の変数
// データを受信したときに呼ばれるコールバック関数
void OnDataRecv(const uint8_t * mac_addr, const uint8_t *incomingData, int len) {
// 受信したデータを myData 構造体にコピー
memcpy(&myData, incomingData, sizeof(myData));
// 受信データの表示(デバッグ用)
Serial.print("Data received from MAC: ");
for (int i = 0; i < 6; i++) {
Serial.printf("%02X", mac_addr[i]);
if (i < 5) Serial.print(":");
}
Serial.println();
Serial.print("Door Open Status: ");
Serial.println(myData.door_open ? "OPEN (Signal ON)" : "CLOSED (Signal OFF)");
// ドアが開いた信号を受信したらブザーを鳴らす
if (myData.door_open) {
Serial.println(">>> ALERT: Door Open Signal Received! Beeping...");
// tone(ピン番号, 周波数, 鳴らす時間[ms])
tone(BUZZER_PIN, 1500, 500); // 1.5kHzの音を0.5秒間鳴らす
} else {
// ドアが閉じた信号
Serial.println("Door Closed/State Reset.");
}
}
void setup() {
Serial.begin(115200);
// Wi-Fiをステーションモードに設定(ESP-NOWの前提条件)
WiFi.mode(WIFI_STA);
// ブザーピンを出力に設定
pinMode(BUZZER_PIN, OUTPUT);
digitalWrite(BUZZER_PIN, LOW); // 初期状態ではブザーをオフ
// ESP-NOWの初期化
if (esp_now_init() != ESP_OK) {
Serial.println("FATAL ERROR: Failed to initialize ESP-NOW");
return;
}
// データ受信時のコールバック関数を登録
esp_now_register_recv_cb(OnDataRecv);
Serial.println("ESP-NOW Receiver (Parent) Initialized.");
}
void loop() {
// ブザーが鳴っている間も他の処理をブロックしないため、
// loop()では特別な処理は不要です。
delay(10);
}
4. 子機(送信側)の完全プログラム
子機は、センサーの状態を監視し、状態が変化したときだけ親機へデータを送信します(省電力の工夫)。
C++
#include <esp_now.h>
#include <WiFi.h>
// ★★★ ここに Step 1 で取得した親機(受信側)の MAC アドレスを設定 ★★★
// 例: AA:BB:CC:DD:EE:FF
uint8_t broadcastAddress[] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF};
// ドアセンサーを接続するGPIOピン(例: GPIO 4)
const int DOOR_SENSOR_PIN = 4;
// センサー入力モード: 内部プルアップ抵抗を使用
const int SENSOR_MODE = INPUT_PULLUP;
// ★★★ 共有するデータ構造体 ★★★
// 受信側と完全に一致させる必要があります。
typedef struct struct_message {
bool door_open;
} struct_message;
struct_message myData;
// 状態変化を検出するための変数
bool lastDoorState = false; // true=開 / false=閉
// データ送信後に呼ばれるコールバック関数(送信成功/失敗を確認)
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
Serial.print("Last Packet Send Status: ");
if (status == ESP_NOW_SEND_SUCCESS) {
Serial.println("Delivery Success");
} else {
Serial.println("Delivery Fail - Retrying...");
// 失敗時はここで再送信ロジックを追加できます
}
}
void setup() {
Serial.begin(115200);
// Wi-Fiをステーションモードに設定
WiFi.mode(WIFI_STA);
// ドアセンサーピンを入力に設定(プルアップ有効)
pinMode(DOOR_SENSOR_PIN, SENSOR_MODE);
// ESP-NOWの初期化
if (esp_now_init() != ESP_OK) {
Serial.println("FATAL ERROR: Failed to initialize ESP-NOW");
return;
}
// データ送信後のコールバック関数を登録
esp_now_register_send_cb(OnDataSent);
// 親機をピア(通信相手)として登録
esp_now_peer_info_t peerInfo;
peerInfo.channel = 0;
peerInfo.encrypt = false;
memcpy(peerInfo.peer_addr, broadcastAddress, 6); // MACアドレスをコピー
if (esp_now_add_peer(&peerInfo) != ESP_OK){
Serial.println("ERROR: Failed to add peer (Check MAC Address!)");
return;
}
Serial.println("ESP-NOW Sender (Child) Initialized.");
Serial.print("Monitoring Door Sensor on GPIO ");
Serial.println(DOOR_SENSOR_PIN);
}
void loop() {
// センサーの状態を読み取る
// INPUT_PULLUPの場合: LOW = 磁石接近(GND接続) = ドア開
bool currentDoorState = (digitalRead(DOOR_SENSOR_PIN) == LOW);
// ★★★ 状態変化検出ロジック ★★★
// 状態が前回と異なる場合にのみ通信を実行する
if (currentDoorState != lastDoorState) {
lastDoorState = currentDoorState; // 状態を更新
// 送信データを設定
myData.door_open = currentDoorState;
// データの送信
esp_err_t result = esp_now_send(broadcastAddress, (uint8_t *) &myData, sizeof(myData));
if (result == ESP_OK) {
Serial.print("State Change Detected. Sending: Door Open ");
Serial.println(myData.door_open ? "YES" : "NO");
} else {
Serial.println("WARNING: Sending Error Occurred.");
}
// 連続送信(チャタリング)を防ぐためのディレイ
delay(500);
}
// センサーの監視間隔
delay(50);
}
❓ トラブルシューティング Q&A
Q1: ブザーが鳴りっぱなしになってしまう。
A1: アクティブブザー(電源を入れると鳴り続けるタイプ)の場合、親機側のプログラムでtone()
関数を使っています。この関数は、指定した時間(例:500ms)が経過すると自動的に音が止まります。もし止まらない場合は、配線が間違っているか、または$\text{GPIO}ピンが意図せず\text{HIGH}$のままになっている可能性があります。tone()
ではなくdigitalWrite(BUZZER_PIN, HIGH);
を使っている場合は、必ずdelay()
後にdigitalWrite(BUZZER_PIN, LOW);
で止める処理が必要です。
Q2: 子機で「Failed to add peer」と表示される。
A2: 親機の MAC アドレスが正しくありません。以下の点を確認してください。
- 子機プログラムの
broadcastAddress[]
配列に、親機のMACアドレスが正しく$\text{0x}$形式で入力されているか。 - 親機側が一度でも実行され、$\text{MAC}$アドレスが表示されたか。
Q3: 子機は「Delivery Success」なのに親機が反応しない。
A3: 最も多い原因は、データ構造体の不一致です。
- 親機と子機のプログラムで定義している
struct_message
の中身(変数名、変数の型、変数の順序)が完全に一致しているか確認してください。 - 親機と子機が同じ Wi-Fi チャンネル(通常は
channel = 0
)を使用しているか確認してください。
Q4: 通信距離を伸ばしたい。
A4: ESP-NOWは比較的遠くまで届きますが、通信が不安定な場合は以下の対策を試してください。
- ESP32の$\text{Wi-Fi}$出力を最大にするコードを追加する。
- 外部アンテナ接続用のESP32モジュール($\text{U.FL/IPEX}$コネクタ付き)を使用する。
- 可能であれば、親機と子機の間に遮蔽物(特に金属や水)を置かないように配置を工夫する。
✨ 応用例:システムをさらにスマートに
このシンプルなESP-NOWシステムを拡張すれば、より高度なスマートホームシステムを構築できます。
応用例 | 必要な追加コンポーネント | 拡張のポイント |
バッテリー駆動のドアセンサー | Li-Po バッテリー、充電回路 | 子機プログラムに$\text{Deep Sleep}を追加し、状態変化時のみ起動するように変更する(\text{Wake-up Source}は外部\text{Interrupt}$)。 |
静かな通知システム | LED や OLED ディスプレイ | 親機側でブザーの代わりに、LED を点滅させるか、ディスプレイに「ドアが開きました」と表示させる。 |
セキュリティ強化 | RTC(リアルタイムクロック) | 子機に時刻情報を追加で送信させ、親機で「夜間の開閉のみ」通知するようにロジックを組む。 |
ESP-NOWは、その手軽さと信頼性から、センサーネットワーク構築の基盤として非常に優秀です。ぜひ、この記事のプログラムをベースに、あなたのアイデアを形にしてみてください!