How to: ESP-Nowで超お手軽デバイス間無線通信
みんな、こんにちは!ESPマイコン同士で通信したい時、みんなは何を使っているのかな?Bluetooth?Wi-Fi?ボクのオススメはEspressifさん独自規格のESP-Nowだよ!ESP-Nowは高速応答、低消費電力が特徴で、ルーターも経由する必要もなくて、1対1はもちろん、1対多、多対多の通信が簡単にできるよ。
この記事では、ボクがM5Stack Japan Tour 2024で展示していた怪しいフィギュアスタンドでやったことをベースにESP-Nowの使い方を説明しているよ。このページではArduino IDEを使用する方法に加えて、M5StackでブロックプログラミングができるUIFlow2を使った方法も説明しているよ。UIFlow2でESP-Nowが使えるようになったことで、今までよりももっとお手軽に使えるようになっているよ!最後の方にUIFlow2でESP-Nowを使う方法を説明しているから、ぜひ参考にしてみてね!
やること
ESP32同士でESP-Nowを使用した1対1の無線通信をするよ。一方がコントローラーになってターゲットデバイス(フィギュアスタンド)に対してデータを送信、ターゲットはコントローラーから送信されたデータを受信して、受信したデータに従って接続されたRGB LEDの色や明るさを変化させるよ。
対象読者
- ESP-Nowに興味がある、使ってみたい!
- デバイス間でお手軽に無線通信したい!
- Matterの複雑さに心が折れちゃったみんな
環境
- Arduino IDE (Arduino core for the ESP32 v3.0.x)
もしくは、UIFlow2 (v2.0.7以降) - M5Dial (ESP32-S3)
- M5NanoC6 (ESP32-C6)
怪しいフィギュアスタンド
最初に怪しいフィギュアスタンドの概要について説明するよ。怪しいフィギュアスタンドは、ESP-Nowのデモ用にボクが作った怪しく光るフィギュアスタンドだよ。
怪しいフィギュアスタンドは以下のように2つのデバイスで構成されているよ。
- フィギュアスタンド (NanoC6)
- コントローラー (M5Dial)
怪しいフィギュアスタンド概略図
フィギュアスタンド (NanoC6)
怪しいフィギュアスタンドはフィギュアスタンド側にM5StackさんのNanoC6を使っていて、NanoC6のGrove端子にはAdafruitさんのNeoPixel Ring(12連)が接続されているよ。NanoC6がNeoPixelの色や明るさを調整してフィギュアスタンドの下から色んな色で照らせるようになっているよ。
コントローラー (M5Dial)
フィギュアスタンドの色や明るさを調整するためのコントローラーとしてM5StackさんのM5Dialを使用しているよ。M5Dialのダイアルを回すことでフィギュアスタンドの色や明るさを調整することができるようになっているよ。M5DialとNanoC6はESP-Nowを使用してデータをやり取りしていて、M5Dial側でダイアルを回した時の値をNanoC6へ送信できるようになっているよ。
ESP-Nowで通信するときは MAC address を指定することで、特定のデバイス宛にデータを送信することができるよ。上の図では、M5DialからESP-Nowでデータを送信するときはNanoC6の MAC address を指定しているよ。
MAC addressって?
MAC address はネットワークに接続するデバイスに割り当てられた物理的なアドレスだよ。このアドレスは、ネットワークインタフェース1つにつき1つのアドレスが割り当てられていて、世界中のどのデバイスとも重複しないように割り当てられているよ。
IP addressも聞いたことがあると思うけど、MAC address はネットワーク内でデバイスを特定するための固定の識別子だよ。IP addressはネットワークの構成や場所に応じて変わることがあるけど、MAC address はほとんどの場合、デバイスが製造された時点で決まっていて、変更されることは無いよ。
ESP-Nowって?
さっきから何度もESP-Nowって言っているけど、そもそもESP-Nowが何なのかについて説明するね。ESP-NowはESPシリーズのマイコンを開発しているEspressifさんがWi-Fiの規格をベースに独自に作った高速応答、低消費電力が特徴の無線通信プロトコルだよ。
👆に詳しい説明があるんだけど、ここではざっくりとESP-Nowについて説明するね。
特徴
ESP-Nowには以下のような特徴があるよ。
-
高速応答、低消費電力
ESP-Nowの応答時間は150ms以内(例えばスイッチON/OFFのデータをターゲットに送信してからターゲットが反応するまでの時間)だから、IoTデバイスの制御や少量のデータを送る用途では全く問題なく使えるよね。XだとESP-Nowで動画の伝送にトライしている人もいたよ👀また、消費電力が少ないのも特徴で、具体的には、スリープ状態にしたESP32-C2でESP-Nowを使って1日に10回スイッチのOn/Offを行った場合、CR2032電池1個で最大5年間動作するぐらいの消費電力だよ。 -
ルーター不要でセットアップが簡単
ESP-NowはWi-Fiネットワークを使わない独立した通信方式で、Wi-Fiルーターやアクセスポイントを用意する必要がないよ。セットアップもシンプルで、1対1、1対多、多対多の通信をすぐに開始できるのはとっても便利だよね。 -
200mまで到達可能
障害物の無い環境だとESP-Nowを使ったデバイス間の通信距離は最大で200m。お家の中でIoTデバイスを制御する分には問題なく使えそうだよね。到達可能な距離は障害物や電波環境にも依存するから、必ず200m届くことが保証されているわけでは無いから、その点は注意してね。
対応マイコン
ESP-NowはEspressifさんの以下のマイコンで使用可能だよ。
- ESP8266
- ESP32
- ESP32-S
- ESP32-C
通信の種類
ESP-Nowで使用できる通信の種類は以下の通りだよ。
種類 | 暗号化 | 接続台数 |
---|---|---|
1対多(含む1対1) | なし | 20台 |
1対多(含む1対1) | あり | 6台 |
ブロードキャスト | なし | 無制限 |
ESP-Nowはピア(送信先)として暗号化なしの場合は20台まで、暗号化ありの場合は6台まで登録できるようになっているよ。1対1の通信は単にピアが1つしか登録されていない場合に1対1の通信になるよ。最後のブロードキャストは、ピアとしてブロードキャストアドレス (FF:FF:FF:FF:FF:FF) という特別なアドレスを指定すると周辺にある全てのデバイスに対してデータを送信することができるよ。ESP-Nowの到達可能な200m以内にあるデバイスであれば数に制限はないよ。
制約
超お手軽に使えるESP-Nowだけど、以下のような制約もあるから、用途に合わせてESP-Nowで不十分な場合はWi-FiやBluetoothも検討してみてね。
-
1回で送信できるデータは最大250バイト
ESP-NowはWi-Fiのvendor-specific element fieldというWi-Fiの規格に元からある仕組みを使用してデータをやり取りしていて、このvendor-specific element fieldの長さが最大255バイトで宛先とか色んなデータを付加して、余った250バイトをデータの送信に使用しているから1回で送信できるデータは250バイトが最大だよ。ただ、データは何回でも送れるから、250バイトのデータを繰り返し送信することは可能だよ。 -
Wi-Fiと一緒に使用する場合はWi-Fi APのチャンネルと一緒にする
もしもESP-NowとWi-Fiを同時にしたいと思った場合、同時に使用することはできるんだけど、ESP-Nowで使用するチャンネルと、Wi-Fiのアクセスポイント(AP)に接続するために使用しているチャンネルは同じチャンネルにする必要があるよ。
ESP-Nowで通信する
ここからは実際にESP-Nowで通信するための方法について説明していくよ。ESP-Nowで通信するための流れは以下の通りだよ。
初期化(共通)
- Wi-FiモードをWIFI_STAに変更
- EPS-Nowを初期化
送信側
- ピア(送信先)を登録
- データ送信時に呼び出されるコールバック登録
- データ送信
受信側
- データ受信時に呼び出されるコールバック登録
- コールバック内でデータ受信処理
今回は送信側と受信側ではっきりと分かれているけど、1つのデバイスで送受信の両方に対応することも可能だよ。送受信の両方に対応するには、送信側、受信側でやっていること両方のコードを書けばOKだよ。
初期化(共通)
まずはESP-Nowを使えるように初期化する手順について説明するね。初期化の手順は送信側も受信側も共通だから、両方のsetup()に同じ初期化のコードを追加してね。コードの説明は以下に直接追加しているから確認してみてね。
// ESP-NowはArduinoIDEのボードマネージャで "esp32 by espressif" を
// 入れておけば以下の2つのヘッダファイルをインクルードするだけで使用可能だよ。
#include <esp_now.h>
#include <WiFi.h>
void setup() {
...
// 1. Wi-FiモードをWIFI_STAに変更
// WiFi.mode()の引数にWIFI_STAを指定することで、Wi-FiのモードをWIFI_STAにするよ。
WiFi.mode(WIFI_STA);
// 2. EPS-Nowを初期化
// esp_now_init()を呼び出すことでESP-Nowを初期化しているよ。
if(esp_now_init() != ESP_OK){
M5.Log(ESP_LOG_ERROR, "Error initializing ESP-NOW");
return;
}
...
}
送信側
次にデータを送信する側のコードだよ。以下のコードでピア(送信先)の登録とデータの送信時に呼び出されるコールバックの登録、データの送信をしているよ。コードの説明はコメントとして直接記載しているよ。
// ターゲット(フィギュアスタンド)の MAC address
uint8_t targetAddress[] = {0x40, 0x4C, 0xCA, 0x5B, 0x1B, 0x94};
// 送信データの構造体。
// 今回はフィギュアスタンド側で点灯するRGB LEDの色と明るさを制御したいから、
// modeで色、brightnessで明るさのデータを定義しているよ。
// データの構造はみんなが自分で送りたいデータの形に合わせて自由に作ってね。
// ただし、ESP-Nowで送信可能なデータサイズは250バイトまでってことは覚えておいてね。
struct ControlData{
uint8_t mode;
uint8_t brightness;
};
// ピア(送信先)の情報を保持するための変数だよ。
esp_now_peer_info_t peerInfo;
// ESP-Nowでデータを送信した後に呼び出されるコールバック関数だよ。
// statusの値をチェックすることで送信が成功したかどうかが分かるよ。
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
M5.Log(ESP_LOG_DEBUG, status == ESP_NOW_SEND_SUCCESS ? "ESPNow data send success" : "ESPNow data send failed");
}
// ESP-Nowでデータを送信するための関数を定義しているよ。
// 上で定義しているControlDataに値をセットしてesp_now_send()関数に
// 渡してあげることでESP-Nowでデータを送信することができるよ。
void sendControlData(){
ControlData data = {
.mode = 1,
.brightness = 5
};
esp_now_send(targetAddress, (uint8_t*)&data, sizeof(data));
}
void setup() {
..
// ESP-Nowの初期化後に以下のコードを記述するよ。
// 1. ピア(送信先)を登録
// targetAddressにはターゲットのMAC addressが格納されていて、この値をpeerInfoにコピーしているよ。
memcpy(peerInfo.peer_addr, targetAddress, 6);
peerInfo.channel = 0; // Wi-Fiのチャンネル
peerInfo.encrypt = false; // 暗号化する or しない
// esp_now_add_peer()を呼び出すことで、ターゲットのアドレスを登録するよ。
// もし、ターゲットが2台以上あるときは、ターゲットの数だけesp_now_add_peer()を呼び出すよ。
if (esp_now_add_peer(&peerInfo) != ESP_OK){
M5.Log(ESP_LOG_ERROR, "Failed to add peer");
return;
}
// 2. データ送信時に呼び出されるコールバック登録
// データの送信が成功したかどうかを知りたいときはコールバック関数を定義して、
// esp_now_register_send_cb()に渡してあげることで、ESP-Nowで送信した後に
// コールバック関数が呼び出されて送信結果を得ることができるよ。
esp_now_register_send_cb(OnDataSent);
..
}
void loop() {
M5.update();
// 3. データ送信
// ボクはデータ送信用にsendControlData()という関数を定義したよ。
// 詳細は上の方に書いているsendControlData関数の中身を見てみてね。
sendControlData()
delay(2000);
}
受信側
最後にデータを受信する側のコードだよ。以下のコードでデータの受信時に呼び出されるコールバックの登録、データの受信をしているよ。受信側は送信側よりもっと簡単だよ。
// こちらは送信側で定義したものと同じ構造体だよ。
// ホントはヘッダにまとめた方が良いと思うけど、デモコードだから全く同じ定義を書いているよ。
struct ControlData{
uint8_t mode;
uint8_t brightness;
};
// ESP-Nowで受信したデータを格納するための変数だよ。
ControlData pixel_settings = {
.mode = 0,
.brightness = 5
};
// 2. コールバック内でデータ受信処理
// ちょっと順番が前後しちゃうけど、このOnDataRecv関数がESP-Nowでデータを受信
// したときに呼び出されるように後ろの方にあるsetup関数内で登録しているよ。
void OnDataRecv(const esp_now_recv_info_t *esp_now_info, const uint8_t *receiveData, int data_len) {
ControlData data;
if(data_len == sizeof(ControlData)){
// ESP-Nowで受信したデータの長さとControlDataの構造体の長さが一致していれば、
// 受信したデータをpixel_settingsという変数へコピーするよ。
// コピーした後は、受信したデータに基づいて色んな処理ができるよね!
memcpy(&pixel_settings, receiveData, data_len);
}else{
M5.Log(ESP_LOG_ERROR, "Received data size does not match ControlData");
}
}
void setup() {
...
// ESP-Nowの初期化後に以下のコードを記載。
// 1. データ受信時に呼び出されるコールバック登録
// ESP-Nowでデータを受信したときに呼び出されるコールバック関数を登録しているよ。
esp_now_register_recv_cb(OnDataRecv);
...
}
void loop() {
...
}
UIFlow2でもっと簡単に
ここまではArduino IDEを使ってESP-Nowのコードを作成していたけど、ブロックを組み合わせることでM5StackのプログラミングができるUIFlow2でもESP-Nowが使えるよ。ここまでで説明してきたものと同じくフィギュアスタンドとコントローラの組み合わせで使えるようなUIFlow2のコードを紹介するよ。
ESP-Nowの処理を追加するには、UIFlow2を開いて、WLANの下からESP-Nowのブロックを選んでいくよ。
送信側
ESP-Nowでデータを送信する側の処理の例だよ。ESP-Nowに関係するところをピンクの枠で囲んでいるよ。
送信側の初期化処理
Init ESP-NOW wifi channel [0] (0-14)
ESP-Nowで使用するWi-Fiのチャンネルを指定するよ。このチャンネルは受信側で設定するWi-Fiのチャンネル番号と一致させる必要があるよ。お家の中だと0でも特に問題ないと思うけど、周りにたくさんのWi-Fiを使うデバイスがある場合はこの番号を調整する必要があるかもしれないね。
ESP-NOW add peer MAC address "XX:XX:XX:XX:XX:XX"
データを送信する送信先のデバイスの MAC address を指定するよ。MAC address はM5Stack製品のパッケージのラベルかUIFlow2のデバイスの設定画面から確認することができるよ。peer IDはピア(送信先)を識別するためのIDで、1つの送信先に対して1つのIDを設定するよ。ifidxはSTA_IFにしておくよ。
次にデータを送信する時の処理だよ。以下の画像はLoopの中でデータを送信しているよ。
データの送信
Set [data] to create empty bytearray with length [0]
ボクは3バイトのデータを送信したかったから、dataというバイト配列を作成しているよ。このブロックはdataという変数を0個のバイト配列で初期化しているよ。
Bytearray[data] append [xxx]
さっき初期化した3バイトのバイト配列に3つのデータを追加しているよ。1つ目がmodeの値、2つ目を5、3つ目を0にしているよ。送信したいデータが1バイトしかない場合はこのブロックは不要だよ。
ESP-NOW send data peer ID [1] (1-20) data [data]
data変数の中身をESP-Nowで送信するためのブロックだよ。peer IDは最初の初期化時に指定したIDと同じIDを指定するよ。複数の送信先がある場合は送信先の数だけこのブロックを使うことになりそうだね。
受信側
ESP-Nowでデータを受信する側の処理の例だよ。ESP-Nowに関係するところをピンクの枠で囲んでいるよ。
受信側の初期化処理
Init ESP-NOW wifi channel [0] (0-14)
ESP-Nowで使用するWi-Fiのチャンネルを指定するよ。このチャンネルは送信側で設定したWi-Fiのチャンネル番号と一致させる必要があるよ。
次に、ESP-Nowで受信したデータを処理する方法を説明するよ。
受信処理
When ESP-NOW receive data
このブロックはESP-Nowのデータを受信したときに実行されるブロックだよ。MACに接続された変数ブロックに送信先の MAC address が、dataに接続された変数に受信したデータが格納されているよ。受信処理はLoopのブロックとは独立
Set [mode] to in list [espnow_data] [get] [first]
この例では3バイトのデータが送られてくるはずだから、espnow_data変数の1番最初の1バイトを取り出してmode変数に格納しているよ。この後に続く処理で、mode変数の値ごとに処理を振り分けているよ。
ここまでで、UIFlow2からESP-Nowを使った通信する方法について説明したよ。送信と受信ができると、後は送信するデータを自分の好きなデータに変更して、受信側で受け取ったデータに応じた処理に変更してみてね。
混信を避けるには
ESP-Nowはブロードキャスト通信という全てのデバイスに対して送信する方法もあるというのは、さっき説明した通りだよ。お家の中でESP-Nowを使う分にはほとんど問題にはならないと思うけど、不特定多数のESPデバイスが存在する場所(展示会など)では、このブロードキャスト通信によって、知らないデバイスからの通信を受け取る可能性があることを覚えておいてね。
ESP-Nowで混信を避けるには、受信用に使用するコールバック関数の第1引数に宛先のアドレスが入っているから、この値と自分の MAC address を比較することで、自分宛のデータのみをフィルタリングすることができるよ。
void OnDataRecv(const esp_now_recv_info_t *esp_now_info, const uint8_t *receiveData, int data_len)
esp_now_info->des_addr
des_addrに送信時に指定された宛先の MAC address が格納されているよ。
まとめ
今日はESP同士で超お手軽に無線通信ができるESP-Nowの概要や使い方について説明したよ。無線通信って言うと複雑なコードが必要なイメージかもしれないけど、ESP-Nowはとっても少ないコードでお手軽に無線通信ができることが分かってもらえたかな?ESP-Nowも万能ではないから、みんながやりたいことと照らし合わせてみて、使えそうならぜひ使ってみてね!
じゃあ、またね!
Discussion