ESP32-C3 入门笔记07: ESP-NOW动态绑定MAC地址. (ESP-IDF + VSCode)

ESP-NOW 简介

ESP-NOW

[gitbuh]
在这里插入图片描述

ESP-NOW 是一种由乐鑫公司定义的无连接 Wi-Fi 通信协议。在 ESP-NOW 中,应用程序数据被封装在各个供应商的动作帧中,然后在无连接的情况下,从一个 Wi-Fi 设备传输到另一个 Wi-Fi 设备。

CTR 与 CBC-MAC 协议 (CCMP) 可用来保护动作帧的安全。ESP-NOW 广泛应用于智能照明、远程控制、传感器等领域。


ESPNOW 示例

/* ESPNOW Example

   This example code is in the Public Domain (or CC0 licensed, at your option.)

   Unless required by applicable law or agreed to in writing, this
   software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
   CONDITIONS OF ANY KIND, either express or implied.
*/

/*
   This example shows how to use ESPNOW.
   Prepare two device, one for sending ESPNOW data and another for receiving
   ESPNOW data.
*/
#include <stdlib.h>
#include <time.h>
#include <string.h>
#include <assert.h>
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "freertos/timers.h"
#include "nvs_flash.h"
#include "esp_random.h"
#include "esp_event.h"
#include "esp_netif.h"
#include "esp_wifi.h"
#include "esp_log.h"
#include "esp_mac.h"
#include "esp_now.h"
#include "esp_crc.h"
#include "espnow_example.h"

#define ESPNOW_MAXDELAY 512

static const char *TAG = "espnow_example";

static QueueHandle_t s_example_espnow_queue;

static uint8_t s_example_broadcast_mac[ESP_NOW_ETH_ALEN] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
static uint16_t s_example_espnow_seq[EXAMPLE_ESPNOW_DATA_MAX] = { 0, 0 };

static void example_espnow_deinit(example_espnow_send_param_t *send_param);

/* WiFi should start before using ESPNOW */
static void example_wifi_init(void)
{
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK( esp_wifi_init(&cfg) );
    ESP_ERROR_CHECK( esp_wifi_set_storage(WIFI_STORAGE_RAM) );
    ESP_ERROR_CHECK( esp_wifi_set_mode(ESPNOW_WIFI_MODE) );
    ESP_ERROR_CHECK( esp_wifi_start());
    ESP_ERROR_CHECK( esp_wifi_set_channel(CONFIG_ESPNOW_CHANNEL, WIFI_SECOND_CHAN_NONE));

#if CONFIG_ESPNOW_ENABLE_LONG_RANGE
    ESP_ERROR_CHECK( esp_wifi_set_protocol(ESPNOW_WIFI_IF, WIFI_PROTOCOL_11B|WIFI_PROTOCOL_11G|WIFI_PROTOCOL_11N|WIFI_PROTOCOL_LR) );
#endif
}

/* ESPNOW sending or receiving callback function is called in WiFi task.
 * Users should not do lengthy operations from this task. Instead, post
 * necessary data to a queue and handle it from a lower priority task. */
static void example_espnow_send_cb(const uint8_t *mac_addr, esp_now_send_status_t status)
{
    example_espnow_event_t evt;
    example_espnow_event_send_cb_t *send_cb = &evt.info.send_cb;

    if (mac_addr == NULL) {
        ESP_LOGE(TAG, "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(s_example_espnow_queue, &evt, ESPNOW_MAXDELAY) != pdTRUE) {
        ESP_LOGW(TAG, "Send send queue fail");
    }
}

static void example_espnow_recv_cb(const esp_now_recv_info_t *recv_info, const uint8_t *data, int len)
{
    example_espnow_event_t evt;
    example_espnow_event_recv_cb_t *recv_cb = &evt.info.recv_cb;
    uint8_t * mac_addr = recv_info->src_addr;
    uint8_t * des_addr = recv_info->des_addr;

    if (mac_addr == NULL || data == NULL || len <= 0) {
        ESP_LOGE(TAG, "Receive cb arg error");
        return;
    }

    if (IS_BROADCAST_ADDR(des_addr)) {
        /* If added a peer with encryption before, the receive packets may be
         * encrypted as peer-to-peer message or unencrypted over the broadcast channel.
         * Users can check the destination address to distinguish it.
         */
        ESP_LOGD(TAG, "Receive broadcast ESPNOW data");
    } else {
        ESP_LOGD(TAG, "Receive unicast ESPNOW data");
    }

    evt.id = EXAMPLE_ESPNOW_RECV_CB;
    memcpy(recv_cb->mac_addr, mac_addr, ESP_NOW_ETH_ALEN);
    recv_cb->data = malloc(len);
    if (recv_cb->data == NULL) {
        ESP_LOGE(TAG, "Malloc receive data fail");
        return;
    }
    memcpy(recv_cb->data, data, len);
    recv_cb->data_len = len;
    if (xQueueSend(s_example_espnow_queue, &evt, ESPNOW_MAXDELAY) != pdTRUE) {
        ESP_LOGW(TAG, "Send receive queue fail");
        free(recv_cb->data);
    }
}

/* Parse received ESPNOW data. */
int example_espnow_data_parse(uint8_t *data, uint16_t data_len, uint8_t *state, uint16_t *seq, int *magic)
{
    example_espnow_data_t *buf = (example_espnow_data_t *)data;
    uint16_t crc, crc_cal = 0;

    if (data_len < sizeof(example_espnow_data_t)) {
        ESP_LOGE(TAG, "Receive ESPNOW data too short, len:%d", data_len);
        return -1;
    }

    *state = buf->state;
    *seq = buf->seq_num;
    *magic = buf->magic;
    crc = buf->crc;
    buf->crc = 0;
    crc_cal = esp_crc16_le(UINT16_MAX, (uint8_t const *)buf, data_len);

    if (crc_cal == crc) {
        return buf->type;
    }

    return -1;
}

/* Prepare ESPNOW data to be sent. */
void example_espnow_data_prepare(example_espnow_send_param_t *send_param)
{
    example_espnow_data_t *buf = (example_espnow_data_t *)send_param->buffer;

    assert(send_param->len >= sizeof(example_espnow_data_t));

    buf->type = IS_BROADCAST_ADDR(send_param->dest_mac) ? EXAMPLE_ESPNOW_DATA_BROADCAST : EXAMPLE_ESPNOW_DATA_UNICAST;
    buf->state = send_param->state;
    buf->seq_num = s_example_espnow_seq[buf->type]++;
    buf->crc = 0;
    buf->magic = send_param->magic;
    /* Fill all remaining bytes after the data with random values */
    esp_fill_random(buf->payload, send_param->len - sizeof(example_espnow_data_t));
    buf->crc = esp_crc16_le(UINT16_MAX, (uint8_t const *)buf, send_param->len);
}

static void example_espnow_task(void *pvParameter)
{
    example_espnow_event_t evt;
    uint8_t recv_state = 0;
    uint16_t recv_seq = 0;
    int recv_magic = 0;
    bool is_broadcast = false;
    int ret;

    vTaskDelay(5000 / portTICK_PERIOD_MS);
    ESP_LOGI(TAG, "Start sending broadcast data");

    /* Start sending broadcast ESPNOW data. */
    example_espnow_send_param_t *send_param = (example_espnow_send_param_t *)pvParameter;
    if (esp_now_send(send_param->dest_mac, send_param->buffer, send_param->len) != ESP_OK) {
        ESP_LOGE(TAG, "Send error");
        example_espnow_deinit(send_param);
        vTaskDelete(NULL);
    }

    while (xQueueReceive(s_example_espnow_queue, &evt, portMAX_DELAY) == pdTRUE) {
        switch (evt.id) {
            case EXAMPLE_ESPNOW_SEND_CB:
            {
                example_espnow_event_send_cb_t *send_cb = &evt.info.send_cb;
                is_broadcast = IS_BROADCAST_ADDR(send_cb->mac_addr);

                ESP_LOGD(TAG, "Send data to "MACSTR", status1: %d", MAC2STR(send_cb->mac_addr), send_cb->status);

                if (is_broadcast && (send_param->broadcast == false)) {
                    break;
                }

                if (!is_broadcast) {
                    send_param->count--;
                    if (send_param->count == 0) {
                        ESP_LOGI(TAG, "Send done");
                        example_espnow_deinit(send_param);
                        vTaskDelete(NULL);
                    }
                }

                /* Delay a while before sending the next data. */
                if (send_param->delay > 0) {
                    vTaskDelay(send_param->delay/portTICK_PERIOD_MS);
                }

                ESP_LOGI(TAG, "send data to "MACSTR"", MAC2STR(send_cb->mac_addr));

                memcpy(send_param->dest_mac, send_cb->mac_addr, ESP_NOW_ETH_ALEN);
                example_espnow_data_prepare(send_param);

                /* Send the next data after the previous data is sent. */
                if (esp_now_send(send_param->dest_mac, send_param->buffer, send_param->len) != ESP_OK) {
                    ESP_LOGE(TAG, "Send error");
                    example_espnow_deinit(send_param);
                    vTaskDelete(NULL);
                }
                break;
            }
            case EXAMPLE_ESPNOW_RECV_CB:
            {
                example_espnow_event_recv_cb_t *recv_cb = &evt.info.recv_cb;

                ret = example_espnow_data_parse(recv_cb->data, recv_cb->data_len, &recv_state, &recv_seq, &recv_magic);
                free(recv_cb->data);
                if (ret == EXAMPLE_ESPNOW_DATA_BROADCAST) {
                    ESP_LOGI(TAG, "Receive %dth broadcast data from: "MACSTR", len: %d", recv_seq, MAC2STR(recv_cb->mac_addr), recv_cb->data_len);

                    /* If MAC address does not exist in peer list, add it to peer list. */
                    if (esp_now_is_peer_exist(recv_cb->mac_addr) == false) {
                        esp_now_peer_info_t *peer = malloc(sizeof(esp_now_peer_info_t));
                        if (peer == NULL) {
                            ESP_LOGE(TAG, "Malloc peer information fail");
                            example_espnow_deinit(send_param);
                            vTaskDelete(NULL);
                        }
                        memset(peer, 0, sizeof(esp_now_peer_info_t));
                        peer->channel = CONFIG_ESPNOW_CHANNEL;
                        peer->ifidx = ESPNOW_WIFI_IF;
                        peer->encrypt = true;
                        memcpy(peer->lmk, CONFIG_ESPNOW_LMK, ESP_NOW_KEY_LEN);
                        memcpy(peer->peer_addr, recv_cb->mac_addr, ESP_NOW_ETH_ALEN);
                        ESP_ERROR_CHECK( esp_now_add_peer(peer) );
                        free(peer);
                    }

                    /* Indicates that the device has received broadcast ESPNOW data. */
                    if (send_param->state == 0) {
                        send_param->state = 1;
                    }

                    /* If receive broadcast ESPNOW data which indicates that the other device has received
                     * broadcast ESPNOW data and the local magic number is bigger than that in the received
                     * broadcast ESPNOW data, stop sending broadcast ESPNOW data and start sending unicast
                     * ESPNOW data.
                     */
                    if (recv_state == 1) {
                        /* The device which has the bigger magic number sends ESPNOW data, the other one
                         * receives ESPNOW data.
                         */
                        if (send_param->unicast == false && send_param->magic >= recv_magic) {
                    	    ESP_LOGI(TAG, "Start sending unicast data");
                    	    ESP_LOGI(TAG, "send data to "MACSTR"", MAC2STR(recv_cb->mac_addr));

                    	    /* Start sending unicast ESPNOW data. */
                            memcpy(send_param->dest_mac, recv_cb->mac_addr, ESP_NOW_ETH_ALEN);
                            example_espnow_data_prepare(send_param);
                            if (esp_now_send(send_param->dest_mac, send_param->buffer, send_param->len) != ESP_OK) {
                                ESP_LOGE(TAG, "Send error");
                                example_espnow_deinit(send_param);
                                vTaskDelete(NULL);
                            }
                            else {
                                send_param->broadcast = false;
                                send_param->unicast = true;
                            }
                        }
                    }
                }
                else if (ret == EXAMPLE_ESPNOW_DATA_UNICAST) {
                    ESP_LOGI(TAG, "Receive %dth unicast data from: "MACSTR", len: %d", recv_seq, MAC2STR(recv_cb->mac_addr), recv_cb->data_len);

                    /* If receive unicast ESPNOW data, also stop sending broadcast ESPNOW data. */
                    send_param->broadcast = false;
                }
                else {
                    ESP_LOGI(TAG, "Receive error data from: "MACSTR"", MAC2STR(recv_cb->mac_addr));
                }
                break;
            }
            default:
                ESP_LOGE(TAG, "Callback type error: %d", evt.id);
                break;
        }
    }
}

static esp_err_t example_espnow_init(void)
{
    example_espnow_send_param_t *send_param;

    s_example_espnow_queue = xQueueCreate(ESPNOW_QUEUE_SIZE, sizeof(example_espnow_event_t));
    if (s_example_espnow_queue == NULL) {
        ESP_LOGE(TAG, "Create mutex fail");
        return ESP_FAIL;
    }

    /* Initialize ESPNOW and register sending and receiving callback function. */
    ESP_ERROR_CHECK( esp_now_init() );
    ESP_ERROR_CHECK( esp_now_register_send_cb(example_espnow_send_cb) );
    ESP_ERROR_CHECK( esp_now_register_recv_cb(example_espnow_recv_cb) );
#if CONFIG_ESPNOW_ENABLE_POWER_SAVE
    ESP_ERROR_CHECK( esp_now_set_wake_window(CONFIG_ESPNOW_WAKE_WINDOW) );
    ESP_ERROR_CHECK( esp_wifi_connectionless_module_set_wake_interval(CONFIG_ESPNOW_WAKE_INTERVAL) );
#endif
    /* Set primary master key. */
    ESP_ERROR_CHECK( esp_now_set_pmk((uint8_t *)CONFIG_ESPNOW_PMK) );

    /* Add broadcast peer information to peer list. */
    esp_now_peer_info_t *peer = malloc(sizeof(esp_now_peer_info_t));
    if (peer == NULL) {
        ESP_LOGE(TAG, "Malloc peer information fail");
        vSemaphoreDelete(s_example_espnow_queue);
        esp_now_deinit();
        return ESP_FAIL;
    }
    memset(peer, 0, sizeof(esp_now_peer_info_t));
    peer->channel = CONFIG_ESPNOW_CHANNEL;
    peer->ifidx = ESPNOW_WIFI_IF;
    peer->encrypt = false;
    memcpy(peer->peer_addr, s_example_broadcast_mac, ESP_NOW_ETH_ALEN);
    ESP_ERROR_CHECK( esp_now_add_peer(peer) );
    free(peer);

    /* Initialize sending parameters. */
    send_param = malloc(sizeof(example_espnow_send_param_t));
    if (send_param == NULL) {
        ESP_LOGE(TAG, "Malloc send parameter fail");
        vSemaphoreDelete(s_example_espnow_queue);
        esp_now_deinit();
        return ESP_FAIL;
    }
    memset(send_param, 0, sizeof(example_espnow_send_param_t));
    send_param->unicast = false;
    send_param->broadcast = true;
    send_param->state = 0;
    send_param->magic = esp_random();
    send_param->count = CONFIG_ESPNOW_SEND_COUNT;
    send_param->delay = CONFIG_ESPNOW_SEND_DELAY;
    send_param->len = CONFIG_ESPNOW_SEND_LEN;
    send_param->buffer = malloc(CONFIG_ESPNOW_SEND_LEN);
    if (send_param->buffer == NULL) {
        ESP_LOGE(TAG, "Malloc send buffer fail");
        free(send_param);
        vSemaphoreDelete(s_example_espnow_queue);
        esp_now_deinit();
        return ESP_FAIL;
    }
    memcpy(send_param->dest_mac, s_example_broadcast_mac, ESP_NOW_ETH_ALEN);
    example_espnow_data_prepare(send_param);

    xTaskCreate(example_espnow_task, "example_espnow_task", 2048, send_param, 4, NULL);

    return ESP_OK;
}

static void example_espnow_deinit(example_espnow_send_param_t *send_param)
{
    free(send_param->buffer);
    free(send_param);
    vSemaphoreDelete(s_example_espnow_queue);
    esp_now_deinit();
}

void app_main(void)
{
    // Initialize NVS
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_ERROR_CHECK( nvs_flash_erase() );
        ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK( ret );

    example_wifi_init();
    example_espnow_init();
}

代码功能概述

该示例展示如何使用 ESPNOW 在 ESP32 设备之间实现无线通信。具体流程如下:

  1. 设备初始化:
    初始化 WiFi、ESPNOW,并准备广播数据包。

  2. 广播通信:
    设备发送广播消息来发现其他设备。

  3. 对等设备动态添加:
    如果发现新的设备,将其添加为对等设备(Peer)。

  4. 单播通信:
    设备完成对等发现后,停止广播并开始单播通信。

  5. 数据校验:
    通过 CRC16 校验码验证接收数据的完整性。


代码翻译及注释

/*
   ESPNOW 示例头文件

   本示例代码为公共领域(Public Domain)或 CC0 许可(根据您的选择)。

   除非适用法律要求或书面同意,否则该软件按“现状”(AS IS)提供,
   不提供任何明示或暗示的保证或条件。
*/

#ifndef ESPNOW_EXAMPLE_H
#define ESPNOW_EXAMPLE_H

/* 根据配置,ESPNOW 可以在 Station 模式或 SoftAP 模式下运行 */
#if CONFIG_ESPNOW_WIFI_MODE_STATION
#define ESPNOW_WIFI_MODE WIFI_MODE_STA       // Station 模式(客户端模式)
#define ESPNOW_WIFI_IF   ESP_IF_WIFI_STA     // Station 网络接口
#else
#define ESPNOW_WIFI_MODE WIFI_MODE_AP        // SoftAP 模式(热点模式)
#define ESPNOW_WIFI_IF   ESP_IF_WIFI_AP      // SoftAP 网络接口
#endif

#define ESPNOW_QUEUE_SIZE           6        // ESPNOW 事件队列的大小

/* 判断目标地址是否为广播地址 */
#define IS_BROADCAST_ADDR(addr) (memcmp(addr, s_example_broadcast_mac, ESP_NOW_ETH_ALEN) == 0)

/* 定义 ESPNOW 事件类型 */
typedef enum {
    EXAMPLE_ESPNOW_SEND_CB,   // 发送回调事件
    EXAMPLE_ESPNOW_RECV_CB,   // 接收回调事件
} example_espnow_event_id_t;

/* 发送回调事件结构体 */
typedef struct {
    uint8_t mac_addr[ESP_NOW_ETH_ALEN];   // 目标设备的 MAC 地址
    esp_now_send_status_t status;        // 发送状态:成功或失败
} example_espnow_event_send_cb_t;

/* 接收回调事件结构体 */
typedef struct {
    uint8_t mac_addr[ESP_NOW_ETH_ALEN];   // 发送方的 MAC 地址
    uint8_t *data;                       // 指向接收到的数据缓冲区
    int data_len;                        // 接收到的数据长度
} example_espnow_event_recv_cb_t;

/* 事件信息联合体:包含发送和接收事件数据 */
typedef union {
    example_espnow_event_send_cb_t send_cb; // 发送事件信息
    example_espnow_event_recv_cb_t recv_cb; // 接收事件信息
} example_espnow_event_info_t;

/* ESPNOW 事件结构体:包含事件 ID 和事件信息 */
typedef struct {
    example_espnow_event_id_t id;         // 事件类型(发送或接收)
    example_espnow_event_info_t info;     // 事件信息
} example_espnow_event_t;

/* 定义 ESPNOW 数据类型 */
enum {
    EXAMPLE_ESPNOW_DATA_BROADCAST,        // 广播数据类型
    EXAMPLE_ESPNOW_DATA_UNICAST,          // 单播数据类型
    EXAMPLE_ESPNOW_DATA_MAX,              // 数据类型的最大值
};

/* 用户自定义的 ESPNOW 数据结构 */
typedef struct {
    uint8_t type;                         // 数据类型:广播或单播
    uint8_t state;                        // 标识是否接收到广播数据
    uint16_t seq_num;                     // 数据包的序列号
    uint16_t crc;                         // 数据包的 CRC 校验值
    uint32_t magic;                       // 用于识别设备的随机数
    uint8_t payload[0];                   // 数据负载(灵活长度)
} __attribute__((packed)) example_espnow_data_t;

/* ESPNOW 发送参数结构体 */
typedef struct {
    bool unicast;                         // 是否为单播
    bool broadcast;                       // 是否为广播
    uint8_t state;                        // 发送状态
    uint32_t magic;                       // 用于识别设备的随机数
    uint16_t count;                       // 发送的总次数
    uint16_t delay;                       // 发送数据的间隔,单位:ms
    int len;                              // 发送数据的长度,单位:字节
    uint8_t *buffer;                      // 指向发送数据缓冲区
    uint8_t dest_mac[ESP_NOW_ETH_ALEN];   // 目标设备的 MAC 地址
} example_espnow_send_param_t;

#endif /* ESPNOW_EXAMPLE_H */

/*
   This example shows how to use ESPNOW.
   Prepare two device, one for sending ESPNOW data and another for receiving
   ESPNOW data.
*/

#include <stdlib.h>        // 包含标准库头文件,提供内存分配、随机数生成、数据转换等函数。
#include <time.h>          // 提供时间相关函数,如获取系统时间、时间延迟等。
#include <string.h>        // 提供字符串操作函数,如 memcpy、strlen 和 memset 等。
#include <assert.h>        // 提供断言功能,用于程序调试和错误检查。

#include "freertos/FreeRTOS.h"  // FreeRTOS 核心头文件,提供实时操作系统功能。
#include "freertos/semphr.h"    // FreeRTOS 信号量和互斥锁 API。
#include "freertos/timers.h"    // FreeRTOS 软件定时器 API。

#include "nvs_flash.h"     // 非易失性存储 (NVS) 相关功能,提供数据的存储和恢复。
#include "esp_random.h"    // 提供硬件随机数生成函数。
#include "esp_event.h"     // ESP-IDF 事件循环库,用于管理系统级事件和自定义事件。
#include "esp_netif.h"     // 提供网络接口初始化和管理功能。
#include "esp_wifi.h"      // Wi-Fi 驱动库,提供 Wi-Fi 初始化、配置和管理功能。
#include "esp_log.h"       // ESP-IDF 日志库,提供日志输出功能。
#include "esp_mac.h"       // 用于处理和管理 MAC 地址的 API。
#include "esp_now.h"       // ESPNOW 协议头文件,提供 ESPNOW 通信功能。
#include "esp_crc.h"       // 提供 CRC 校验功能,确保数据完整性。
#include "espnow_example.h" // 自定义头文件,通常包含示例代码的结构体和函数声明。

#define ESPNOW_MAXDELAY 512
/* 定义 ESPNOW 操作中的最大队列延迟时间。
 * 单位是 FreeRTOS 的 Tick(通常 1 Tick = 10ms,具体取决于系统配置)。
 * 这个值用于 `xQueueSend` 等函数,确保事件队列发送不会无限等待。
 */

static const char *TAG = "espnow_example";
/* 定义日志输出的标签字符串,用于标识日志的来源。
 * 在调用 ESP-IDF 提供的日志函数(如 ESP_LOGI 和 ESP_LOGE)时,会显示该标签。
 * 示例输出: [espnow_example]: 日志内容
 */

static QueueHandle_t s_example_espnow_queue;
/* 定义一个队列句柄 `s_example_espnow_queue`。
 * 该队列用于任务间传递 ESPNOW 事件(如发送回调事件和接收回调事件)。
 * - 发送任务:将发送结果放入队列。
 * - 接收任务:将接收到的数据放入队列。
 */

static uint8_t s_example_broadcast_mac[ESP_NOW_ETH_ALEN] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
/* 定义一个广播地址的 MAC 地址数组。
 * - `ESP_NOW_ETH_ALEN` 是 MAC 地址的字节长度,通常为 6。
 * - 全部为 `0xFF` 的地址表示广播地址,数据会发送给所有 ESPNOW 设备。
 */

static uint16_t s_example_espnow_seq[EXAMPLE_ESPNOW_DATA_MAX] = { 0, 0 };
/* 定义一个数组 `s_example_espnow_seq` 用于存储数据包的序列号。
 * - `EXAMPLE_ESPNOW_DATA_MAX` 表示 ESPNOW 数据类型的最大值(例如广播和单播两种类型)。
 * - 每种数据类型都有独立的序列号,用于区分发送的数据包,确保数据包有序。
 */

static void example_espnow_deinit(example_espnow_send_param_t *send_param);
/* 声明一个静态函数 `example_espnow_deinit`。
 * 功能:释放 ESPNOW 相关的资源,包括发送缓冲区和队列。
 *
 * 参数:
 *   - `send_param`: 发送参数结构体,包含了发送缓冲区等资源。
 */


/*********************************************************************
 * Wi-Fi 初始化函数
 * 功能:在使用 ESPNOW 之前,必须先启动 Wi-Fi 并进行相关配置。
 *       包括网络接口初始化、Wi-Fi 驱动初始化、设置工作模式和信道等。
 *********************************************************************/
static void example_wifi_init(void)
{
    // 1. 初始化网络接口(Network Interface)
    ESP_ERROR_CHECK(esp_netif_init());
    /* esp_netif_init():初始化网络接口库。
     * 必须调用该函数,确保网络接口(如 Wi-Fi 或以太网)能够正常工作。
     */

    // 2. 创建默认事件循环
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    /* esp_event_loop_create_default():创建一个默认的事件循环。
     * 事件循环用于处理系统事件(例如 Wi-Fi 事件)和用户自定义事件。
     */

    // 3. 初始化 Wi-Fi 驱动配置
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    /* WIFI_INIT_CONFIG_DEFAULT():获取 Wi-Fi 驱动的默认配置。
     * 该配置包含 Wi-Fi 初始化所需的参数,如缓冲区大小、任务优先级等。
     */

    ESP_ERROR_CHECK(esp_wifi_init(&cfg));
    /* esp_wifi_init(&cfg):初始化 Wi-Fi 驱动。
     * 将配置参数传入 Wi-Fi 驱动进行初始化。
     */

    // 4. 设置 Wi-Fi 存储类型为 RAM
    ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));
    /* esp_wifi_set_storage(WIFI_STORAGE_RAM):设置 Wi-Fi 配置存储方式。
     * WIFI_STORAGE_RAM 表示 Wi-Fi 配置存储在 RAM 中。
     * 每次重启设备时,Wi-Fi 配置会重置,需要重新配置。
     */

    // 5. 设置 Wi-Fi 工作模式
    ESP_ERROR_CHECK(esp_wifi_set_mode(ESPNOW_WIFI_MODE));
    /* esp_wifi_set_mode():设置 Wi-Fi 的工作模式。
     * ESPNOW_WIFI_MODE 是一个宏,通常配置为 WIFI_MODE_STA(站点模式)或 WIFI_MODE_AP。
     * ESPNOW 通信需要 Wi-Fi 处于工作状态,但不依赖于 AP 或连接到网络。
     */

    // 6. 启动 Wi-Fi
    ESP_ERROR_CHECK(esp_wifi_start());
    /* esp_wifi_start():启动 Wi-Fi 驱动。
     * 设备会进入初始化状态,但不会连接到任何 Wi-Fi 网络。
     */

    // 7. 设置 Wi-Fi 信道
    ESP_ERROR_CHECK(esp_wifi_set_channel(CONFIG_ESPNOW_CHANNEL, WIFI_SECOND_CHAN_NONE));
    /* esp_wifi_set_channel():设置 Wi-Fi 的信道。
     * CONFIG_ESPNOW_CHANNEL 是用户定义的信道号。
     * ESPNOW 通信的发送和接收设备必须位于相同的 Wi-Fi 信道上。
     * 第二个参数 WIFI_SECOND_CHAN_NONE 表示不使用次要信道。
     */

    // 8. 如果启用了长距离模式,设置 Wi-Fi 协议支持
#if CONFIG_ESPNOW_ENABLE_LONG_RANGE
    ESP_ERROR_CHECK(esp_wifi_set_protocol(ESPNOW_WIFI_IF, WIFI_PROTOCOL_11B | WIFI_PROTOCOL_11G | WIFI_PROTOCOL_11N | WIFI_PROTOCOL_LR));
    /* esp_wifi_set_protocol():设置 Wi-Fi 协议。
     * - WIFI_PROTOCOL_11B:支持 IEEE 802.11b 协议。
     * - WIFI_PROTOCOL_11G:支持 IEEE 802.11g 协议。
     * - WIFI_PROTOCOL_11N:支持 IEEE 802.11n 协议(高速传输)。
     * - WIFI_PROTOCOL_LR:启用长距离模式(低速率但长距离传输)。
     * 该配置使设备可以进行低速率但长距离的通信。
     */
#endif
}

/* ESPNOW 发送回调函数
 *
 * 功能:
 *   - 当 ESPNOW 数据发送完成(成功或失败)时,会自动调用此回调函数。
 *   - 该函数在 Wi-Fi 任务中运行,用户不应在此执行耗时操作,否则会影响系统性能。
 *   - 处理逻辑应通过事件队列将数据传递给其他任务,以便在低优先级任务中处理发送结果。
 *
 * 参数:
 *   - const uint8_t *mac_addr: 目标设备的 MAC 地址。
 *   - esp_now_send_status_t status: 发送状态(成功或失败)。
 */
 
static void example_espnow_send_cb(const uint8_t *mac_addr, esp_now_send_status_t status)
{
    example_espnow_event_t evt;              // 定义一个 ESP-NOW 事件结构体
    example_espnow_event_send_cb_t *send_cb = &evt.info.send_cb; // 事件结构体中的发送回调信息

    /* 1. 检查参数有效性 */
    if (mac_addr == NULL) {
        ESP_LOGE(TAG, "Send cb arg error");  // 如果目标地址为空,输出错误日志
        return;
    }

    /* 2. 设置事件 ID 和发送回调相关信息 */
    evt.id = EXAMPLE_ESPNOW_SEND_CB;         // 设置事件类型为 "发送回调"
    memcpy(send_cb->mac_addr, mac_addr, ESP_NOW_ETH_ALEN); // 复制目标设备的 MAC 地址
    send_cb->status = status;               // 设置发送结果状态(成功或失败)

    /* 3. 将事件发送到队列中 */
    if (xQueueSend(s_example_espnow_queue, &evt, ESPNOW_MAXDELAY) != pdTRUE) {
        ESP_LOGW(TAG, "Send send queue fail"); // 如果队列已满,输出警告日志
    }
}

/* ESPNOW 接收回调函数
 *
 * 功能:
 *   - 当 ESPNOW 接收到数据时,此回调函数会被触发。
 *   - 解析接收到的数据包的源地址、目标地址和数据内容。
 *   - 将接收到的数据打包成事件,并发送到任务队列进行异步处理。
 *
 * 参数:
 *   - const esp_now_recv_info_t *recv_info: 包含接收方的信息(如源地址、目标地址)。
 *   - const uint8_t *data: 指向接收到的数据缓冲区。
 *   - int len: 数据长度。
 */
static void example_espnow_recv_cb(const esp_now_recv_info_t *recv_info, const uint8_t *data, int len)
{
    example_espnow_event_t evt;               // 定义一个 ESP-NOW 事件结构体
    example_espnow_event_recv_cb_t *recv_cb = &evt.info.recv_cb; // 事件结构体中的接收回调信息
    uint8_t *mac_addr = recv_info->src_addr;  // 发送方的 MAC 地址
    uint8_t *des_addr = recv_info->des_addr;  // 目标地址(单播或广播)

    /* 1. 检查参数有效性 */
    if (mac_addr == NULL || data == NULL || len <= 0) {
        ESP_LOGE(TAG, "Receive cb arg error"); // 参数错误,输出日志
        return;
    }

    /* 2. 判断数据类型(广播或单播) */
    if (IS_BROADCAST_ADDR(des_addr)) { // 判断是否为广播地址
        /* 如果添加了加密的对等设备,接收的数据可能是加密的点对点消息,
         * 也可能是未加密的广播消息,可以通过目标地址区分。
         */
        ESP_LOGD(TAG, "Receive broadcast ESPNOW data");
    } else {
        ESP_LOGD(TAG, "Receive unicast ESPNOW data");
    }

    /* 3. 打包接收事件 */
    evt.id = EXAMPLE_ESPNOW_RECV_CB;          // 设置事件类型为“接收事件”
    memcpy(recv_cb->mac_addr, mac_addr, ESP_NOW_ETH_ALEN); // 复制源 MAC 地址

    /* 4. 分配内存存储接收到的数据 */
    recv_cb->data = malloc(len);              // 为接收数据分配内存
    if (recv_cb->data == NULL) {              // 检查内存分配是否成功
        ESP_LOGE(TAG, "Malloc receive data fail");
        return;
    }
    memcpy(recv_cb->data, data, len);         // 复制接收到的数据内容
    recv_cb->data_len = len;                  // 设置接收数据的长度

    /* 5. 将接收事件发送到事件队列 */
    if (xQueueSend(s_example_espnow_queue, &evt, ESPNOW_MAXDELAY) != pdTRUE) {
        ESP_LOGW(TAG, "Send receive queue fail"); // 如果队列已满,输出警告
        free(recv_cb->data);                // 释放已分配的数据内存,避免内存泄漏
    }
}

/* 解析接收到的 ESPNOW 数据包
 *
 * 功能:
 *   - 从接收到的原始数据包中提取状态(state)、序列号(seq)和 magic 值。
 *   - 验证数据包的完整性,通过计算 CRC 校验码与数据包中的 CRC 进行比较。
 *
 * 参数:
 *   - uint8_t *data: 指向接收到的数据缓冲区。
 *   - uint16_t data_len: 数据包的长度。
 *   - uint8_t *state: 输出参数,保存提取的数据包的状态字段。
 *   - uint16_t *seq: 输出参数,保存提取的数据包的序列号。
 *   - uint32_t *magic: 输出参数,保存提取的数据包的 magic 字段。
 *
 * 返回值:
 *   - 数据类型(广播或单播): 如果解析成功且 CRC 校验通过。
 *   - -1: 如果数据包无效或 CRC 校验失败。
 */
int example_espnow_data_parse(uint8_t *data, uint16_t data_len, uint8_t *state, uint16_t *seq, uint32_t *magic)
{
    example_espnow_data_t *buf = (example_espnow_data_t *)data;  // 将数据缓冲区转换为自定义数据结构
    uint16_t crc, crc_cal = 0;                                  // 定义 CRC 校验相关变量

    /* 1. 检查数据长度是否足够 */
    if (data_len < sizeof(example_espnow_data_t)) {
        ESP_LOGE(TAG, "Receive ESPNOW data too short, len:%d", data_len); // 数据长度不足,输出错误日志
        return -1;  // 返回错误
    }

    /* 2. 提取数据包的各个字段 */
    *state = buf->state;       // 提取状态字段
    *seq = buf->seq_num;       // 提取序列号字段
    *magic = buf->magic;       // 提取 magic 字段
    crc = buf->crc;            // 提取接收到的数据包中的 CRC 校验值

    /* 3. 清零 CRC 字段,计算校验码 */
    buf->crc = 0;  // 将 CRC 字段清零,因为计算 CRC 时不包含原始 CRC 值
    crc_cal = esp_crc16_le(UINT16_MAX, (uint8_t const *)buf, data_len); // 计算 CRC 校验码

    /* 4. 验证 CRC 校验值 */
    if (crc_cal == crc) {
        return buf->type;  // 如果 CRC 校验成功,返回数据包的类型(广播或单播)
    }

    return -1;  // CRC 校验失败,返回错误
}

/* 准备要发送的 ESPNOW 数据包
 *
 * 功能:
 *   - 根据目标地址类型(广播/单播)设置数据包类型。
 *   - 填充数据包的状态、序列号、随机标识符(magic)。
 *   - 填充数据负载的剩余部分为随机内容,确保数据包有足够长度。
 *   - 计算并附加 CRC 校验码,确保数据完整性。
 *
 * 参数:
 *   - example_espnow_send_param_t *send_param: 发送参数结构体,包含发送的目标地址、缓冲区和数据长度。
 */
void example_espnow_data_prepare(example_espnow_send_param_t *send_param)
{
    // 将发送缓冲区转换为自定义数据包结构体
    example_espnow_data_t *buf = (example_espnow_data_t *)send_param->buffer;

    // 1. 确保数据缓冲区的大小大于等于数据包结构体的大小
    assert(send_param->len >= sizeof(example_espnow_data_t));

    // 2. 设置数据包类型(广播或单播)
    buf->type = IS_BROADCAST_ADDR(send_param->dest_mac) ? 
                EXAMPLE_ESPNOW_DATA_BROADCAST : EXAMPLE_ESPNOW_DATA_UNICAST;
    /* 
     * - 如果目标地址是广播地址(FF:FF:FF:FF:FF:FF),类型为广播。
     * - 否则,类型为单播。
     */

    // 3. 设置状态字段
    buf->state = send_param->state;

    // 4. 生成数据包的序列号(自增)
    buf->seq_num = s_example_espnow_seq[buf->type]++;
    /*
     * s_example_espnow_seq[buf->type] 是一个数组,存储不同数据类型(广播/单播)的序列号。
     * 每次发送时,序列号自增,确保数据包有唯一的标识。
     */

    // 5. 清零 CRC 字段,准备计算新的 CRC 校验码
    buf->crc = 0;

    // 6. 设置 magic 字段(随机标识符)
    buf->magic = send_param->magic;

    // 7. 使用随机数据填充数据包的负载部分
    esp_fill_random(buf->payload, send_param->len - sizeof(example_espnow_data_t));
    /*
     * - `esp_fill_random` 是一个硬件随机数生成函数。
     * - 填充 payload 区域的所有剩余字节,增加数据的随机性,确保安全和测试的有效性。
     */

    // 8. 计算并填充 CRC 校验码
    buf->crc = esp_crc16_le(UINT16_MAX, (uint8_t const *)buf, send_param->len);
    /*
     * - CRC 校验用于验证数据包的完整性。
     * - esp_crc16_le:计算 CRC16 校验码,初始值为 UINT16_MAX。
     * - 校验码包括整个数据包(除了 CRC 字段本身)。
     */
}



/* ESPNOW 数据通信任务
 *
 * 功能:
 *   - 负责发送广播数据,并根据接收到的数据动态切换到单播通信模式。
 *   - 处理发送和接收的回调事件,确保数据正确发送和接收。
 *   - 动态维护对等节点列表,添加新的单播目标设备。
 *
 * 参数:
 *   - void *pvParameter: 任务输入参数,传递发送参数结构体(example_espnow_send_param_t)。
 */
 
static void example_espnow_task(void *pvParameter)
{
    example_espnow_event_t evt;  // 事件结构体
    uint8_t recv_state = 0;      // 接收到的数据状态
    uint16_t recv_seq = 0;       // 接收到的数据序列号
    uint32_t recv_magic = 0;     // 接收到的 magic 标识符
    bool is_broadcast = false;   // 标记当前是否为广播通信
    int ret;

    /* 1. 等待一段时间,开始发送广播数据 */
    vTaskDelay(5000 / portTICK_PERIOD_MS);
    ESP_LOGI(TAG, "Start sending broadcast data");

    /* 2. 开始发送广播 ESPNOW 数据 */
    example_espnow_send_param_t *send_param = (example_espnow_send_param_t *)pvParameter;
    if (esp_now_send(send_param->dest_mac, send_param->buffer, send_param->len) != ESP_OK) {
        ESP_LOGE(TAG, "Send error");
        example_espnow_deinit(send_param); // 释放资源
        vTaskDelete(NULL);                 // 删除当前任务
    }

    /* 3. 主循环:处理发送和接收回调事件 */
    while (xQueueReceive(s_example_espnow_queue, &evt, portMAX_DELAY) == pdTRUE) {
        switch (evt.id) {
            case EXAMPLE_ESPNOW_SEND_CB: // 发送回调事件
            {
                example_espnow_event_send_cb_t *send_cb = &evt.info.send_cb;

                is_broadcast = IS_BROADCAST_ADDR(send_cb->mac_addr); // 判断目标地址是否为广播

                ESP_LOGD(TAG, "Send data to "MACSTR", status1: %d", MAC2STR(send_cb->mac_addr), send_cb->status);

                /* 如果当前是广播模式且广播结束,退出广播流程 */
                if (is_broadcast && (send_param->broadcast == false)) {
                    break;
                }

                /* 如果是单播模式,发送指定次数后退出 */
                if (!is_broadcast) {
                    send_param->count--;
                    if (send_param->count == 0) {
                        ESP_LOGI(TAG, "Send done");
                        example_espnow_deinit(send_param); // 关闭 ESPNOW 功能
                        vTaskDelete(NULL);
                    }
                }

                /* 发送下一个数据包 */
                if (send_param->delay > 0) {
                    vTaskDelay(send_param->delay / portTICK_PERIOD_MS); // 发送延时
                }
                ESP_LOGI(TAG, "send data to "MACSTR"", MAC2STR(send_cb->mac_addr));

                memcpy(send_param->dest_mac, send_cb->mac_addr, ESP_NOW_ETH_ALEN);
                example_espnow_data_prepare(send_param);// 准备要发送的数据包 

                if (esp_now_send(send_param->dest_mac, send_param->buffer, send_param->len) != ESP_OK) {
                    ESP_LOGE(TAG, "Send error");
                    example_espnow_deinit(send_param); // // 关闭 ESPNOW 功能
                    vTaskDelete(NULL);
                }
                break;
            }

            case EXAMPLE_ESPNOW_RECV_CB: // 接收回调事件
            {
                example_espnow_event_recv_cb_t *recv_cb = &evt.info.recv_cb;

                /* 解析接收到的数据包 */
                ret = example_espnow_data_parse(recv_cb->data, recv_cb->data_len, &recv_state, &recv_seq, &recv_magic);
                free(recv_cb->data); // 释放数据内存

                if (ret == EXAMPLE_ESPNOW_DATA_BROADCAST) {
                    ESP_LOGI(TAG, "Receive %dth broadcast data from: "MACSTR", len: %d", recv_seq, MAC2STR(recv_cb->mac_addr), recv_cb->data_len);

                    /* 动态添加新的单播目标节点 */
                    if (esp_now_is_peer_exist(recv_cb->mac_addr) == false) {
                        esp_now_peer_info_t *peer = malloc(sizeof(esp_now_peer_info_t));
                        if (peer == NULL) {
                            ESP_LOGE(TAG, "Malloc peer information fail");
                            example_espnow_deinit(send_param);
                            vTaskDelete(NULL);
                        }
                        memset(peer, 0, sizeof(esp_now_peer_info_t));
                        peer->channel = CONFIG_ESPNOW_CHANNEL;
                        peer->ifidx = ESPNOW_WIFI_IF;
                        peer->encrypt = true; // 开启加密
                        memcpy(peer->lmk, CONFIG_ESPNOW_LMK, ESP_NOW_KEY_LEN); // 设置加密密钥
                        memcpy(peer->peer_addr, recv_cb->mac_addr, ESP_NOW_ETH_ALEN);
                        ESP_ERROR_CHECK(esp_now_add_peer(peer));
                        free(peer);
                    }

                    /* 如果接收到的数据状态指示其他设备也在通信,切换到单播模式 */
                    if (recv_state == 1 && send_param->magic >= recv_magic) {
                        ESP_LOGI(TAG, "Start sending unicast data");
                        memcpy(send_param->dest_mac, recv_cb->mac_addr, ESP_NOW_ETH_ALEN);
                        example_espnow_data_prepare(send_param);
                        esp_now_send(send_param->dest_mac, send_param->buffer, send_param->len);
                        send_param->broadcast = false;
                        send_param->unicast = true;
                    }
                }
                else if (ret == EXAMPLE_ESPNOW_DATA_UNICAST) {
                    ESP_LOGI(TAG, "Receive %dth unicast data from: "MACSTR", len: %d", recv_seq, MAC2STR(recv_cb->mac_addr), recv_cb->data_len);
                    send_param->broadcast = false;
                }
                else {
                    ESP_LOGI(TAG, "Receive error data from: "MACSTR"", MAC2STR(recv_cb->mac_addr));
                }
                break;
            }

            default:
                ESP_LOGE(TAG, "Callback type error: %d", evt.id);
                break;
        }
    }
}


/* 初始化 ESPNOW 模块
 *
 * 功能:
 *   - 初始化 ESPNOW 通信,包括回调注册、密钥设置、广播目标配置等。
 *   - 创建用于事件处理的 FreeRTOS 队列。
 *   - 分配并初始化发送参数。
 *   - 创建一个任务用于发送和接收 ESPNOW 数据。
 *
 * 返回值:
 *   - ESP_OK: 初始化成功。
 *   - ESP_FAIL: 初始化失败。
 */
static esp_err_t example_espnow_init(void)
{
    example_espnow_send_param_t *send_param;  // 发送参数结构体指针

    /* 1. 创建用于存储 ESPNOW 事件的队列 */ 
    s_example_espnow_queue = xQueueCreate(ESPNOW_QUEUE_SIZE, sizeof(example_espnow_event_t));
    
	if (s_example_espnow_queue == NULL) {
        ESP_LOGE(TAG, "Create queue fail");  // 如果队列创建失败,返回错误
        return ESP_FAIL;
    }

    /* 2. 初始化 ESPNOW,并注册发送和接收回调函数 */
    ESP_ERROR_CHECK( esp_now_init() );  // 初始化 ESPNOW 模块
    ESP_ERROR_CHECK( esp_now_register_send_cb(example_espnow_send_cb) ); // 注册发送回调
    ESP_ERROR_CHECK( esp_now_register_recv_cb(example_espnow_recv_cb) ); // 注册接收回调

#if CONFIG_ESPNOW_ENABLE_POWER_SAVE
    /* 3. 如果启用低功耗模式,设置唤醒窗口和间隔 */
    ESP_ERROR_CHECK( esp_now_set_wake_window(CONFIG_ESPNOW_WAKE_WINDOW) );
    ESP_ERROR_CHECK( esp_wifi_connectionless_module_set_wake_interval(CONFIG_ESPNOW_WAKE_INTERVAL) );
#endif

    /* 4. 设置 ESPNOW 主密钥(PMK) */
    ESP_ERROR_CHECK( esp_now_set_pmk((uint8_t *)CONFIG_ESPNOW_PMK) );

    /* 5. 添加广播目标到 ESPNOW 对等节点列表 */
    esp_now_peer_info_t *peer = malloc(sizeof(esp_now_peer_info_t));
    if (peer == NULL) {
        ESP_LOGE(TAG, "Malloc peer information fail"); // 内存分配失败
        vSemaphoreDelete(s_example_espnow_queue); // 删除队列
        esp_now_deinit();  // 卸载 ESPNOW 模块
        return ESP_FAIL;
    }
    memset(peer, 0, sizeof(esp_now_peer_info_t));  // 清空结构体内容
    peer->channel = CONFIG_ESPNOW_CHANNEL;        // 设置通信信道
    peer->ifidx = ESPNOW_WIFI_IF;                 // 设置 Wi-Fi 接口
    peer->encrypt = false;                        // 不使用加密
    memcpy(peer->peer_addr, s_example_broadcast_mac, ESP_NOW_ETH_ALEN); // 设置广播地址
    ESP_ERROR_CHECK( esp_now_add_peer(peer) );    // 添加广播对等节点
    free(peer);  // 释放 `peer` 结构体内存

    /* 6. 初始化发送参数 */
    send_param = malloc(sizeof(example_espnow_send_param_t));
    if (send_param == NULL) {
        ESP_LOGE(TAG, "Malloc send parameter fail"); // 发送参数内存分配失败
        vSemaphoreDelete(s_example_espnow_queue);
        esp_now_deinit();
        return ESP_FAIL;
    }
    memset(send_param, 0, sizeof(example_espnow_send_param_t)); // 清空发送参数结构体
    send_param->unicast = false;                               // 初始为广播发送
    send_param->broadcast = true;
    send_param->state = 0;
    send_param->magic = esp_random();                          // 设置随机 magic 值
    send_param->count = CONFIG_ESPNOW_SEND_COUNT;              // 发送次数
    send_param->delay = CONFIG_ESPNOW_SEND_DELAY;              // 发送间隔
    send_param->len = CONFIG_ESPNOW_SEND_LEN;                  // 数据长度
    send_param->buffer = malloc(CONFIG_ESPNOW_SEND_LEN);       // 分配发送缓冲区
    if (send_param->buffer == NULL) {
        ESP_LOGE(TAG, "Malloc send buffer fail"); // 发送缓冲区分配失败
        free(send_param);
        vSemaphoreDelete(s_example_espnow_queue);
        esp_now_deinit();
        return ESP_FAIL;
    }
    memcpy(send_param->dest_mac, s_example_broadcast_mac, ESP_NOW_ETH_ALEN); // 设置目标地址
    example_espnow_data_prepare(send_param); // 准备要发送的数据

    /* 7. 创建任务处理 ESPNOW 发送和接收 */
    xTaskCreate(example_espnow_task, "example_espnow_task", 2048, send_param, 4, NULL);

    return ESP_OK;  // 初始化成功
}



/* 释放 ESPNOW 资源并停止 ESPNOW 功能
 *
 * 功能:
 *   - 释放动态分配的内存资源,包括发送缓冲区和发送参数结构体。
 *   - 删除用于事件传递的 FreeRTOS 队列。
 *   - 停止并卸载 ESPNOW 模块。
 *
 * 参数:
 *   - example_espnow_send_param_t *send_param: 发送参数结构体,包含发送缓冲区等动态资源。
 */
static void example_espnow_deinit(example_espnow_send_param_t *send_param)
{
    // 1. 释放发送缓冲区的内存
    free(send_param->buffer);
    /*
     * send_param->buffer 是通过 malloc 动态分配的内存,
     * 用于存储待发送的 ESPNOW 数据包。
     * 调用 free() 释放这块内存,避免内存泄漏。
     */

    // 2. 释放发送参数结构体的内存
    free(send_param);
    /*
     * send_param 结构体本身也是通过 malloc 分配的。
     * 这里释放结构体内存,确保不会占用多余资源。
     */

    // 3. 删除事件队列
    vSemaphoreDelete(s_example_espnow_queue);
    /*
     * s_example_espnow_queue 是 FreeRTOS 的队列句柄。
     * 调用 vSemaphoreDelete() 删除队列,释放与队列相关的内存。
     * 队列用于传递 ESPNOW 事件(如发送和接收回调事件)。
     */

    // 4. 停止并卸载 ESPNOW 模块
    esp_now_deinit();
    /*
     * esp_now_deinit() 停止 ESPNOW 功能并释放相关资源。
     * 调用此函数后,ESP32 将不再能够通过 ESPNOW 通信。
     */
}

void app_main(void)
{
    // 初始化 NVS(非易失性存储器)
    esp_err_t ret = nvs_flash_init();
    /* nvs_flash_init():初始化 NVS 分区。
     * NVS(Non-Volatile Storage)用于存储非易失性数据,如 Wi-Fi 配置、密钥等。
     * 这一步是 ESP-IDF 项目中 Wi-Fi 或其他功能初始化的必要步骤。
     */

    /* 如果 NVS 分区已满或存在新版本的 NVS 数据结构,则擦除 NVS 分区 */
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_ERROR_CHECK(nvs_flash_erase()); // 擦除 NVS 分区数据
        ret = nvs_flash_init();            // 重新初始化 NVS
    }

    /* 检查 NVS 初始化结果是否成功 */
    ESP_ERROR_CHECK(ret);

    /* 初始化 Wi-Fi */
    example_wifi_init();
    /* 调用自定义的 Wi-Fi 初始化函数。
     * 该函数配置 Wi-Fi 工作模式、信道等参数,确保 Wi-Fi 模块正常工作。
     */

    /* 初始化 ESPNOW */
    example_espnow_init();
    /* 调用自定义的 ESPNOW 初始化函数。
     * 该函数初始化 ESPNOW 模块,设置回调函数、发送和接收参数,并开始通信。
     */
}


修改说明

  • 详细添加了注释,便于理解每个模块的功能。
  • 翻译了重要日志输出和函数说明。

使用步骤

  1. 将代码烧录到两个 ESP32 设备。
  2. 观察日志输出,设备将自动广播、发现对方并进行通信。

问题


1. 广播切换单播的过程是怎么样的?

初始阶段通过广播发现设备,后续阶段切换到单播进行数据传输。
代码中哪个位置是广播,又是如何切换成单播模式传输数据的? 两个设备不是需要绑定mac地址吗?怎么会从广播改为单播呢?

设备1开机广播出自己的地址,设备2接收到设备1的广播后提取广播数据中携带设备1的mac地址,并且提取出来添加进设备2的对等列表,绑定设备1的mac地址。这样设备1的mac地址就被设备2绑定了。

代码中广播与单播的实现及切换过程

在 ESP-NOW 通信中,广播与单播的主要区别是目标 MAC 地址的不同:

  1. 广播:目标地址设置为 FF:FF:FF:FF:FF:FF,即广播地址,所有设备都可以接收到数据。
  2. 单播:目标地址为特定设备的 MAC 地址,数据只发送给该设备。

在代码中,设备通过以下逻辑从 广播模式 切换到 单播模式


1. 广播发送的实现

初始阶段,设备使用广播地址进行数据发送。
代码位置在 example_espnow_task() 函数中:

/* 开始发送广播 ESPNOW 数据 */
if (esp_now_send(send_param->dest_mac, send_param->buffer, send_param->len) != ESP_OK) {
    ESP_LOGE(TAG, "Send error");
    example_espnow_deinit(send_param); // 释放资源
    vTaskDelete(NULL);                 // 删除当前任务
}
  • send_param->dest_mac 的初始值是广播地址 FF:FF:FF:FF:FF:FF
  • esp_now_send() 函数将数据发送到广播地址。
  • 所有在相同信道的设备都会接收到广播数据。

2. 设备接收到广播数据后如何进行处理

设备接收到广播数据后,会进入 接收回调函数 example_espnow_recv_cb()

static void example_espnow_recv_cb(const esp_now_recv_info_t *recv_info, const uint8_t *data, int len)
{
    /* 接收广播数据后,解析数据包,提取发送方 MAC 地址 */
    evt.id = EXAMPLE_ESPNOW_RECV_CB;
    memcpy(recv_cb->mac_addr, mac_addr, ESP_NOW_ETH_ALEN);
    recv_cb->data = malloc(len);
    memcpy(recv_cb->data, data, len);
    recv_cb->data_len = len;

    /* 将事件发送到事件队列 */
    if (xQueueSend(s_example_espnow_queue, &evt, ESPNOW_MAXDELAY) != pdTRUE) {
        free(recv_cb->data);
    }
}
  • 当设备接收到广播数据时,会提取发送方的 MAC 地址,并通过事件队列传递到主任务中。

3. 动态绑定对等设备的 MAC 地址

主任务 example_espnow_task() 解析接收到的数据后,会将发送方的 MAC 地址动态添加到 ESPNOW 对等节点列表中。

/* 如果 MAC 地址不在对等设备列表中,添加到对等设备列表 */
if (esp_now_is_peer_exist(recv_cb->mac_addr) == false) {
    esp_now_peer_info_t *peer = malloc(sizeof(esp_now_peer_info_t));
    memset(peer, 0, sizeof(esp_now_peer_info_t));
    peer->channel = CONFIG_ESPNOW_CHANNEL;
    peer->ifidx = ESPNOW_WIFI_IF;
    peer->encrypt = true; // 可选加密
    memcpy(peer->peer_addr, recv_cb->mac_addr, ESP_NOW_ETH_ALEN);
    ESP_ERROR_CHECK(esp_now_add_peer(peer));
    free(peer);
}
  • esp_now_is_peer_exist():检查对等节点是否已存在。
  • esp_now_add_peer():将接收到广播数据的设备 MAC 地址添加到对等节点列表,允许单播数据发送。

4. 切换到单播模式发送数据

当接收到的广播数据中标识设备状态(例如 state == 1)且满足特定条件时,设备会切换到单播模式:

/* 如果接收到的数据状态为 1,切换到单播模式 */
if (recv_state == 1 && send_param->magic >= recv_magic) {
    ESP_LOGI(TAG, "Start sending unicast data");

    /* 设置目标 MAC 地址为接收到的数据来源地址 */
    memcpy(send_param->dest_mac, recv_cb->mac_addr, ESP_NOW_ETH_ALEN);

    example_espnow_data_prepare(send_param); // 准备数据包
    esp_now_send(send_param->dest_mac, send_param->buffer, send_param->len); // 单播发送
    send_param->broadcast = false;
    send_param->unicast = true;
}
  • 逻辑
    1. 从接收到的广播数据中提取发送方的 MAC 地址。
    2. 将发送参数 send_param->dest_mac 设置为接收数据的设备 MAC 地址。
    3. 调用 esp_now_send() 发送数据,此时目标 MAC 地址是特定设备地址,即单播模式。

5. 关键流程总结

  1. 广播阶段

    • 设备通过广播地址 FF:FF:FF:FF:FF:FF 发送数据。
    • 所有设备接收广播数据,并提取发送方的 MAC 地址。
  2. 设备绑定 MAC 地址

    • 通过 esp_now_add_peer() 将发送方的 MAC 地址动态添加到对等设备列表中。
  3. 切换到单播模式

    • 从接收到的广播数据中提取发送方 MAC 地址,设置为单播目标地址。
    • 调用 esp_now_send() 发送数据,实现单播通信。

6. 为什么不需要提前绑定 MAC 地址?

ESPNOW 支持 动态添加对等节点

  • 初始阶段通过广播发送,所有设备都可以接收广播数据。
  • 接收设备会提取发送方的 MAC 地址,并通过 esp_now_add_peer() 动态绑定 MAC 地址。
  • 绑定后,即可通过单播模式发送数据,无需提前配置对方的 MAC 地址。

这种机制非常适合设备发现和配对的场景,例如 IoT 设备自动组网。


2.为什么设备1要向外发送广播地址为 FF:FF:FF:FF:FF:FF?

设备1向外发送广播数据,目标地址设置为 FF:FF:FF:FF:FF:FF,是为了实现网络中的设备发现,让所有 ESPNOW 支持的设备都能接收到该广播数据。这种设计有以下几个关键原因:

1. 广播地址的定义与作用

  • 广播地址 FF:FF:FF:FF:FF:FF网络中约定的特殊地址,表示将数据包发送给网络中的所有设备。
  • 任何设备如果处于相同信道并且启用了 ESPNOW 协议,都能够接收到发送到广播地址的数据包。

2. 设备发现的需求

  • 在初始阶段,设备1并不知道网络中其他设备的存在以及它们的 MAC 地址。
  • 为了让其他设备(例如设备2)能够识别并发现设备1,设备1需要发送一个广播数据包
  • 广播数据包的作用
    • 让网络中所有的 ESPNOW 设备都能接收这条信息。
    • 广播数据包中通常会携带设备1的 MAC 地址、状态信息、标识符等内容。
    • 其他设备(设备2)接收广播后,可以提取设备1的 MAC 地址,并将设备1添加到其对等节点列表中。

3. 实现动态绑定和对等通信

  • 动态发现与绑定

    • 设备1广播自己的存在,通过广播地址 FF:FF:FF:FF:FF:FF 让所有设备接收。
    • 设备2接收到广播数据后,提取设备1的 MAC 地址,使用 esp_now_add_peer() 将设备1添加到对等节点列表。
  • 单播通信切换

    • 当设备2将设备1的 MAC 地址绑定到对等节点列表中后,设备2就可以向设备1进行单播通信。
    • 单播通信目标是设备1的具体 MAC 地址,而不再使用广播地址。

4. 节省配置步骤,提高灵活性

  • 在实际应用中,提前配置设备的 MAC 地址(静态绑定)可能会增加配置复杂度。
  • 通过广播机制,设备1无需知道其他设备的存在,可以主动发送自己的信息,让其他设备自动发现并绑定其地址。
  • 这种动态绑定的方案非常适用于以下场景:
    • 设备自动发现:无需人工配置设备的 MAC 地址。
    • 系统灵活性:适合大规模部署和临时网络建立。

5. 广播的局限性与单播的优势

  • 广播数据包会被网络中所有设备接收,可能造成不必要的通信负担。
  • 一旦设备2绑定了设备1的 MAC 地址,就可以切换到单播模式进行通信:
    • 单播模式数据包仅传输给指定的目标设备,减少了网络中的数据冗余。
    • 提高通信效率,减少干扰。

总结

设备1向外发送目标地址为 FF:FF:FF:FF:FF:FF 的广播数据包,是为了让所有设备都能接收并发现它的存在。这种方式实现了动态设备发现,并为后续的单播通信奠定基础。广播机制的优势在于无需提前配置对方设备的 MAC 地址,适用于设备自动发现的场景。


3. 给设备1和设备2都烧录相同的程序,同时开机会出现怎么情况?

如果你给两个设备(设备1 和 设备2)都烧录相同的 ESPNOW 程序,并同时上电启动,将会出现以下情况:


1. 两个设备都进行广播发送

  • 程序初始化时,两个设备都会发送广播数据,目标地址是 FF:FF:FF:FF:FF:FF
  • 广播数据中会携带设备的 MAC 地址、状态(state = 0)和其他标识信息(例如随机生成的 magic 值)。
  • 两个设备都会试图让其他设备发现自己并进行后续的绑定。

2. 设备互相接收对方的广播数据

  • 设备1 会接收到设备2 发送的广播数据,解析其中的 MAC 地址和 state 信息。
  • 设备2 同样会接收到设备1 发送的广播数据,并提取设备1 的 MAC 地址。

3. 添加对等节点(动态绑定)

  • 设备1 在接收到设备2 的广播数据后,会提取设备2 的 MAC 地址并调用 esp_now_add_peer(),将设备2 添加到自己的对等节点列表中。
  • 设备2 同样在接收到设备1 的广播数据后,将设备1 的 MAC 地址添加到对等节点列表中。

4. 判断谁先发送单播数据

设备的广播数据中包含 magic 值(一个随机生成的标识符),用于确定哪个设备先发送单播数据:

  • magic 值比较

    • 如果设备1 的 magic 值大于设备2 的 magic 值,设备1 会停止发送广播数据,并开始向设备2 发送单播数据
    • 如果设备2 的 magic 值大于设备1 的 magic 值,设备2 会停止发送广播数据,并开始向设备1 发送单播数据
  • 这条规则确保最终只有一个设备发送单播数据,而另一个设备接收数据。


5. 开始单播通信

  • 停止广播后,具有较大 magic 值的设备会切换到单播模式,向对方发送单播数据。
  • 此时,另一个设备会接收该单播数据。
  • 单播数据发送和接收都由相应的回调函数处理,数据会通过任务队列传递并在主循环中执行逻辑。

6. 结果总结

  • 设备发现与绑定:设备1 和 设备2 会通过广播发现彼此,并动态绑定对方的 MAC 地址。
  • 单播数据传输:较大 magic 值的设备会优先切换到单播模式,并开始传输数据,另一设备负责接收。
  • 广播停止:一旦切换到单播通信,两个设备就不再发送广播数据,避免资源浪费和干扰。

观察到的现象

  1. 在设备的串口监控输出中,你会看到类似以下的日志:

    • 广播阶段
      I (1000) espnow_example: Start sending broadcast data
      I (2000) espnow_example: Receive 0th broadcast data from: <MAC地址>, len: 200
      
    • 切换到单播阶段
      I (3000) espnow_example: Start sending unicast data
      I (4000) espnow_example: send data to <对方MAC地址>
      
  2. 最终,设备1 和 设备2 会建立单播通信,一个设备发送数据,另一个设备接收数据。


总结

两个设备烧录相同的程序并同时启动,会先进行广播通信,互相发现并绑定对方的 MAC 地址,然后通过magic 值比较确定哪个设备先发送单播数据。单播通信开始后,广播通信会停止,系统进入单播数据传输阶段。这种设计确保了设备能够自动发现并建立高效的点对点通信。


4. 如何稍作修改让设备1优先设备2发送广播,这样一来会发生什么情况?

要实现设备1 优先于设备2 发送广播,可以通过在代码中引入一个简单的优先级逻辑,确保设备1 先发送广播,而设备2 等待一段时间再启动广播。

以下是修改建议以及运行后的情况分析:


如何修改代码让设备1优先广播?

思路

example_espnow_task() 的初始化阶段,添加一个固定的延时,使设备2 稍后才开始广播。

代码修改

假设设备1 和设备2 的代码中添加一个设备标识,可以用 CONFIG_DEVICE_ID(通过 menuconfig 配置)来区分:

  1. 添加设备 ID 宏定义:
    menuconfig 中配置一个标志(例如 CONFIG_DEVICE_ID)来区分设备1 和设备2。

  2. 修改 example_espnow_task 函数:
    在设备2 的代码中增加一个额外的延时,例如 5 秒。这样设备1 会先发送广播数据,设备2 会稍后才开始广播。


示例代码修改:

example_espnow_task() 函数中增加设备 ID 判断:

// 设备2延迟5秒
#ifdef CONFIG_DEVICE_ID
    if (CONFIG_DEVICE_ID == 2) { // 设备ID为2时增加延时
        ESP_LOGI(TAG, "Device 2 delaying broadcast by 5 seconds...");
        vTaskDelay(5000 / portTICK_PERIOD_MS); // 延迟5秒
    }
#endif

ESP_LOGI(TAG, "Start sending broadcast data");

配置设备1和设备2

  1. 设备1:将 CONFIG_DEVICE_ID 配置为 1,正常启动广播。
  2. 设备2:将 CONFIG_DEVICE_ID 配置为 2,在广播启动前增加5秒延时。

发生的情况分析

1. 设备1 先发送广播

  • 设备1 上电后会立即开始广播数据,目标地址为 FF:FF:FF:FF:FF:FF
  • 设备2 上电后延迟5秒,这时设备1 已经发送了广播数据。

2. 设备2 接收到设备1 的广播

  • 设备2 在延时结束后,会收到设备1 的广播数据。
  • 设备2 提取设备1 的 MAC 地址,并将设备1 添加到对等设备列表(调用 esp_now_add_peer())。
  • 此时,设备2 停止发送广播,开始与设备1 进行单播通信。

3. 设备1 切换到单播模式

  • 设备1 会继续发送广播数据一段时间,直到它收到设备2 的单播数据为止。
  • 设备1 在接收到设备2 的回复后,停止广播,并切换到单播模式向设备2 发送数据。

4. 最终结果

  • 设备1 优先发送广播,设备2 接收到广播并提取设备1 的 MAC 地址后,立即切换到单播模式。
  • 设备1 和设备2 之间建立单播通信,设备1 和设备2 通过单播发送和接收数据。

总结

  • 设备1 优先广播有助于减少不必要的广播冲突,确保设备2 更快地绑定设备1 的 MAC 地址。
  • 设备2 会等待一段时间才发送广播,避免两台设备同时广播,导致资源冲突和干扰。
  • 这种情况下,系统会更快地完成设备发现和单播通信的切换。

获取MAC地址

要获取 ESP32 的 MAC 地址,可以通过 ESP-IDF 提供的 esp_wifi_get_mac() 函数。这可以在 Wi-Fi 设置完成后,获取当前设备的 MAC 地址。

获取 ESP32 MAC 地址的步骤:

  1. 包含必要的头文件
    在代码中需要包含相关的 Wi-Fi 配置头文件。

    #include "esp_wifi.h"
    #include "esp_system.h"
    
  2. 使用 esp_wifi_get_mac() 获取 MAC 地址
    通过 esp_wifi_get_mac() 函数来获取 MAC 地址。该函数可以返回当前 Wi-Fi 接口(Station 或 SoftAP)的 MAC 地址。

    uint8_t mac[6]; // 存储MAC地址
    esp_err_t ret = esp_wifi_get_mac(ESP_IF_WIFI_STA, mac);  // 获取Station模式的MAC地址
    if (ret == ESP_OK) {
        printf("MAC地址: %02x:%02x:%02x:%02x:%02x:%02x\n",
               mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
    } else {
        printf("获取MAC地址失败\n");
    }
    

参数说明:

  • ESP_IF_WIFI_STA:表示 Wi-Fi Station 模式,获取的是当前设备的 Station 模式下的 MAC 地址。
  • ESP_IF_WIFI_AP:表示 Wi-Fi SoftAP 模式,获取的是设备作为热点时的 MAC 地址。

在你的代码中,可以根据需要选择是获取 Station 模式(客户端模式)的 MAC 地址,还是 SoftAP 模式(热点模式)的 MAC 地址。

示例代码:

#include "esp_wifi.h"
#include "esp_system.h"
#include "esp_log.h"

void app_main(void) {
    // 初始化 Wi-Fi
    esp_wifi_init(NULL);

    // 获取 Station 模式下的 MAC 地址
    uint8_t mac_sta[6];
    esp_wifi_get_mac(ESP_IF_WIFI_STA, mac_sta);

    ESP_LOGI("MAC Address", "Station MAC: %02x:%02x:%02x:%02x:%02x:%02x",
             mac_sta[0], mac_sta[1], mac_sta[2], mac_sta[3], mac_sta[4], mac_sta[5]);

    // 获取 SoftAP 模式下的 MAC 地址
    uint8_t mac_ap[6];
    esp_wifi_get_mac(ESP_IF_WIFI_AP, mac_ap);

    ESP_LOGI("MAC Address", "SoftAP MAC: %02x:%02x:%02x:%02x:%02x:%02x",
             mac_ap[0], mac_ap[1], mac_ap[2], mac_ap[3], mac_ap[4], mac_ap[5]);
}

运行结果:

I (xx) MAC Address: Station MAC: 30:ae:a4:xx:xx:xx
I (xx) MAC Address: SoftAP MAC: 30:ae:a4:yy:yy:yy

在这个例子中,mac_sta 获取的是设备的 Station 模式 MAC 地址,而 mac_ap 获取的是 SoftAP 模式 MAC 地址


总结

通过 esp_wifi_get_mac() 函数,可以轻松获取 ESP32 的 MAC 地址,无论是在 Station 模式还是 SoftAP 模式下。


广播改单播

要将乐鑫(Espressif)官方提供的 ESPNOW 示例中的 广播模式 改为 单播模式,只需进行以下修改:


修改步骤

  1. 移除广播地址

    • 官方例程使用广播地址 FF:FF:FF:FF:FF:FF 进行数据广播。
    • 替换广播地址为接收设备的 MAC 地址
  2. 静态绑定接收设备的 MAC 地址

    • 在发送端代码中,预先设置接收设备的 MAC 地址并添加到 ESPNOW 的对等列表中。
  3. 禁用广播数据

    • 修改发送参数结构体,将广播模式标志设置为 false,单播标志设置为 true

具体修改代码

1. 定义接收端设备的 MAC 地址

在头文件或主程序的开头定义目标设备的 MAC 地址(单播目标地址):

// 目标设备的 MAC 地址 (例:接收设备的 MAC)
static const uint8_t target_device_mac[ESP_NOW_ETH_ALEN] = {0xE4, 0xB0, 0x63, 0x22, 0xD7, 0x48};

2. 移除广播地址,将目标设备添加到对等设备列表

在初始化 ESPNOW 的代码中,替换广播地址为目标设备的 MAC 地址。

修改 example_espnow_init() 函数:

/* 5. 添加目标设备到 ESPNOW 对等节点列表 */
esp_now_peer_info_t *peer = malloc(sizeof(esp_now_peer_info_t));
if (peer == NULL) {
    ESP_LOGE(TAG, "Malloc peer information fail");
    vSemaphoreDelete(s_example_espnow_queue);
    esp_now_deinit();
    return ESP_FAIL;
}
memset(peer, 0, sizeof(esp_now_peer_info_t));  // 清空结构体内容
peer->channel = CONFIG_ESPNOW_CHANNEL;        // 设置通信信道
peer->ifidx = ESPNOW_WIFI_IF;                 // 设置 Wi-Fi 接口
peer->encrypt = false;                        // 不使用加密
memcpy(peer->peer_addr, target_device_mac, ESP_NOW_ETH_ALEN); // 设置目标设备 MAC 地址
ESP_ERROR_CHECK(esp_now_add_peer(peer));    // 添加对等节点
free(peer);

3. 修改发送参数,将广播改为单播

example_espnow_init() 函数中,设置发送参数 unicastbroadcast 的标志:

send_param->unicast = true;   // 设置为单播模式
send_param->broadcast = false; // 禁用广播
memcpy(send_param->dest_mac, target_device_mac, ESP_NOW_ETH_ALEN); // 设置目标设备的 MAC 地址

4. 移除广播逻辑(可选)

在发送任务中,将原有广播相关的判断逻辑去掉,因为我们现在是 直接单播 目标设备:

example_espnow_task() 中:

/* 删除广播模式判断,直接发送单播数据 */
ESP_LOGI(TAG, "Sending unicast data to "MACSTR, MAC2STR(send_param->dest_mac));

example_espnow_data_prepare(send_param);
if (esp_now_send(send_param->dest_mac, send_param->buffer, send_param->len) != ESP_OK) {
    ESP_LOGE(TAG, "Send error");
    example_espnow_deinit(send_param);
    vTaskDelete(NULL);
}

修改后的逻辑

  1. 设备开机
    • 直接使用预定义的目标设备 MAC 地址进行单播通信。
  2. 发送端
    • 添加接收端的 MAC 地址到 ESPNOW 对等设备列表。
    • 直接向目标设备发送单播数据。
  3. 接收端
    • 处理接收到的 ESPNOW 单播数据。

小结

通过以上修改,广播模式被替换成了 单播模式,发送端无需广播发现设备,直接向目标设备的 MAC 地址进行数据发送。这样可以减少网络上的广播流量,提高通信的效率和安全性。


如果有任何疑问或者进一步需要帮助,请告诉我!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/936953.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Elasticsearch02-安装7.x

零、文章目录 Elasticsearch02-安装7.x 1、Windows安装Elasticsearch &#xff08;1&#xff09;JDK安装 Elasticsearch是基于java开发的&#xff0c;所以需要安装JDK。我们安装的Elasticsearch版本是7.15&#xff0c;对应JDK至少1.8版本以上。也可以不安装jdk&#xff0c;…

微信小程序中 crypto-js 加解密全攻略

一、引言 在微信小程序开发中&#xff0c;数据的安全至关重要。加解密技术在保护用户数据和应用程序的安全性方面起着关键作用。小程序在与服务器进行数据交互时&#xff0c;面临着数据泄露、篡改等安全风险。为了确保用户信息的安全&#xff0c;选择合适的加解密算法变得尤为…

fiddler设置抓取https,还抓取不到https如何解决?

一、清楚 C:\Users\Admin\AppData\Roaming\Microsoft\Crypto\RSA 目录下所有文件&#xff08;首次安装fiddler请忽略&#xff09; 二、清除电脑上的根证书&#xff0c;WINR快捷键&#xff0c;输入&#xff1a;certmgr.msc&#xff0c; 然后回车&#xff0c;查找所有fiddler证书…

React 19 除了 RSC 等新功能,还优化了什么?

提示:记录工作中遇到的需求及解决办法 文章目录 前言01. ref 作为 prop02. Context 作为 provider03. refs 的清理函数04. useDeferredValue 的初始值05. 支持文档元数据06. 支持样式表07. 支持异步脚本08. 支持预加载资源09. 支持自定义元素总结前言 React 19 正式发布,新功…

SpringBoot快速使用

一些名词的碎碎念: 1> 俩种网络应用设计模式 C/S 客户端/服务器 B/S 浏览器/服务器 俩者对比: 2> 集群和分布式的概念 集群: 分布式: 例子: 一个公司有一个人身兼多职 集群: 招聘N个和上面这个人一样身兼多职 分布式: 招聘N个人,分担上面这个人的工作,进行工作的拆分. 工…

ASP.NET |日常开发中读写XML详解

ASP.NET &#xff5c;日常开发中读写XML详解 前言一、XML 概述1.1 定义和结构1.2 应用场景 二、读取 XML 文件2.1 使用XmlDocument类&#xff08;DOM 方式&#xff09;2.2 使用XmlReader类&#xff08;流方式&#xff09; 三、写入 XML 文件3.1 使用XmlDocument类3.2 使用XmlWr…

PLC6-CODESYS 的库问题:更改库版本

目录 【一】在安装SP8时NBS 3.5.8.0 依赖的TCP 3.5.8.10不能下载导致程序报错。 【二】移植codesys程序时通常会有库缺失&#xff0c;需要在库管理器选项卡中电机下载缺失的库&#xff0c;也可在报错信息处右键更新占位符 【三】低版本软件添加库需要点击添加库--高级--然后…

如何使mysql数据库ID从0开始编号——以BiCorpus为例

BiCorpus是北京语言大学韩林涛老师研制一款在线语料库网站&#xff0c;可以通过上传tmx文件&#xff0c;实现在线检索功能&#xff0c;程序在github上开源免费&#xff0c;深受广大网友的喜欢。 在使用过程中&#xff0c;我发现我上传的语言资产经历修改后&#xff0c;mysql的…

什么叫ip地址一样?网络ip地址一样说明什么

在探索网络世界的奥秘中&#xff0c;IP地址作为网络设备的唯一身份标识&#xff0c;其重要性不言而喻。然而&#xff0c;当我们遇到“IP地址一样”的情况时&#xff0c;不禁会产生诸多疑问&#xff1a;这究竟意味着什么&#xff1f;是否会对网络产生影响&#xff1f;虎观代理小…

【Linux基础】基本开发工具的使用

目录 一、编译器——gcc/g的使用 gcc/g的安装 gcc的安装&#xff1a; g的安装&#xff1a; gcc/g的基本使用 gcc的使用 g的使用 动态链接与静态链接 程序的翻译过程 1. 一个C/C程序的构建过程&#xff0c;程序从源代码到可执行文件必须经历四个阶段 2. 理解选项的含…

全栈开发:后台管理系统时代的技术破局之道

在当前的互联网发展阶段&#xff0c;许多二三线城市的互联网项目正在经历一个显著的转变。传统的To C项目逐渐减少&#xff0c;取而代之的是大量的企业级后台管理系统。在这样的背景下&#xff0c;全栈开发——特别是前端加Java的组合&#xff0c;正在成为一个备受关注的发展方…

Jmeter 性能压测-Tomcat连接数

1、影响性能的线程状态 ①BLOCKED&#xff0c;如果线程中有BLOCKED&#xff0c;就代表有阻塞情况&#xff0c;需要进行排查 ②TIMED_WAITING&#xff0c;如果线程中有TIMED_WAITING&#xff0c;就代表有等待的情况&#xff0c;要分情况来排查 系统线程在等待&#xff08;如果…

SAP-ABAP开发学习-面向对象OOALV(1)

本文目录 一、概述 面向对象开发特点 二、类与对象 程序中类的创建 Class构成要素 对象 方法 一、概述 随着SAP R/3 4.0版本的开发&#xff0c;ABAP语言开始引入了面向对象的开发概念。这在ABAP语言的发展过程中&#xff0c;面向对象&#xff08;Object-oriented&#…

OpenCV 功能函数介绍

一&#xff0c; 二值化函数 功能&#xff1a; 用于对图像进行二值化处理 参数&#xff1a; cv2.threshold(输入你的图像所对应的灰度图&#xff0c; 阈值&#xff1a;是浮点还是整数取决予图像的数据类型 最大值;高于阈值的像素值&#xff0c; 阈值类型&#xff1a;cv2.THR…

kubeadm安装K8s集群之高可用组件keepalived+nginx及kubeadm部署

系列文章目录 1.kubeadm安装K8s集群之基础环境配置 2.kubeadm安装K8s集群之高可用组件keepalivednginx及kubeadm部署 3.kubeadm安装K8s集群之master节点加入 4.kubeadm安装K8s集群之worker1节点加入 kubeadm安装K8s集群之高可用组件keepalivednginx及kubeadm部署 1.安装kubeadm…

细说STM32F407单片机SPI基础知识

目录 一、 SPI接口和通信协议 1、 SPI硬件接口 &#xff08;1&#xff09;MOSI(Master Output Slave Input) &#xff08;2&#xff09;MISO(Master Input Slave Output) &#xff08;3&#xff09;SCK 2、SPI传输协议 &#xff08;1&#xff09;CPHA0时的数据传输时序 …

【图像处理】利用numpy、opencv、python实现车牌检测

| 利用opencv实现车牌检测 整体流程涉及5个部分 图像通道转换对比度增强边缘连接二值化边界区域裁剪 图像通道转换 将RGB图像转换为HSV图像&#xff0c;仅保留V通道。V通道表示颜色的明暗&#xff0c;常用于图像对比度拉伸、直方图均衡化等流程。 原图像&#xff1a; V通…

结构变量的占用多少个字节

1、在linux中&#xff0c;这种写法比较清晰 struct gpio_led_data { u8 can_sleep; //如果定义了结构变量&#xff0c;则该成员占用1个字节 u8 blinking; //如果定义了结构变量&#xff0c;则该成员占用1个字节 }; struct gpio_leds_priv { int num_leds; //如…

[COLM 2024] V-STaR: Training Verifiers for Self-Taught Reasoners

本文是对 STaR 的改进方法&#xff0c;COLM 是 Conference On Language Models&#xff0c;大模型领域新出的会议&#xff0c;在国际上很知名&#xff0c;不过目前还没有被列入 ccf list&#xff08;新会议一般不会列入&#xff09;&#xff1b;作者来自高校、微软研究院和 Goo…

uni-app Android平台上架要求的隐私政策提示配置方法【跨端开发系列】

文章目录 前言&#x1f4d6;一、前言二、DCloud 数据采集说明三、配置方式3.1 HBuilderX3.2.1及以上版本配置方式3.2 HBuilderX3.2.0及以下版本配置方法3.3 模板提示框3.4 无提示框 四、离线打包配置方式五、模板提示框六、二次确认提示框七、国际化八、隐私协议内容需要注意的…