AESP-IDFを使ってみる

ESPNOW


ESPNOWはEspressIFが提供するPeerToPeerの通信規格です。
標準的なWi-Fiと同様に2.4GHzの周波数帯域で動作し、データパケットに標準的なWi-Fiチャネル(約20〜22MHzの帯域幅)を使 用して、
ルーターなしでESP32/ESP8266デバイス間の直接的な低消費電力通信を可能にします。
WiFiチャネルの選択やユーザ名、パスワードの認証などを行わないので、WiFiに比べ短時間、省電力で動くのが特徴です。
ESPNOWはESP8266でも利用可能で、ESP8266とESP32の間でもESPNOWを使った通信が可能です。
PeerToPeerの通信規格としてはZigBeeなどが有りますが、使用する無線の中心周波数と周波数帯域がWiFiとは異なります。

WiFi channel1
中心周波数: 2412 MHz (2.412 GHz)
周波数帯域: 2401MHz-2423MHz (使用帯域: 20-22 MHz)

ZigBee channel11
中心周波数: 2405 MHz
周波数帯域: 2402.5Mhz-2407.5MHz (使用帯域: 2.5 MHz)

ESPNOWは同じチャネルを使用している相手にだけメッセージが届きます。
ESPNOWの受信側がチャネル#11を使用する場合、送信側もチャネル番号#11を使用する必要が有ります。
APに接続した場合、使用するWiFiのチャネルはAPが決めますが、APに接続していない状態では、WiFiのデフォルトのチャネルは#1で す。
ESPNOWでチャネル#11を使いたい場合、以下のコードでWiFiのチャネルを#11に変更する必要が有ります。
    /* Change WiFi primary channel */
    uint8_t primary;
    wifi_second_chan_t second;
    ESP_ERROR_CHECK(esp_wifi_get_channel(&primary, &second));
    ESP_LOGI(TAG, "current primary channel=%d", primary);
    primary = 11;
    ESP_ERROR_CHECK(esp_wifi_set_channel(primary, WIFI_SECOND_CHAN_NONE));
    ESP_ERROR_CHECK(esp_wifi_get_channel(&primary, &second));
    ESP_LOGI(TAG, "new primary channel=%d", primary);

あまり知られていない更新ですが、ESP-IDF V5.4からESPNOWのバージョンが2.0に更新されました。
ESPNOW Ver1では最大ペイロードサイズは250バイトでしたが、Ver2では1470バイトに拡張されています。
ESP-IDFのESPNOWはVer1とVer2で相互に通信することができます。
ESP8266のESPNOWはVer1なので、ESP8266と通信するときは250バイトまでの制限が有ります。




こ ちらにESPNOW通信のサンプルが公開されています。
このサンプルを実行するためには2台のESP32が必要です。
menuconfigを起動するとWiFi modeの選択が有ります。


ESP32はどのモデルもStation InterfaceとSoftAP Interfaceの2つのWiFi Interfaceを持っています。
それぞれのInterfaceには、異なるMACアドレスがアサインされています。
ESPNOWはStation Interface/SoftAP InterfaceのどちらのInterfaceを使っても通信することができます。
WiFi modeはどちらのInterfaceを使うかの選択で、WiFi modeと言うメニュー名が分かりにくいです。
WiFi modeの選択ではなく、使用するWiFi Interfaceの選択です。

こ ちらにサポートされているRFの共存シナリオが公開されています。
ESPNOWとBLE/Bluetoothは共存できる様ですが、ESPNOWのRXはSTAモードのみ使用可能となっています。
なぜ、ESPNOWのRXにこのような制限があるのか不明です。

Station Interfaceを使うとESPNOWの省電力機能を使うことができます。
esp_now_set_wake_window() と esp_wifi_connectionless_module_set_wake_interval() を使ってSleepの設定を行います。


こちらがStation Interfaceを使った時のロギングです。


こちらがSoftAP Interfaceを使った時のロギングです。
送信側のMACアドレスが24:0a:c4:c5:46:fcから24:0a:c4:c5:46:fdに変わります。




なかなか難解なコードですが、このサンプルではBroadcast通信とUnicast通信の使い方が紹介されています。
このサンプルは最初はBroadcastで通信相手を探しますが、相手が見つかるとUnicast送信に切り替えます。
送信先のMACアドレスが「FF:FF:FF:FF:FF:FF」の場合はBroadcast通信、それ以外の場合はUnicast通信となりま す。
ESPNOWで通信を行う場合、相手側のデバイスにデータを送信する前に、esp_now_add_peer()を使って、
相手側のMACアドレスをデバイスリストに登録しておく必要が有ります。
Broadcast通信を行う場合も、「FF:FF:FF:FF:FF:FF」のアドレスを、デバイスリストに登録しておく必要が有ります。
デバイスリストに登録する際に、通信を暗号化するかどうかを指定することができます。
このサンプルではBroadcast通信は非暗号化、Unicast通信は暗号化で通信を行っています。
暗号化通信を行うときは、送信側と受信側で同じ暗号キーを使う必要が有ります。

こちらがBroadcast送信からUnicast送信に切り替わる時のロギングです。
7708TickまではBroadcastで送信しますが、7708TickでUnicast送信に切り替わります。


Unicast通信に切り替えて100回送信すると、送信を終了します。
こちらが送信側のロギングです。


こちらが受信側のロギングです。


コードを少し変更して、通信速度を測定してみました。
パケット長1470バイトを100回送信した場合、70.6KBytes/Sec(552KBits/Sec)程度の通信速度です。
そこで、使用するWiFiのチャネルを変えて、同じ条件で試してみました。
チャネルの使用状況により通信速度は大きく変わります。
WiFi Channel KBytes/Sec KBits/Sec
1 70.0 547
3 78.1 610
5 94.8 741
7 96.7 755
9 98.6 770
11 78.1 610
13 90.7 709
14 102.7 802



ESP8266の場合、ESPNOWとWiFiを同時に使うことができませんでしたが、
ESP32ではこの制限がなくなり、ESPNOWとWiFiを同時に使うことができます。
WiFiをStationモードで使う場合、電波の混雑状況などを考慮して、使用するチャネルはAPが決めます。
ESPNOWとWiFiを同時に使う場合、ESPNOWもWiFiと同じチャネルを使って通信を行います。
ESPNOWが使用するチャネルを変更することはできません。



ESPNOWでは送信完了時と受信完了時にCallBack関数が呼ばれます。
ESP-IDF V5.5から送信完了時のCallBack関数のIFが(いきなり)変わりました。
こ ちらに短い説明が有ります。
以下のコードで、V5.4とV5.5のどちらでもビルドできるようになります。
esp_now_send_info_tは、ドキュメントによると typedef wifi_tx_info_t esp_now_send_info_t です。
V5.4まではCallBack関数には送信先MACアドレスしか渡りませんでしたが、V5.5ではそれ以外に802.11のパケットなども渡る みたいです。
ESPNOWも802.11を使うので、この変数型に統一したようです。
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
static void example_espnow_send_cb(const esp_now_send_info_t *tx_info, esp_now_send_status_t status)
#else
static void example_espnow_send_cb(const uint8_t *mac_addr, esp_now_send_status_t status)
#endif
{
    example_espnow_event_t evt;
    example_espnow_event_send_cb_t *send_cb = &evt.info.send_cb;
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
    const uint8_t *mac_addr = tx_info->des_addr;
#endif

    if (mac_addr == NULL) {
        ESP_LOGE(__FUNCTION__, "Send cb arg error");
        return;
    }

    evt.id = EXAMPLE_ESPNOW_SEND_CB;
    memcpy(send_cb->mac_addr, mac_addr, ESP_NOW_ETH_ALEN);
    send_cb->status = status;
    if (xQueueSend(xQueueESPNOWSend, &evt, ESPNOW_MAXDELAY) != pdTRUE) {
        ESP_LOGW(__FUNCTION__, "xQueueSend fail");
    }
}



こちらにESPNOWのサ ンプルコードが公開されています。
ESP8266もESP32もArduino環境のコードですが、Linux用のESPNOWのコードが公開されています。
LinuxでESPNOWを使う場合、WiFiインターフェースをモニター モードに変更する必要が有ります。
WiFiのモニター モードは、WiFi アダプターの特別なモードで、送信先に関係なく、特定のチャネルのすべてのワイヤレス トラフィックをキャプチャできます。
こちらが、wlx1cbfceaae44dのInterfaceをmonitorモードに変更する手順です。
$ iwconfig
lo        no wireless extensions.

enp2s0    no wireless extensions.

wlx1cbfceaae44d  IEEE 802.11  ESSID:off/any
          Mode:Managed  Access Point: Not-Associated   Tx-Power=20 dBm
          Retry short limit:7   RTS thr:off   Fragment thr:off
          Power Management:off


$ sudo ifconfig wlx1cbfceaae44d
wlx1cbfceaae44d: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
        ether 1c:bf:ce:aa:e4:4d  txqueuelen 1000  (イーサネット)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

$ sudo ifconfig wlx1cbfceaae44d down

$ sudo iwconfig wlx1cbfceaae44d mode monitor

$ sudo ifconfig wlx1cbfceaae44d up

$ sudo ifconfig wlx1cbfceaae44d
wlx1cbfceaae44d: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
        unspec 1C-BF-CE-AA-E4-4D-00-00-00-00-00-00-00-00-00-00  txqueuelen 1000  (不明なネット)
        RX packets 2412317  bytes 380570756 (380.5 MB)
        RX errors 0  dropped 109773  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0



こちらにESPNOWの汎用 コンポーネントが公開されています。
以下のコードで、簡単にPeerToPeerの通信を行うことができます。
#include "nvs_flash.h"
#include "esp_netif.h"
#include "zh_network.h"

#define MAC2STR(a) (a)[0], (a)[1], (a)[2], (a)[3], (a)[4], (a)[5]

void zh_network_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data);

uint8_t target[6] = {0x58, 0xBF, 0x25, 0x18, 0xC8, 0x04};

typedef struct
{
    char char_value[30];
    int int_value;
    float float_value;
    bool bool_value;
} example_message_t;

void app_main(void)
{
    esp_log_level_set("zh_vector", ESP_LOG_NONE);
    esp_log_level_set("zh_network", ESP_LOG_NONE);
    nvs_flash_init();
    esp_netif_init();
    esp_event_loop_create_default();
    wifi_init_config_t wifi_init_config = WIFI_INIT_CONFIG_DEFAULT();
    esp_wifi_init(&wifi_init_config);
    esp_wifi_set_mode(WIFI_MODE_STA);
    esp_wifi_start();
    esp_wifi_set_max_tx_power(8); // Power reduction is for example and testing purposes only. Do not use in your own programs!
    zh_network_init_config_t network_init_config = ZH_NETWORK_INIT_CONFIG_DEFAULT();
    zh_network_init(&network_init_config);
#ifdef CONFIG_IDF_TARGET_ESP8266
    esp_event_handler_register(ZH_NETWORK, ESP_EVENT_ANY_ID, &zh_network_event_handler, NULL);
#else
    esp_event_handler_instance_register(ZH_NETWORK, ESP_EVENT_ANY_ID, &zh_network_event_handler, NULL, NULL);
#endif
    example_message_t send_message = {0};
    printf("sizeof(send_message)=%d\n",sizeof(send_message));
    strcpy(send_message.char_value, "THIS IS A CHAR");
    send_message.int_value = 1234;
    send_message.float_value = 1.234;
    send_message.bool_value = false;
    for (;;)
    {
        zh_network_send(NULL, (uint8_t *)&send_message, sizeof(send_message));
        vTaskDelay(5000 / portTICK_PERIOD_MS);
#if 0
        zh_network_send(target, (uint8_t *)&send_message, sizeof(send_message));
        vTaskDelay(5000 / portTICK_PERIOD_MS);
#endif
    }
}

void zh_network_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
{
    switch (event_id)
    {
    case ZH_NETWORK_ON_RECV_EVENT:;
        zh_network_event_on_recv_t *recv_data = event_data;
        printf("Message from MAC %02X:%02X:%02X:%02X:%02X:%02X is received. Data lenght %d bytes.\n", MAC2STR(recv_data->mac_addr), recv_data->data_len);
        example_message_t *recv_message = (example_message_t *)recv_data->data;
        printf("Char %s\n", recv_message->char_value);
        printf("Int %d\n", recv_message->int_value);
        printf("Float %f\n", recv_message->float_value);
        printf("Bool %d\n", recv_message->bool_value);
        heap_caps_free(recv_data->data); // Do not delete to avoid memory leaks!
        break;
    case ZH_NETWORK_ON_SEND_EVENT:;
        zh_network_event_on_send_t *send_data = event_data;
        if (send_data->status == ZH_NETWORK_SEND_SUCCESS)
        {
            printf("Message to MAC %02X:%02X:%02X:%02X:%02X:%02X sent success.\n", MAC2STR(send_data->mac_addr));
        }
        else
        {
            printf("Message to MAC %02X:%02X:%02X:%02X:%02X:%02X sent fail.\n", MAC2STR(send_data->mac_addr));
        }
        break;
    default:
        break;
    }
}

mainフォルダーに以下のidf_component.ymlを作成すると、コンパイル時に自動的にコンポーネントをダウンロードします。
## IDF Component Manager Manifest File
dependencies:
  aZholtikov/zh_vector:
    git: https://github.com/aZholtikov/zh_vector.git
  aZholtikov/zh_network:
    git: https://github.com/aZholtikov/zh_network.git

続く...