arduino-esp32 v3でのESP-NOW研究 その3 速度別送信成功率

概要

前回はピアまわりを調べてみましたが、今回は送信速度まわりを確認しました。送信サイズと送信間隔、送信速度を変えながら送信成功率を確認しています。

送信成功

ESP-NOWの場合にはピアに対して送信した場合には、受信したピアから受信成功のパケットを受け取っています。ブロードキャスト宛の送信だとこの受信確認はありません。

そして、受信が成功していても戻りの成功パケットが受信できない場合があるので、受信成功パケットが届かない場合に必ずしも受信側デバイスに届いていないわけではありません。

受信側スケッチ

#include "ESP32_NOW.h"
#include "WiFi.h"
#include "EspEasySerialCommand.h"

EspEasySerialCommand command(Serial);

#define ESPNOW_WIFI_CHANNEL 4  // 1 - 14

#define ESPNOW_ADVERTISING_STRING "ADVERTISING_ESP32_ESPNOW_STRING"

uint16_t receiveCount;

class MY_ESP_NOW_Peer : public ESP_NOW_Peer {
public:
  MY_ESP_NOW_Peer(const uint8_t *mac_addr, uint8_t channel, wifi_interface_t iface, const uint8_t *lmk)
    : ESP_NOW_Peer(mac_addr, channel, iface, lmk) {}
  ~MY_ESP_NOW_Peer() {
  }
  bool Begin() {
    return add();
  }
  bool Send(const uint8_t *data, size_t len) {
    return send(data, len);
  }
  void onReceive(const uint8_t *data, size_t len, bool broadcast) {
    if (broadcast && memcmp(ESPNOW_ADVERTISING_STRING, data, len) == 0) {
      // ADVERTISING Echo
      this->Send((const uint8_t *)ESPNOW_ADVERTISING_STRING, strlen(ESPNOW_ADVERTISING_STRING) + 1);
    } else {
      // Communication
      receiveCount++;
    }
  }
};

MY_ESP_NOW_Peer *espnow_peer_broadcast = nullptr;
MY_ESP_NOW_Peer *espnow_peer = nullptr;

void espnow_receive(const esp_now_recv_info_t *info, const uint8_t *data, int len, void *arg) {
  if ((espnow_peer == nullptr) && (memcmp(ESPNOW_ADVERTISING_STRING, data, len) == 0)) {
    // Add peer
    Serial.println("Add peer");
    espnow_peer = new MY_ESP_NOW_Peer(info->src_addr, ESPNOW_WIFI_CHANNEL, WIFI_IF_STA, NULL);
    espnow_peer->Begin();

    // ADVERTISING Echo
    espnow_peer->Send((const uint8_t *)ESPNOW_ADVERTISING_STRING, strlen(ESPNOW_ADVERTISING_STRING) + 1);
    Serial.printf("Peer Send[%s] : %s\n", ARDUINO_BOARD, ESPNOW_ADVERTISING_STRING);
  }
  Serial.printf("Non Peer Receive(%02X:%02X:%02X:%02X:%02X:%02X) : %s\n", info->src_addr[0], info->src_addr[1], info->src_addr[2], info->src_addr[3], info->src_addr[4], info->src_addr[5], data);
}

void setup() {
  Serial.begin(115200);
  delay(2000);

  Serial.println("ESP-NOW");

  WiFi.mode(WIFI_STA);
  WiFi.setChannel(ESPNOW_WIFI_CHANNEL);
  while (!WiFi.STA.started()) {
    delay(100);
  }

  if (!ESP_NOW.begin()) {
    ESP.restart();
  }

  ESP_NOW.onNewPeer(espnow_receive, NULL);

  espnow_peer_broadcast = new MY_ESP_NOW_Peer(ESP_NOW.BROADCAST_ADDR, ESPNOW_WIFI_CHANNEL, WIFI_IF_STA, NULL);
  espnow_peer_broadcast->Begin();

  command.addCommand("CLEAR", [](EspEasySerialCommand::command_t) {
    Serial.println("#CLEAR");
    receiveCount = 0;
  });
}

void loop() {
  if (espnow_peer == nullptr) {
    // ADVERTISING
    espnow_peer_broadcast->Send((const uint8_t *)ESPNOW_ADVERTISING_STRING, strlen(ESPNOW_ADVERTISING_STRING) + 1);
    Serial.printf("Broadcast Send[%s] : %s\n", ARDUINO_BOARD, ESPNOW_ADVERTISING_STRING);
  } else {
    Serial.printf("receiveCount = %d\n", receiveCount);
  }

  command.task();

  delay(1000);
}

前回の単純なピアを組むスケッチをベースに組んであります。今回受信成功したらカウントアップする仕組みと、シリアル経由でカウントを0に戻すコードが入っています。

  void onReceive(const uint8_t *data, size_t len, bool broadcast) {
    if (broadcast && memcmp(ESPNOW_ADVERTISING_STRING, data, len) == 0) {
      // ADVERTISING Echo
      this->Send((const uint8_t *)ESPNOW_ADVERTISING_STRING, strlen(ESPNOW_ADVERTISING_STRING) + 1);
    } else {
      // Communication
      receiveCount++;
    }
  }

コード的にはアドバタイズの処理以外はカウントアップしています。

  command.addCommand("CLEAR", [](EspEasySerialCommand::command_t) {
    Serial.println("#CLEAR");
    receiveCount = 0;
  });

あとはEspEasyUtilsライブラリのEspEasySerialCommandクラスを使って、シリアルコンソールからCLEARと送るとカウントを0に戻す処理を入れました。

void loop() {
  if (espnow_peer == nullptr) {
    // ADVERTISING
    espnow_peer_broadcast->Send((const uint8_t *)ESPNOW_ADVERTISING_STRING, strlen(ESPNOW_ADVERTISING_STRING) + 1);
    Serial.printf("Broadcast Send[%s] : %s\n", ARDUINO_BOARD, ESPNOW_ADVERTISING_STRING);
  } else {
    Serial.printf("receiveCount = %d\n", receiveCount);
  }

  command.task();

  delay(1000);
}

あとはloopでアドバタイズの処理と、現在のカウント値を定期的に出力しています。

送信側スケッチ

#include "ESP32_NOW.h"
#include "WiFi.h"
#include "EspEasySerialCommand.h"

EspEasySerialCommand command(Serial);

#define ESPNOW_WIFI_CHANNEL 4  // 1 - 14

#define ESPNOW_ADVERTISING_STRING "ADVERTISING_ESP32_ESPNOW_STRING"

uint16_t successCount;
uint16_t failCount;

class MY_ESP_NOW_Peer : public ESP_NOW_Peer {
public:
  MY_ESP_NOW_Peer(const uint8_t *mac_addr, uint8_t channel, wifi_interface_t iface, const uint8_t *lmk)
    : ESP_NOW_Peer(mac_addr, channel, iface, lmk) {}
  ~MY_ESP_NOW_Peer() {
  }
  bool Begin() {
    return add();
  }
  bool Send(const uint8_t *data, size_t len) {
    return send(data, len);
  }
  void onReceive(const uint8_t *data, size_t len, bool broadcast) {
    if (broadcast && memcmp(ESPNOW_ADVERTISING_STRING, data, len) == 0) {
      // ADVERTISING Echo
      this->Send((const uint8_t *)ESPNOW_ADVERTISING_STRING, strlen(ESPNOW_ADVERTISING_STRING) + 1);
    } else {
      // Communication
      const uint8_t *mac = addr();
      Serial.printf("Peer Receive(%02X:%02X:%02X:%02X:%02X:%02X) %s : %s\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5], (broadcast ? "Broadcast" : "Unicast"), data);
    }
  }
  void onSent(bool success) {
    if (success) {
      successCount++;
    } else {
      failCount++;
    }
  }
};

MY_ESP_NOW_Peer *espnow_peer_broadcast = nullptr;
MY_ESP_NOW_Peer *espnow_peer = nullptr;

void espnow_receive(const esp_now_recv_info_t *info, const uint8_t *data, int len, void *arg) {
  if ((espnow_peer == nullptr) && (memcmp(ESPNOW_ADVERTISING_STRING, data, len) == 0)) {
    // Add peer
    Serial.println("Add peer");
    espnow_peer = new MY_ESP_NOW_Peer(info->src_addr, ESPNOW_WIFI_CHANNEL, WIFI_IF_STA, NULL);
    espnow_peer->Begin();

    // ADVERTISING Echo
    espnow_peer->Send((const uint8_t *)ESPNOW_ADVERTISING_STRING, strlen(ESPNOW_ADVERTISING_STRING) + 1);
    Serial.printf("Peer Send[%s] : %s\n", ARDUINO_BOARD, ESPNOW_ADVERTISING_STRING);
  }
  Serial.printf("Non Peer Receive(%02X:%02X:%02X:%02X:%02X:%02X) : %s\n", info->src_addr[0], info->src_addr[1], info->src_addr[2], info->src_addr[3], info->src_addr[4], info->src_addr[5], data);
}

void setup() {
  Serial.begin(115200);
  delay(2000);

  WiFi.mode(WIFI_STA);
  WiFi.setChannel(ESPNOW_WIFI_CHANNEL);
  while (!WiFi.STA.started()) {
    delay(100);
  }

  if (!ESP_NOW.begin()) {
    ESP.restart();
  }

  ESP_NOW.onNewPeer(espnow_receive, NULL);

  espnow_peer_broadcast = new MY_ESP_NOW_Peer(ESP_NOW.BROADCAST_ADDR, ESPNOW_WIFI_CHANNEL, WIFI_IF_STA, NULL);
  espnow_peer_broadcast->Begin();

  command.addCommand("SEND", [](EspEasySerialCommand::command_t command) {
    uint16_t count = command.command2.toInt();
    uint16_t size = command.command3.toInt();
    uint16_t wait = command.command4.toInt();
    Serial.printf("#SEND count:%d size:%d wait:%d\n", count, size, wait);

    successCount = 0;
    failCount = 0;

    for (uint16_t i = 0; i < count; i++) {
      uint8_t data[250] = {};
      espnow_peer->Send(data, size);
      delay(wait);
    }

    Serial.printf("successCount = %d, failCount = %d\n", successCount, failCount);
  });

  command.addCommand("RATE", [](EspEasySerialCommand::command_t command) {
    uint8_t rate = command.command2.toInt();
    Serial.printf("#RATE 0x%02x\n", rate);
    esp_wifi_config_espnow_rate(WIFI_IF_STA, (wifi_phy_rate_t)rate);
  });
}

void loop() {
  if (espnow_peer == nullptr) {
    // ADVERTISING
    espnow_peer_broadcast->Send((const uint8_t *)ESPNOW_ADVERTISING_STRING, strlen(ESPNOW_ADVERTISING_STRING) + 1);
    Serial.printf("Broadcast Send[%s] : %s\n", ARDUINO_BOARD, ESPNOW_ADVERTISING_STRING);
  }

  command.task();

  delay(1000);
}

送信側も受信側と同じような感じに作ってあります。

  command.addCommand("SEND", [](EspEasySerialCommand::command_t command) {
    uint16_t count = command.command2.toInt();
    uint16_t size = command.command3.toInt();
    uint16_t wait = command.command4.toInt();
    Serial.printf("#SEND count:%d size:%d wait:%d\n", count, size, wait);

    successCount = 0;
    failCount = 0;

    for (uint16_t i = 0; i < count; i++) {
      uint8_t data[250] = {};
      espnow_peer->Send(data, size);
      delay(wait);
    }

    Serial.printf("successCount = %d, failCount = %d\n", successCount, failCount);
  });

ピアの登録が終わったら「SEND [送信回数] [送信サイズ] [待機ms]」でESP-NOWを連続送信します。このパラメータを変えながら実験をしています。

  command.addCommand("RATE", [](EspEasySerialCommand::command_t command) {
    uint8_t rate = command.command2.toInt();
    Serial.printf("#RATE 0x%02x\n", rate);
    esp_wifi_config_espnow_rate(WIFI_IF_STA, (wifi_phy_rate_t)rate);
  });

送信速度は上記のコードで変更しています。esp_wifi_config_espnow_rateは非推奨なのですが、Wi-Fi6を使わない場合には利用しやすい関数です。本来はesp_now_set_peer_rate_configという関数があり、ピアごとに送信速度の変更とWi-Fi6関連と思われるオプション設定が可能です。ただ、新しい関数は細かいパラメーターについての解説がなかったので、古いesp_wifi_config_espnow_rateを使っていますのでご注意ください。

esp-idf/components/esp_wifi/include/esp_wifi_types_generic.h at master · espressif/esp-idf
Espressif IoT Development Framework. Official development framework for Espressif SoCs. - espressif/esp-idf

速度は上記にあるようにWIFI_PHY_RATE_1M_Lなどのenumを使って設定するのが正しいのですが、コマンドでは上記のページを見ながら内部の数値を直接渡して設定しています。

    esp_wifi_config_espnow_rate(WIFI_IF_STA, WIFI_PHY_RATE_54M);

通常は上記の指定となります。

上記で少し古いバージョンですが、速度を変更した実験をしているので合わせて参考にしてください。基本的に送信時間などはまわりの電波状況によって変わります。混雑しているチャンネルで他の電波と同じタイミングで送信をしようとすると待機する時間などが発生しますのでどのチャンネルを利用するかも重要な用途となります。

デフォルト設定での送信成功率

ratecountsizewaitsuccessreceive
WIFI_PHY_RATE_1M_L1000102759
WIFI_PHY_RATE_1M_L100011550582
WIFI_PHY_RATE_1M_L1000129981000
WIFI_PHY_RATE_1M_L1000139991000
WIFI_PHY_RATE_1M_L10001410001000

デフォルトは1Mbpsでの通信になります。1バイトのデータを1000回送信しており、waitが送信間隔で、successが送信側が送信成功を受信した回数、receiveが受信側が受信した回数となります。

waitが2msのときには受信側はほぼ100%受信していますが、送信側の成功は少し取りこぼしています。基本的に1Mbpsの通信速度の場合には片道2ms程度の時間がかかるので、受信側は2ms以上の送信間隔である必要があり、送信側は往復で4ms程度の送信間隔でないと取りこぼす危険があります。

実際のところまわりの電波状況などで取りこぼしは発生しますので送信側の成功が戻るまで送信するのか、ストリーミングとして受信側が受け取っていると思われる送信間隔で送るのかは要件によると思います。

ratecountsizewaitsuccessreceive
WIFI_PHY_RATE_1M_L100010001648
WIFI_PHY_RATE_1M_L10001001370402
WIFI_PHY_RATE_1M_L10001002638669
WIFI_PHY_RATE_1M_L100010039981000
WIFI_PHY_RATE_1M_L100010049991000
WIFI_PHY_RATE_1M_L1000100510001000

送信データを1バイトから100バイトに増やしました。送信時間が増えますので片道3msぐらいかかっていそうでした。

ratecountsizewaitsuccessreceive
WIFI_PHY_RATE_1M_L10002500941
WIFI_PHY_RATE_1M_L10002501289321
WIFI_PHY_RATE_1M_L10002502567599
WIFI_PHY_RATE_1M_L10002503847878
WIFI_PHY_RATE_1M_L100025049991000
WIFI_PHY_RATE_1M_L100025059991000
WIFI_PHY_RATE_1M_L1000250610001000

最大長である250バイトにしてみました。送信側が250バイトだと4ms、成功パケットは短いので2msぐらいで戻って来ているようです。

送信速度を変更してみる

ratecountsizewaitsuccessreceive
WIFI_PHY_RATE_1M_L10002502524556
WIFI_PHY_RATE_2M_L10002502737767
WIFI_PHY_RATE_2M_S10002502706736
WIFI_PHY_RATE_5M_L10002502984985
WIFI_PHY_RATE_5M_S100025029981000
WIFI_PHY_RATE_6M100025029981000
WIFI_PHY_RATE_9M100025029991000
WIFI_PHY_RATE_11M_L100025029821000
WIFI_PHY_RATE_11M_S100025029991000
WIFI_PHY_RATE_12M100025029991000
WIFI_PHY_RATE_18M100025029971000
WIFI_PHY_RATE_24M100025029961000
WIFI_PHY_RATE_36M100025029991000
WIFI_PHY_RATE_48M100025029981000
WIFI_PHY_RATE_54M100025029991000

最大長の250バイトで2msの送信間隔で送信速度を変更しながら実験してみました。LとSがついているものがありますが、これはパケットのヘッダー長が短いSと長いLがある送信速度になります。基本的にはSの短いほうが早いです。

250バイトだと1Mbpsだと送信に4ms程度かかっていましたが、5Mbpsだと倍の2ms程度になりました。プロトコル的に最初に1Mbpsでヘッダーを送信して、ペイロード部分のみ高速になるのでデータ長が長くないと送信速度の恩恵は少なくなります。

全体の傾向として受信側は2msの送信間隔でもある受信成功しているのですが、送信成功パケットは最速の54Mbpsにしても安定しませんでした。

ratecountsizewaitsuccessreceive
WIFI_PHY_RATE_1M_L100011594626
WIFI_PHY_RATE_2M_L100011787819
WIFI_PHY_RATE_2M_S100011974977
WIFI_PHY_RATE_5M_L100011972975
WIFI_PHY_RATE_5M_S100011979999
WIFI_PHY_RATE_6M1000119951000
WIFI_PHY_RATE_9M1000119971000
WIFI_PHY_RATE_11M_L100011831861
WIFI_PHY_RATE_11M_S1000119921000
WIFI_PHY_RATE_12M1000119801000
WIFI_PHY_RATE_18M1000119881000
WIFI_PHY_RATE_24M1000119951000
WIFI_PHY_RATE_36M1000119921000
WIFI_PHY_RATE_48M1000119971000
WIFI_PHY_RATE_54M1000119961000

送信データを1バイトに減らして、送信間隔も1msになっています。11M_Lはヘッダーが長いのでパフォーマンス的には悪いですが、データ長が少ない場合にはがんばれば1ms送信間隔でもそれなりの通信ができそうです。

まとめ

送信速度は早い方がよい結果でしたが、消費電力や電波が届く範囲などで影響がでる可能性があります。今回は1度だけの計測だったり、2台のボードが隣接している場所で実験しているなどあまり厳密な実験ではありません。全体的な傾向のみ確認をして、実利用では距離などを考慮してパラメータの調整をするのがよいと思います。

今回の実験だと受信側は速度設定が必要なく、どの速度でも受取が可能でした。そのためスループット的にはなるべく通信が少ないチャンネルで送信速度をあげて通信するのが有効そうでした。

コメント