M5StickCでESP-NOW その1

現時点の情報です。最新情報はM5StickC非公式日本語リファレンスを確認してみてください。

概要

省電力でWi-Fi通信ができるESP-NOWを、2台のM5StickCに同じプログラムを入れて、ブロードキャスト通信ができるところまでの検証をしました。

ESP-NOWとは?

ESP32の開発会社であるEspressifが開発した通信方式で、内容的にはIEEE 802.11のCCM Protocolを使って通信しているようです。

内容的にはWi-fiでMACアドレス宛に直接通信をしている方式であり、到達性は悪いですが、LoRaなどに近い省電力用の通信方式です。

SlaveとController

ESP32のスケッチ例だとSlaveとMasterという用語を使っていますが、主にハブとなる電源につながっている送受信装置をSlave。端末側をControllerと表現することが多いようです。

ESP-NOW自体にはSlaveとControllerの区別はありませんが、概念としては理解しておいたほうがよいです。

Slaveの役割

通信のハブとして動き、Controllerに接続されているセンサー情報などを受信したり、Controllerへの制御を行ったりします。

電源に接続されていることが多く、基本的には常に送受信が可能な状態になっていることが多いです。

Controllerの役割

センサーなどを搭載した端末などであり、センサー情報を定期的にSlaveに送信したあとは電源スリープするなど、省電力で動いていることが多いです。

送信先確定の仕組み

基本的にはWi-fiのMACアドレス宛に直接送信をするので、SlaveとControllerの差はありません。しかしながら、Controllerが送信すべきSlaveのMACアドレスをどう設定するかによって、実装に差がでてきます。

ブロードキャストする

すべてFFのMACアドレスに送信することで、すべての端末にESP-NOWを送信することができます。事前に設定をすることもないですし、非常にお手軽に利用できます。

しかしながら、すべての端末に届くと受信側の消費電力があがったり、不要な通信を送信してしまうデメリットがあります。

SOFTAPサーバー

ESP32のスケッチ例がこのタイプですが、Slave側がSOFTAPモードでWi-Fiアクセスポイントとして動作します。

Controller側から周りにあるWi-Fiアクセスポイントの一覧をスキャンして、条件に一致するアクセスポイント名をSlaveとして、該当MACアドレスを送信先に指定します。

事前書き込み等

あらかじめSlaveのMACアドレスをプログラムの中などで指定して送信します。NVSなどに保存しておくことで変更も可能になります。

ESP-NOW スケッチ例

#include <M5StickC.h>
#include <esp_now.h>
#include <WiFi.h>
esp_now_peer_info_t slave;
// 送信コールバック
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
  char macStr[18];
  snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X",
           mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
  Serial.print("Last Packet Sent to: ");
  Serial.println(macStr);
  Serial.print("Last Packet Send Status: ");
  Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Delivery Success" : "Delivery Fail");
  // 画面にも描画
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setCursor(0, 0);
  M5.Lcd.print("Last Packet Sent to: \n  ");
  M5.Lcd.println(macStr);
  M5.Lcd.print("Last Packet Send Status: \n  ");
  M5.Lcd.println(status == ESP_NOW_SEND_SUCCESS ? "Delivery Success" : "Delivery Fail");
}
// 受信コールバック
void OnDataRecv(const uint8_t *mac_addr, const uint8_t *data, int data_len) {
  char macStr[18];
  snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X",
           mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
  Serial.printf("Last Packet Recv from: %s\n", macStr);
  Serial.printf("Last Packet Recv Data(%d): ", data_len);
  for ( int i = 0 ; i < data_len ; i++ ) {
    Serial.print(data[i]);
    Serial.print(" ");
  }
  Serial.println("");
  // 画面にも描画
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setCursor(0, 0);
  M5.Lcd.print("Last Packet Recv from: \n  ");
  M5.Lcd.println(macStr);
  M5.Lcd.printf("Last Packet Recv Data(%d): \n  ", data_len);
  for ( int i = 0 ; i < data_len ; i++ ) {
    M5.Lcd.print(data[i]);
    M5.Lcd.print(" ");
  }
}
void setup() {
  M5.begin();
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setRotation(3);
  M5.Lcd.print("ESP-NOW Test\n");
  // ESP-NOW初期化
  WiFi.mode(WIFI_STA);
  WiFi.disconnect();
  if (esp_now_init() == ESP_OK) {
    Serial.println("ESPNow Init Success");
    M5.Lcd.print("ESPNow Init Success\n");
  } else {
    Serial.println("ESPNow Init Failed");
    M5.Lcd.print("ESPNow Init Failed\n");
    ESP.restart();
  }
  // マルチキャスト用Slave登録
  memset(&slave, 0, sizeof(slave));
  for (int i = 0; i < 6; ++i) {
    slave.peer_addr[i] = (uint8_t)0xff;
  }
  esp_err_t addStatus = esp_now_add_peer(&slave);
  if (addStatus == ESP_OK) {
    // Pair success
    Serial.println("Pair success");
  }
  // ESP-NOWコールバック登録
  esp_now_register_send_cb(OnDataSent);
  esp_now_register_recv_cb(OnDataRecv);
}
void loop() {
  M5.update();
  // ボタンを押したら送信
  if ( M5.BtnA.wasPressed() ) {
    uint8_t data[2] = {123, 234};
    esp_err_t result = esp_now_send(slave.peer_addr, data, sizeof(data));
    Serial.print("Send Status: ");
    if (result == ESP_OK) {
      Serial.println("Success");
    } else if (result == ESP_ERR_ESPNOW_NOT_INIT) {
      Serial.println("ESPNOW not Init.");
    } else if (result == ESP_ERR_ESPNOW_ARG) {
      Serial.println("Invalid Argument");
    } else if (result == ESP_ERR_ESPNOW_INTERNAL) {
      Serial.println("Internal Error");
    } else if (result == ESP_ERR_ESPNOW_NO_MEM) {
      Serial.println("ESP_ERR_ESPNOW_NO_MEM");
    } else if (result == ESP_ERR_ESPNOW_NOT_FOUND) {
      Serial.println("Peer not found.");
    } else {
      Serial.println("Not sure what happened");
    }
  }
  delay(1);
}

サンプルで組んでみたスケッチです。SlaveとControllerの区別はなく、ブロードキャストで送受信しています。BtnAを押すことで送信し、受信すると受信結果を画面に表示しています。

ESP-NOWの流れ

無線初期化

  • WiFi.mode(WIFI_STA)
  • WiFi.disconnect()
  • esp_now_init()

Wi-FiとESP-NOWを初期化します。Wi-Fiは接続しっぱなしだと消費電力が高いのでdisconnect()しているのだと思います。

送信先登録

  • esp_now_add_peer()

あらかじめ送信する先を登録する必要があります。最大20箇所まで登録が可能で、登録の削除や変更も可能です。

登録時の設定でWi-Fiで送信する電波チャンネル数や、通信の暗号化などの設定も行います。

コールバック関数登録

  • esp_now_register_send_cb()
  • esp_now_register_recv_cb()

送信コールバックは、送信したあとに送信結果を受信します。ブロードキャスト宛に送信している場合には宛になりませんが、MACアドレス宛の場合には送信ができたのかを確認することができます。

受信コールバックは、受信時に呼ばれます。ポーリングでの受信確認はできず、すべてコールバック関数により受信をする必要があります。

コールバック関数自体は送信だけして、送信結果がいらないのであれば両方登録する必要はありません。

送信

  • esp_now_send()

送信先MACアドレスを指定して、250バイトまでのデータを送信可能です。送信が完了したかは送信コールバックで確認が必要です。

連続送信をすると、コールバックの順番がおかしくなる場合があるので、1つ送信した場合には、送信コールバックが呼び出されるまで待機するのが推奨ですが、コールバックの内容が必要なければ連続送信も可能そうです。

受信

受信コールバック関数で処理されます。送信元MACアドレスもわかるので、ブロードキャストでSlaveやControllerを探すことも可能です。

まとめ

非常にかんたんにESP-NOWを利用することができました。ESP32とESP8266しかESP-NOWは利用できませんが、複数台のセンサーからデータを手軽に集めるのには使いやすいかもしれません。

続編

コメント

  1. […] ●Lang-Ship様に感謝 サンプルプログラムだけで複数台接続が自由にできます。 https://lang-ship.com/blog/work/m5stickc-esp-now-1/ […]

  2. […] ●Lang-Ship様に感謝 サンプルプログラムだけで複数台接続が自由にできます。 https://lang-ship.com/blog/work/m5stickc-esp-now-1/ […]