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 ExampleThis example code is in the Public Domain (or CC0 licensed, at your option.)Unless required by applicable law or agreed to in writing, thissoftware is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES ORCONDITIONS 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 receivingESPNOW 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 512static 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_RANGEESP_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_SAVEESP_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 NVSesp_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; // 发送数据的间隔,单位:msint 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 receivingESPNOW 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 存储类型为 RAMESP_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-FiESP_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_RANGEESP_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_IDif (CONFIG_DEVICE_ID == 2) { // 设备ID为2时增加延时ESP_LOGI(TAG, "Device 2 delaying broadcast by 5 seconds...");vTaskDelay(5000 / portTICK_PERIOD_MS); // 延迟5秒}
#endifESP_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-Fiesp_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 地址进行数据发送。这样可以减少网络上的广播流量,提高通信的效率和安全性。
如果有任何疑问或者进一步需要帮助,请告诉我!
相关文章:
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 协…...
Windows如何安装go环境,离线安装beego
一、安装go 1、下载go All releases - The Go Programming Language 通过网盘分享的文件:分享的文件 链接: https://pan.baidu.com/s/1MCbo3k3otSoVdmIR4mpPiQ 提取码: hxgf 下载amd64.zip文件,然后解压到指定的路径 2、配置环境变量 需要新建两个环境…...
【Unity技巧】如何设置屏幕最小宽度
在 Unity 中,设置屏幕最小宽度可以通过调整 Canvas 的 CanvasScaler 组件来控制 UI 元素的缩放,并确保 UI 在不同屏幕宽度下始终能保持适当的布局。 不过,如果你想要限制游戏的实际窗口宽度,通常是通过代码来实现的。例如&#x…...
【新版】阿里云ACP云计算题库及答案解析
阿里云ACO云计算考试提醒都是选择题,70道单选题30道单选题,聪明的小伙伴都知道刷题备考加深记忆,给大家分享一波阿里云ACP云计算题库及答案,希望对大家顺利拿到阿里云ACP云计算高级工程师证书有所帮助! 1、设计云上架…...
【蓝桥杯每日一题】推导部分和——带权并查集
推导部分和 2024-12-11 蓝桥杯每日一题 推导部分和 带权并查集 题目大意 对于一个长度为 ( N ) 的整数数列 A 1 , A 2 , ⋯ , A N A_1, A_2, \cdots, A_N A1,A2,⋯,AN ,小蓝想知道下标 ( l ) 到 ( r ) 的部分和 ∑ i l r A i A l A l 1 ⋯ A r \sum_{…...
【Linux服务器nginx前端部署详解】ubantu22.04,前端Vue项目dist打包
本文主要讲一下在Linux系统环境下(以ubantu22.04为例),如何用nginx部署前端Vue项目打包的dist静态资源。有些具体的命令就不展开讲了,可以自行查看其他博主的文章,我主要讲整体的步骤和思路。 一、ubantu系统安装ngin…...
Groovy 语法快速入门
文章目录 1. Groovy 的特点2. 基本语法2.1. 变量2.2. 字符串2.3. 条件语句 3. 集合操作3.1. 列表(List)3.2. 映射(Map) 4. 循环语句4.1. 普通循环4.2. 闭包遍历 5. 方法定义6. 闭包(Closure)6.1. 定义与调用…...
vue3 Textarea在光标定位处,增加一定的关键词。
1、经常碰到这种情况,有一些是系统预留的关键词可以选择,当用户把光标定位到什么地方,我们就要在这个位置插入指定的关键词。 2、光标定位在今天的前面,那么我们点击【逗号】按钮,在这个位置增加一个逗号。 3、代码&…...
硬件设计-电源轨噪声对时钟抖动的影响
目录 定义 实际案例 总结 定义 首先了解抖动的定义,在ITU-T G.701中有关抖动的定义如下: 数字信号重要瞬间相对于其理想时间位置的短期非累积变化。 抖动是时钟或数据信号时序的短期时域变化。抖动包括信号周期、频率、相位、占空比或其他一些定时特…...
Graspness 端到端抓取点估计 | 环境搭建 | 模型推理测试
在复杂场景中实现抓取检测,Graspness是一种端到端的方法; 输入点云数据,输出抓取角度、抓取深度、夹具宽度等信息。 开源地址:https://github.com/rhett-chen/graspness_implementation?tabreadme-ov-file 论文地址࿱…...
大模型呼入机器人的缺点是什么?(转)
大模型呼入机器人的缺点是什么?(转) 原作者:开源呼叫中心FreeIPCC,其Github:https://github.com/FreeIPCC/FreeIPCC 大模型呼入机器人在提供高效、自动化服务的同时,也存在一些缺点。以下是对其缺点的详细归纳&#…...
ASP.NET |日常开发中连接Oracle数据库详解
ASP.NET |日常开发中连接Oracle数据库详解 前言一、安装和配置 Oracle 数据访问组件1.1 安装ODP.NET(Oracle Data Provider for.NET):1.2 引用相关程序集: 二、配置连接字符串2.1 连接字符串的基本组成部分:…...
Kaggler日志-Day4
进度24/12/14 昨日复盘: Pandas课程完成 Intermediate Mechine Learning2/7 今日记录: Intermediate Mechine Learning之类型变量 读两篇讲解如何提问的文章,在提问区里发起一次提问 实战:自己从头到尾首先Housing Prices Compe…...
onnx算子的注册详解及案例 (完整版)
文章目录 1. 介绍1.1 导出onnx不成功1.2 分析和解决方案2. 案例2.1 Asinh算子注册2.1.1 导出onnx2.1.2 算子注册2.2 自定义算子的注册2.1 直接导出自定义算子2.2 自定义算子的注册并导出2.3 导出带deformable conv 的onnx2.3.1 直接导出deformable conv2.3.2 注册并导出deforma…...
2024生命科学前沿技术
前沿技术是指高技术领域中具有前瞻性、先导性和探索性的重大技术,是未来高技术更新换代和新兴产业发展的重要基础,是国家高技术创新能力的综合体现。选择前沿技术的主要原则一是代表世界高技术前沿的发展方向。二是对国家未来新兴产业的形成和发展具有引…...
游戏引擎学习第47天
仓库: https://gitee.com/mrxiao_com/2d_game 昨天我们花了一点时间来修复一个问题,但基本上是在修复这个问题的过程中,我们决定添加一个功能,那就是在屏幕上控制多个实体。所以如果我有一个手柄,我可以添加另一个角色࿰…...
1.编写 Prompt 的原则
一、环境配置 使用 OpenAI 的 ChatGPT API,需要有 API_KEY,并安装 OpenAI 库。安装命令:pip install openai 和 pip install zhipuai。配置方法:直接设置 openai.api_key 或通过环境变量设置。 二、两个基本原则 2.1 原则一&am…...
【JavaEE】网络(2)
一、网络编程套接字 1.1 基础概念 【网络编程】指网络上的主机,通过不同的进程,以编程的方式实现网络通信;当然,我们只要满足进程不同就行,所以即便是同一个主机,只要是不同进程,基于网络来传…...
SAS - Subtractive Port
在SAS(串行连接SCSI,Serial Attached SCSI)协议中,subtractive port 是一种特殊类型的端口,主要用于设备间的路由功能。它的作用是在路径选择过程中充当默认路径,以处理未明确指定路径的请求。以下是它的定…...
Unity3D项目为什么要使用FairyGUI
前言 Unity3D项目选择使用FairyGUI的原因是多方面的,主要涵盖性能优化、设计模式、编辑器支持、跨平台兼容性以及丰富的功能特性。以下是对这些方面的详细解析以及相关的代码实现。 对惹,这里有一个游戏开发交流小组,希望大家可以点击进来一…...
Pytest接口自动化测试框架Python自动化测试开发
一、引言 在软件开发过程中,接口测试是确保软件各个组件之间数据传输和功能交互正常工作的重要环节。通过接口测试,可以提高软件的整体质量和稳定性。Pytest是一个流行的Python自动化测试框架,提供了丰富的断言方法和灵活的测试组织结构&…...
MySQL追梦旅途之性能优化
1、索引优化 索引可以显著加速查询操作,但过多或不适当的索引也会带来负面影响(如增加写入开销)。因此,选择合适的索引至关重要。 创建索引: 为经常用于WHERE子句、JOIN条件和ORDER BY排序的列创建索引。 CREATE I…...
数字校园:信息时代的教育新形态
现如今,我们生活在一个信息爆炸的时代,每一天都有海量的信息产生。而在教育领域,也正在经历一场数字化的变革,这就是所谓的“数字校园”。数字校园可不是简单的把课本搬到电脑上那么简单,它其实是一个综合性的平台&…...
数字产业化和产业数字化到底是什么?
“数字产业化”和“产业数字化”在很多官方文件和领导人讲话中都是成对出现的,这两个术语看起来非常相似,但它们作为数字经济的两个重要组成部分,既有联系又有区别。 在谈数字产业化和产业数字化之前,我这里需要先给大家介绍一个概…...
每日十题八股-2024年12月14日
1.类加载器有哪些? 2.双亲委派模型的作用 3.讲一下类加载过程? 4.讲一下类的加载和双亲委派原则 5.什么是Java里的垃圾回收?如何触发垃圾回收? 6.判断垃圾的方法有哪些? 7.垃圾回收算法是什么,是为了解决了…...
大模型呼入机器人有哪些功能特点?(转)
大模型呼入机器人有哪些功能特点?(转) 原作者:开源呼叫中心FreeIPCC,其Github:https://github.com/lihaiya/freeipcc 大模型呼入机器人,作为现代通信技术与人工智能深度融合的产物,正逐渐成为企业提升服务…...
EasyExcel设置表头上面的那种大标题(前端传递来的大标题)
1、首先得先引用easyExcel的版本依赖,我那 <dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>2.2.6</version> </dependency> 2、然后得弄直接的实体类,&…...
[笔记] 编译LetMeowIn(C++汇编联编程序)过程
文章目录 前言过程下载源码vs2017 创建空项目 引入编译文件改项目依赖属性改汇编编译属性该项目还需注意编译运行 总结 前言 编译LetMeowin 项目发现是个混编项目,c调用汇编的程序,需要配置一下,特此记录一下 过程 下载源码 首先下载源码…...
(三)机器学习 - 标准差/方差
标准差 标准差是统计学中一个非常重要的概念,它用来衡量一组数据的离散程度,即数据点与平均值之间的偏离程度。标准差越大,表示数据点越分散;标准差越小,表示数据点越集中。 标准差的计算步骤如下: 计算数…...
笔记:在WPF中InvalidateMeasure,InvalidateArrange,InvalidateVisual,UpdateLayout主要功能
一、目的:简要介绍在WPF中InvalidateMeasure,InvalidateArrange,InvalidateVisual,UpdateLayout主要功能 在 WPF 中,InvalidateMeasure、InvalidateArrange、InvalidateVisual 和 UpdateLayout 是用于控制布局系统的四…...
[笔记]Qt下使用SendMessage、PostMessage和接收window消息
1.头文件和库引用 首先必须要包含windows.h这个头文件,如果使用一些扩展函数,还需要包含windowsx.h。网上说使用FindWindow要添加头文件winuser.h,不过应该windows.h是自动包含这个依赖的(我没有添加) #include <…...
使用echarts实现3d柱状图+折线图
以下代码有问题请直接问国内直连GPT/Claude HTML 需要注意threeDchart一定要设置宽度高度,不然图不显示,然后echarts版本不要太低,不然也不显示 <div id"threeDchart" class"threeDchart"></div>js set3DBarChart2(dat…...
【经验分享】容器云搭建的知识点
最近忙于备考没关注,有次点进某小黄鱼发现首页出现了我的笔记还被人收费了 虽然我也卖了一些资源,但我以交流、交换为主,笔记都是免费给别人看的 由于当时刚刚接触写的并不成熟,为了避免更多人花没必要的钱,所以决定公…...
JAVA |日常开发中Websocket详解
JAVA |日常开发中Websocket详解 前言一、Websocket 概述1.1 定义1.2 优势 二、Websocket 协议基础2.1 握手过程2.2 消息格式2.3 数据传输方式 三、Java 中使用 Websocket3.1 Java WebSocket API(JSR - 356)3.2 第三方库(如 Tyrus&…...
30.攻防世界unserialize3
进入场景 解读一下 这个类 xctf 中有一个公共属性 $flag ,其值为 111 ,并且定义了一个 __wakeup 魔术方法,当对象被反序列化时会自动调用该方法,该方法会输出 bad requests 并终止程序的执行。 ?code提示了参数 <?php clas…...
IS-IS协议
IS-IS协议介绍 IS-IS(Intermediate System to Intermediate System)协议是一种链路状态的内部网关协议(IGP),用于在同一个自治系统(Autonomous System, AS)内部的路由器之间交换路由信息。IS-I…...
接口文档之swagger、kinife4j的基本使用
1.swagger3的使用: 1.1pom.xml中加入依赖: <dependency><groupId>io.springfox</groupId><artifactId>springfox-boot-starter</artifactId><version>3.0.0</version> </dependency> <!--swagger的…...
Oracle plsqldev1106 安装及TNS配置
Oracle plsqldev1106 安装及TNS配置 下载好安装包,直接双击安装 点击 I Agree 默认是C盘的,我改了D盘,根据自己实际情况修改 这里用默认的for current user 也可以,我选了for all user 点Finish,等待安装完成即可 …...
[数据结构]无向图的深度优先非递归遍历
采用邻接表存储实现无向图的深度优先非递归遍历。 输入格式: 先输入两个整数(m,n)(分别表示待创建的图顶点数和边数),之后是m个顶点的信息,再之后是n 条边。 输出格式: 对每一组输入,在一行…...
Android后端签到flask迁移到rust的axum的过程-签到性能和便携
本次变更了以下内容: 为了使用之前ip2sta的ip到端点名的python,dic变量,将其存入redis hashset.使用地址/api/ip2dic 手动执行之.并且定义在/station/init,这个每天初始化redis的路径下.在rust axum使用redis 连接池在test中 ip2dic,IP转端点名,转本日此端网址.在前端的人名下…...
Android13开机向导
文章目录 前言需求-场景第三方资料说明需求思路按照平台 思路 从配置上去 feature换个思路,去feature。SimMissingActivity 判断跳过逻辑SetupWizardUtils 判断SIM 、 hasSystemFeature FEATURE_TELEPHONYPackageManager.FEATURE_TELEPHONYApplicationPackageManage…...
泷羽sec学习打卡-brupsuite6暴力破解与验证码识别绕过
声明 学习视频来自B站UP主 泷羽sec,如涉及侵权马上删除文章 笔记的只是方便各位师傅学习知识,以下网站只涉及学习内容,其他的都 与本人无关,切莫逾越法律红线,否则后果自负 关于brupsuite的那些事儿-验证码绕过以及字典爆破 如何利用brpsuite进行验证码绕过呢?1、下…...
vue季度选择器(antd2.0 版本无此控件,单独写一个)
vue季度选择器 效果显示 效果显示 <template><div><a-popoverplacement"bottom"overlayClassName"season-picker"trigger"click"v-model"showSeason"><template #content><div class"season-picker-b…...
Microsemi Libero使用技巧11——CoreUARTAPB RX管脚分配时不显示
调用串口IP核CoreUARTAPB,并例化到顶层设计,发现UART_RX管脚在进行管脚分配时没有显示出来,最后发现是CoreAPB3总线IP核配置不对导致,改为如下配置后正常。...
回归预测 | MATLAB实现SVM-Adaboost集成学习结合支持向量机多输入单输出回归预测
回归预测 | MATLAB实现SVM-Adaboost集成学习结合支持向量机多输入单输出回归预测 目录 回归预测 | MATLAB实现SVM-Adaboost集成学习结合支持向量机多输入单输出回归预测基本介绍程序设计基本介绍 SVM-Adaboost集成学习是一种将支持向量机(SVM)与AdaBoost算法相结合的集成学习…...
Keil-MDK开发环境编译后axf自动转换bin格式文件
编译选项添加如下,调用fromelf工具自动完成转换: fromelf --bin -o "$LL.bin" "#L"...
计算机组成原理(五):程序装载
在计算机组成原理中,程序装载(Program Loading)是指将程序从外存(如磁盘)加载到内存中,并为其运行做好准备的过程。程序装载是实现程序从静态存储状态到动态运行状态的关键环节,涉及地址映射、内…...
开发EDA工具常用的三方开源
EDA软件是制造芯片重要工具,是现在举国的大难题。这个工具难在哪里,几句话说不清,但它确实也有一些非常通用的功能,这些功能依赖一些成熟的轮子,这些轮子,就是三方的开源项目,下面列举一些常用的…...
微信小程序中 crypto-js 加解密全攻略
一、引言 在微信小程序开发中,数据的安全至关重要。加解密技术在保护用户数据和应用程序的安全性方面起着关键作用。小程序在与服务器进行数据交互时,面临着数据泄露、篡改等安全风险。为了确保用户信息的安全,选择合适的加解密算法变得尤为…...
Vue2 - 最新实现将多个文件批量导出为ZIP压缩包格式并下载功能,纯前端下载多个文件打包输出成zip格式,vue2将文件批量下载打包成ZIP下载保存本地(后端二进制文件流/base64图片/url
前言 Vue3 版本,请访问 这篇文章。 在 vue2 | nuxt2 项目开发中,详解实现把多个文件组合成一个ZIP压缩包格式下载到用户本地,将文件批量下载打包成zip格式并自定义压缩包命名名称,vue批量下载文件并导出为压缩包的功能,如何将后端返回的二进制文件流打包成zip格式,支持任…...