ESP32でpingする

概要

ESP32が接続しているネットワークのMTUを調べたくなったのでpingの実装を調べてみました。何個かライブラリがありましたが、現在はESP-IDFでping送信をするサンプルがあったので、それをArduinoで動かしてみました。

ESP-IDFのサンプル

esp-idf/examples/protocols/icmp_echo/README.md at master · espressif/esp-idf
Espressif IoT Development Framework. Official development framework for Espressif SoCs. - espressif/esp-idf

上記のスケッチになります。esp_consoleという対話型コマンドを利用して、pingを送信する例になります。esp_consoleも気になったので別記事で紹介したいと思います。

pingはICMPを利用してEchoを送り、そのReplyを受信する動きになります。読み方はピンもしくはピングになります。潜水艦のソナーのピンとか卓球のピンポンのピンがこのpingとなります。

ESP-IDFのpingは非同期実行を行い、Echoを送ったあとにバックグラウンドでReplyの受信待ちをして、コールバック関数で結果を通知します。サンプルはprintfでそのままシリアル出力していましたがArduinoの場合にはきれいじゃないのでキューを使って通知を受信したいと思います。

ベタ移植ミニマムスケッチ例

#include <WiFi.h>
#include "lwip/inet.h"
#include "ping/ping_sock.h"

static void cmd_ping_on_ping_success(esp_ping_handle_t hdl, void *args)
{
    uint8_t ttl;
    uint16_t seqno;
    uint32_t elapsed_time, recv_len;
    ip_addr_t target_addr;
    esp_ping_get_profile(hdl, ESP_PING_PROF_SEQNO, &seqno, sizeof(seqno));
    esp_ping_get_profile(hdl, ESP_PING_PROF_TTL, &ttl, sizeof(ttl));
    esp_ping_get_profile(hdl, ESP_PING_PROF_IPADDR, &target_addr, sizeof(target_addr));
    esp_ping_get_profile(hdl, ESP_PING_PROF_SIZE, &recv_len, sizeof(recv_len));
    esp_ping_get_profile(hdl, ESP_PING_PROF_TIMEGAP, &elapsed_time, sizeof(elapsed_time));
    printf("%" PRIu32 " bytes from %s icmp_seq=%" PRIu16 " ttl=%" PRIu16 " time=%" PRIu32 " ms\n",
           recv_len, ipaddr_ntoa((ip_addr_t*)&target_addr), seqno, ttl, elapsed_time);
}

static void cmd_ping_on_ping_timeout(esp_ping_handle_t hdl, void *args)
{
    uint16_t seqno;
    ip_addr_t target_addr;
    esp_ping_get_profile(hdl, ESP_PING_PROF_SEQNO, &seqno, sizeof(seqno));
    esp_ping_get_profile(hdl, ESP_PING_PROF_IPADDR, &target_addr, sizeof(target_addr));
    printf("From %s icmp_seq=%d timeout\n",ipaddr_ntoa((ip_addr_t*)&target_addr), seqno);
}

static void cmd_ping_on_ping_end(esp_ping_handle_t hdl, void *args)
{
    ip_addr_t target_addr;
    uint32_t transmitted;
    uint32_t received;
    uint32_t total_time_ms;
    uint32_t loss;

    esp_ping_get_profile(hdl, ESP_PING_PROF_REQUEST, &transmitted, sizeof(transmitted));
    esp_ping_get_profile(hdl, ESP_PING_PROF_REPLY, &received, sizeof(received));
    esp_ping_get_profile(hdl, ESP_PING_PROF_IPADDR, &target_addr, sizeof(target_addr));
    esp_ping_get_profile(hdl, ESP_PING_PROF_DURATION, &total_time_ms, sizeof(total_time_ms));

    if (transmitted > 0) {
        loss = (uint32_t)((1 - ((float)received) / transmitted) * 100);
    } else {
        loss = 0;
    }
    if (IP_IS_V4(&target_addr)) {
        printf("\n--- %s ping statistics ---\n", inet_ntoa(*ip_2_ip4(&target_addr)));
    } else {
        printf("\n--- %s ping statistics ---\n", inet6_ntoa(*ip_2_ip6(&target_addr)));
    }
    printf("%" PRIu32 " packets transmitted, %" PRIu32 " received, %" PRIu32 "%% packet loss, time %" PRIu32 "ms\n",
           transmitted, received, loss, total_time_ms);
    esp_ping_delete_session(hdl);
}

static int ping(const ip_addr_t target){
    esp_ping_config_t config = ESP_PING_DEFAULT_CONFIG();
    config.target_addr = target;

    esp_ping_callbacks_t cbs = {
        .cb_args = NULL,
        .on_ping_success = cmd_ping_on_ping_success,
        .on_ping_timeout = cmd_ping_on_ping_timeout,
        .on_ping_end = cmd_ping_on_ping_end
    };
    esp_ping_handle_t ping;

    esp_ping_new_session(&config, &cbs, &ping);
    esp_ping_start(ping);

    return 0;
}

void setup() {
  WiFi.begin();
  while (!WiFi.isConnected()) {
    delay(1000);
  }
}

void loop() {
  printf("Ping\b");
  ping(IPADDR4_INIT_BYTES(8,8,8,8));
  delay(10000);
}

ほぼベタ移植したものになります。

    esp_ping_callbacks_t cbs = {
        .cb_args = NULL,
        .on_ping_success = cmd_ping_on_ping_success,
        .on_ping_timeout = cmd_ping_on_ping_timeout,
        .on_ping_end = cmd_ping_on_ping_end
    };

上記でコールバック関数の設定をしています。成功した場合、失敗した場合、規定回数のpingが完了したあとの結果呼び出しの3種類になります。

各コールバックは結果の取り方が少し面倒ですが、比較的シンプルだと思います。

Arduino用スケッチ例

#include <WiFi.h>
#include "lwip/inet.h"
#include "ping/ping_sock.h"

QueueHandle_t xQueuePingResult;
QueueHandle_t xQueuePingEnd;

typedef struct {
  uint8_t ttl;
  uint16_t seqno;
  uint32_t elapsed_time;
  uint32_t recv_len;
  ip_addr_t target_addr;
  uint8_t timeout;
} ping_result;

typedef struct {
  ip_addr_t target_addr;
  uint32_t transmitted;
  uint32_t received;
  uint32_t total_time_ms;
  uint32_t loss;
} ping_end;

static void cmd_ping_on_ping_success(esp_ping_handle_t hdl, void *args) {
  ping_result result = {};
  esp_ping_get_profile(hdl, ESP_PING_PROF_SEQNO, &result.seqno, sizeof(result.seqno));
  esp_ping_get_profile(hdl, ESP_PING_PROF_TTL, &result.ttl, sizeof(result.ttl));
  esp_ping_get_profile(hdl, ESP_PING_PROF_IPADDR, &result.target_addr, sizeof(result.target_addr));
  esp_ping_get_profile(hdl, ESP_PING_PROF_SIZE, &result.recv_len, sizeof(result.recv_len));
  esp_ping_get_profile(hdl, ESP_PING_PROF_TIMEGAP, &result.elapsed_time, sizeof(result.elapsed_time));
  xQueueSend(xQueuePingResult, &result, 0);
}

static void cmd_ping_on_ping_timeout(esp_ping_handle_t hdl, void *args) {
  ping_result result = {};
  esp_ping_get_profile(hdl, ESP_PING_PROF_SEQNO, &result.seqno, sizeof(result.seqno));
  esp_ping_get_profile(hdl, ESP_PING_PROF_IPADDR, &result.target_addr, sizeof(result.target_addr));
  result.timeout = 1;
  xQueueSend(xQueuePingResult, &result, 0);
}

static void cmd_ping_on_ping_end(esp_ping_handle_t hdl, void *args) {
  ping_end end = {};
  esp_ping_get_profile(hdl, ESP_PING_PROF_REQUEST, &end.transmitted, sizeof(end.transmitted));
  esp_ping_get_profile(hdl, ESP_PING_PROF_REPLY, &end.received, sizeof(end.received));
  esp_ping_get_profile(hdl, ESP_PING_PROF_IPADDR, &end.target_addr, sizeof(end.target_addr));
  esp_ping_get_profile(hdl, ESP_PING_PROF_DURATION, &end.total_time_ms, sizeof(end.total_time_ms));
  if (end.transmitted > 0) {
    end.loss = (uint32_t)((1 - ((float)end.received) / end.transmitted) * 100);
  } else {
    end.loss = 0;
  }
  xQueueSend(xQueuePingEnd, &end, 0);
  esp_ping_delete_session(hdl);
}

static void ping(const ip_addr_t target, uint32_t count = 5, uint16_t interval_ms = 1000, uint16_t data_size = 64) {
  esp_ping_config_t config = ESP_PING_DEFAULT_CONFIG();
  config.target_addr = target;
  config.count = count;
  config.interval_ms = interval_ms;
  config.data_size = data_size;

  esp_ping_callbacks_t cbs = {
    .cb_args = NULL,
    .on_ping_success = cmd_ping_on_ping_success,
    .on_ping_timeout = cmd_ping_on_ping_timeout,
    .on_ping_end = cmd_ping_on_ping_end
  };
  esp_ping_handle_t ping;

  esp_ping_new_session(&config, &cbs, &ping);
  esp_ping_start(ping);
}

void setup() {
  Serial.begin(115200);
  Serial.println("Connecting to the AP...");
  WiFi.begin();
  while (!WiFi.isConnected()) {
    delay(1000);
  }

  xQueuePingResult = xQueueCreate(10, sizeof(ping_result));
  xQueuePingEnd = xQueueCreate(1, sizeof(ping_end));

  // Check MTU
  for (int size = 900;; size += 10) {
    ping_end end;
    ping(IPADDR4_INIT_BYTES(8, 8, 8, 8), 3, 300, size);
    Serial.printf("Check SIZE = %d, MTU = %d ", size, size + 28);
    xQueueReceive(xQueuePingEnd, &end, portMAX_DELAY);
    xQueueReset(xQueuePingResult);
    if (end.loss == 100) {
      Serial.println("NG");
      break;
    } else {
      Serial.println("OK");
    }
  }
}

void loop() {
  static bool pingFlag = true;
  ping_result result;
  ping_end end;

  if (xQueueReceive(xQueuePingResult, &result, 0)) {
    if (result.timeout) {
      // Timeout
      Serial.printf("From %s icmp_seq=%d timeout\n", ipaddr_ntoa((ip_addr_t *)&result.target_addr), result.seqno);
    } else
      // Success
      Serial.printf("%" PRIu32 " bytes from %s icmp_seq=%" PRIu16 " ttl=%" PRIu16 " time=%" PRIu32 " ms\n",
                    result.recv_len, ipaddr_ntoa((ip_addr_t *)&result.target_addr), result.seqno, result.ttl, result.elapsed_time);
  }

  if (xQueueReceive(xQueuePingEnd, &end, 0)) {
    if (IP_IS_V4(&end.target_addr)) {
      Serial.printf("\n--- %s ping statistics ---\n", inet_ntoa(*ip_2_ip4(&end.target_addr)));
    } else {
      Serial.printf("\n--- %s ping statistics ---\n", inet6_ntoa(*ip_2_ip6(&end.target_addr)));
    }
    Serial.printf("%" PRIu32 " packets transmitted, %" PRIu32 " received, %" PRIu32 "%% packet loss, time %" PRIu32 "ms\n",
                  end.transmitted, end.received, end.loss, end.total_time_ms);
    pingFlag = true;
    delay(5000);
  }

  if (pingFlag) {
    pingFlag = false;
    Serial.printf("\n========== Ping ==========\n");
    ping(IPADDR4_INIT_BYTES(8, 8, 8, 8));
  }

  delay(1);
}

Arduinoっぽく手をいれたものになります。printfで直接出力したものを構造体に入力をして、キューで受け渡しをしています。

MTUのチェックですが、通常のpingの場合には分割禁止フラグを付けて送信するのですが、ESP32のpingは常に分割禁止フラグ付きでしたのでサイズを変えながら送信して、送信失敗したところが上限になります。

サンプルスケッチでは900から10ずつ数値を上げながらチェックをしています。1400前後まで通常あるはずですが1400未満の場合にはMTUが小さい環境だと思われます。MTUサイズが小さいとスループットが落ちるのと、特定のプロトコルが動かないなどのトラブルが起こります。

通常のブラウザで通信はできますが、WebカメラとかのストリーミングやFTPの動作がおかしい場合にはMTUを疑う必要があります。

まとめ

実際のところESP32でpingを打つことはあまりないと思います。パソコンが使える環境であればパソコンから調べたほうが早いです。ただし、特殊な環境でどうしてもESP32からpingを打つ必要がある場合には参考にしてみてください。

コメント