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 设备之间实现无线通信。具体流程如下:
-
设备初始化:
初始化 WiFi、ESPNOW,并准备广播数据包。 -
广播通信:
设备发送广播消息来发现其他设备。 -
对等设备动态添加:
如果发现新的设备,将其添加为对等设备(Peer)。 -
单播通信:
设备完成对等发现后,停止广播并开始单播通信。 -
数据校验:
通过 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 模块,设置回调函数、发送和接收参数,并开始通信。
*/
}
修改说明
- 详细添加了注释,便于理解每个模块的功能。
- 翻译了重要日志输出和函数说明。
使用步骤
- 将代码烧录到两个 ESP32 设备。
- 观察日志输出,设备将自动广播、发现对方并进行通信。
问题
1. 广播切换单播的过程是怎么样的?
初始阶段通过广播发现设备,后续阶段切换到单播进行数据传输。
代码中哪个位置是广播,又是如何切换成单播模式传输数据的? 两个设备不是需要绑定mac地址吗?怎么会从广播改为单播呢?
设备1开机广播出自己的地址,设备2接收到设备1的广播后提取广播数据中携带设备1的mac地址,并且提取出来添加进设备2的对等列表,绑定设备1的mac地址。这样设备1的mac地址就被设备2绑定了。
代码中广播与单播的实现及切换过程
在 ESP-NOW 通信中,广播与单播的主要区别是目标 MAC 地址的不同:
- 广播:目标地址设置为
FF:FF:FF:FF:FF:FF
,即广播地址,所有设备都可以接收到数据。 - 单播:目标地址为特定设备的 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;
}
- 逻辑:
- 从接收到的广播数据中提取发送方的 MAC 地址。
- 将发送参数
send_param->dest_mac
设置为接收数据的设备 MAC 地址。 - 调用
esp_now_send()
发送数据,此时目标 MAC 地址是特定设备地址,即单播模式。
5. 关键流程总结
-
广播阶段:
- 设备通过广播地址
FF:FF:FF:FF:FF:FF
发送数据。 - 所有设备接收广播数据,并提取发送方的 MAC 地址。
- 设备通过广播地址
-
设备绑定 MAC 地址:
- 通过
esp_now_add_peer()
将发送方的 MAC 地址动态添加到对等设备列表中。
- 通过
-
切换到单播模式:
- 从接收到的广播数据中提取发送方 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添加到对等节点列表。
- 设备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 发送单播数据。
- 如果设备1 的
-
这条规则确保最终只有一个设备发送单播数据,而另一个设备接收数据。
5. 开始单播通信
- 停止广播后,具有较大
magic
值的设备会切换到单播模式,向对方发送单播数据。 - 此时,另一个设备会接收该单播数据。
- 单播数据发送和接收都由相应的回调函数处理,数据会通过任务队列传递并在主循环中执行逻辑。
6. 结果总结
- 设备发现与绑定:设备1 和 设备2 会通过广播发现彼此,并动态绑定对方的 MAC 地址。
- 单播数据传输:较大
magic
值的设备会优先切换到单播模式,并开始传输数据,另一设备负责接收。 - 广播停止:一旦切换到单播通信,两个设备就不再发送广播数据,避免资源浪费和干扰。
观察到的现象
-
在设备的串口监控输出中,你会看到类似以下的日志:
- 广播阶段:
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地址>
- 广播阶段:
-
最终,设备1 和 设备2 会建立单播通信,一个设备发送数据,另一个设备接收数据。
总结
两个设备烧录相同的程序并同时启动,会先进行广播通信,互相发现并绑定对方的 MAC 地址,然后通过magic 值比较确定哪个设备先发送单播数据。单播通信开始后,广播通信会停止,系统进入单播数据传输阶段。这种设计确保了设备能够自动发现并建立高效的点对点通信。
4. 如何稍作修改让设备1优先设备2发送广播,这样一来会发生什么情况?
要实现设备1 优先于设备2 发送广播,可以通过在代码中引入一个简单的优先级逻辑,确保设备1 先发送广播,而设备2 等待一段时间再启动广播。
以下是修改建议以及运行后的情况分析:
如何修改代码让设备1优先广播?
思路
在 example_espnow_task()
的初始化阶段,添加一个固定的延时,使设备2 稍后才开始广播。
代码修改
假设设备1 和设备2 的代码中添加一个设备标识,可以用 CONFIG_DEVICE_ID
(通过 menuconfig 配置)来区分:
-
添加设备 ID 宏定义:
在menuconfig
中配置一个标志(例如CONFIG_DEVICE_ID
)来区分设备1 和设备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:将
CONFIG_DEVICE_ID
配置为1
,正常启动广播。 - 设备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 地址的步骤:
-
包含必要的头文件:
在代码中需要包含相关的 Wi-Fi 配置头文件。#include "esp_wifi.h" #include "esp_system.h"
-
使用
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 示例中的 广播模式 改为 单播模式,只需进行以下修改:
修改步骤
-
移除广播地址:
- 官方例程使用广播地址
FF:FF:FF:FF:FF:FF
进行数据广播。 - 替换广播地址为接收设备的 MAC 地址。
- 官方例程使用广播地址
-
静态绑定接收设备的 MAC 地址:
- 在发送端代码中,预先设置接收设备的 MAC 地址并添加到 ESPNOW 的对等列表中。
-
禁用广播数据:
- 修改发送参数结构体,将广播模式标志设置为
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()
函数中,设置发送参数 unicast
和 broadcast
的标志:
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);
}
修改后的逻辑
- 设备开机:
- 直接使用预定义的目标设备 MAC 地址进行单播通信。
- 发送端:
- 添加接收端的 MAC 地址到 ESPNOW 对等设备列表。
- 直接向目标设备发送单播数据。
- 接收端:
- 处理接收到的 ESPNOW 单播数据。
小结
通过以上修改,广播模式被替换成了 单播模式,发送端无需广播发现设备,直接向目标设备的 MAC 地址进行数据发送。这样可以减少网络上的广播流量,提高通信的效率和安全性。
如果有任何疑问或者进一步需要帮助,请告诉我!