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

news2024/12/12 10:37:28

ESP-NOW 简介

ESP-NOW

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

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

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


ESPNOW 示例

/* ESPNOW Example

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

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

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

#define ESPNOW_MAXDELAY 512

static const char *TAG = "espnow_example";

static QueueHandle_t s_example_espnow_queue;

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

static void example_espnow_deinit(example_espnow_send_param_t *send_param);

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

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

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

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

    evt.id = EXAMPLE_ESPNOW_SEND_CB;
    memcpy(send_cb->mac_addr, mac_addr, ESP_NOW_ETH_ALEN);
    send_cb->status = status;
    if (xQueueSend(s_example_espnow_queue, &evt, ESPNOW_MAXDELAY) != pdTRUE) {
        ESP_LOGW(TAG, "Send send queue fail");
    }
}

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

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

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

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

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

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

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

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

    return -1;
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

static esp_err_t example_espnow_init(void)
{
    example_espnow_send_param_t *send_param;

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

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

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

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

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

    return ESP_OK;
}

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

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

    example_wifi_init();
    example_espnow_init();
}

代码功能概述

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

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

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

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

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

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


代码翻译及注释

/*
   ESPNOW 示例头文件

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

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

#ifndef ESPNOW_EXAMPLE_H
#define ESPNOW_EXAMPLE_H

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

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

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

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

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

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

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

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

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

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

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

#endif /* ESPNOW_EXAMPLE_H */

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

    return ESP_OK;  // 初始化成功
}



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

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

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

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

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

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

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

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

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


修改说明

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

使用步骤

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

问题


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

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

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

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

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

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

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


1. 广播发送的实现

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

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

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

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

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

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

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

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

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

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

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

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

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

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

5. 关键流程总结

  1. 广播阶段

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

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

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

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

ESPNOW 支持 动态添加对等节点

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

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


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

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

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

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

2. 设备发现的需求

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

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

  • 动态发现与绑定

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

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

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

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

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

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

总结

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


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

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


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

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

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

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

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

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

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

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

  • magic 值比较

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


5. 开始单播通信

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

6. 结果总结

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

观察到的现象

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

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


总结

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


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

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

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


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

思路

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

代码修改

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

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

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


示例代码修改:

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

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

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

配置设备1和设备2

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

发生的情况分析

1. 设备1 先发送广播

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

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

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

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

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

4. 最终结果

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

总结

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

《Java核心技术I》并行数组算法

并行数组算法 Arrays类提供了大量并行化操作。 Arrays.parallelSort方法可以对一个基本类型值或对象的数组排序。 package arrays;import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import …

智创 AI 新视界 -- AI 助力金融风险管理的新策略(16 - 10)

&#x1f496;&#x1f496;&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎你们来到 青云交的博客&#xff01;能与你们在此邂逅&#xff0c;我满心欢喜&#xff0c;深感无比荣幸。在这个瞬息万变的时代&#xff0c;我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…

ASP.NET |日常开发中连接Oracle数据库详解

ASP.NET &#xff5c;日常开发中连接Oracle数据库详解 前言一、安装和配置 Oracle 数据访问组件1.1 安装ODP.NET&#xff08;Oracle Data Provider for.NET&#xff09;&#xff1a;1.2 引用相关程序集&#xff1a; 二、配置连接字符串2.1 连接字符串的基本组成部分&#xff1a…

ChatGPT 4:解锁AI文案、绘画与视频创作新纪元

文章目录 AI文案&#xff1a;激发文字的魅力&#xff0c;重塑营销与传播AI绘画&#xff1a;解锁艺术的无限可能&#xff0c;激发创意灵感AI视频&#xff1a;重塑视频创作流程&#xff0c;提升制作效率GPTs&#xff1a;构建个性化AI应用&#xff0c;赋能各行各业《ChatGPT 4 应用…

使用html 和javascript 实现微信界面功能1

1.功能说明&#xff1a; 搜索模块: 提供一个搜索框&#xff0c;但目前没有实现具体的搜索功能。 好友模块: 在左侧的“好友”部分有一个“查看好友”按钮。点击左侧的“查看好友”按钮时&#xff0c;会在右侧显示所有好友的列表。列表中每个好友可以点击查看详情&#xff0c;包…

常用的注解

RequestMapping 用于映射请求路径 可以添加在类或方法上 请求类型 请求类型包括GET、POST、PUT、DELETE等 默认支持GET和POST两种方式 简写&#xff1a;GetMapping、PostMapping、PutMapping、DeleteMapping PostMapping("/buy") 等价 RequestMapping("/buy&quo…

【操作系统】实验二:观察Linux,使用proc文件系统

实验二 观察Linux&#xff0c;使用proc文件系统 实验目的&#xff1a;学习Linux内核、进程、存储和其他资源的一些重要特征。读/proc/stat文件&#xff0c;计算并显示系统CPU占用率和用户态CPU占用率。&#xff08;编写一个程序使用/proc机制获得以及修改机器的各种资源参数。…

Mysql体系架构剖析——岁月云实战笔记

1 体系架构 理论内容阅读了mysql体系架构剖析&#xff0c;其他的根据岁月云的实战进行记录。 1.1 连接层 mysql最上层为连接服务&#xff0c;引入线程池&#xff0c;允许多台客户端连接&#xff0c;主要工作&#xff1a;连接处理、授权认证、安全防护、管理连接等。 连接处理&a…

算法基础Day7(动态规划)

文章目录 1.题目2.题目解答1.第N个泰波那契数题目及题目解析动态规划算法学习1.状态表示2.状态转移方程3.初始化4.填表顺序5.空间优化 代码提交空间优化 2.三步问题题目及题目解析算法学习代码提交 1.题目 1137. 第 N 个泰波那契数 - 力扣&#xff08;LeetCode&#xff09;面试…

Python基础笔记17--面向对象(其他)

一、面向对象的三大特性 1、封装 1、 将属性和⽅法书写到类的⾥⾯的操作 2、封装可以为属性和⽅法添加私有权限 2、继承 1、⼦类默认继承⽗类的所有属性和⽅法 2、⼦类可以重写⽗类属性和⽅法 3、多态 1、 传⼊不同的对象&#xff0c;产⽣不同的结果 二、多态 多态指⼀类事…

梳理你的思路(从OOP到架构设计)_基本OOP知识03

目录 1、<基类/子类 >结构的接口(卡榫函数) 1&#xff09;卡榫(Hook) 2&#xff09;卡榫函数的Java实现 2、IoC机制与基於 Default 軟硬整合觀點 函数 1&#xff09;卡榫函数实现IoC机制 2&#xff09;默认(Default)行为 1、<基类/子类 >结构的接口(卡榫函数…

Y3编辑器官方文档1:编辑器简介及菜单栏详解(文件、编辑、窗口、细节、调试)

文章目录 一、新建项目二、 编辑器主界面2.1 游戏场景2.2 导航栏/菜单栏2.3 功能栏三、菜单栏详细介绍3.1 文件3.1.1 版本管理3.1.2 项目管理(多关卡)3.1.2.1 多关卡功能说明3.1.2.2 关卡切换与关卡存档3.2 编辑3.2.1 通用设置3.2.2 键位设置3.3 窗口(日志)3.4 细节3.4.1 语言…

微信小程序横屏页面跳转后,自定义navbar样式跑了?

文章目录 问题原因&#xff1a;解决方案&#xff1a; 今天刚遇到的问题&#xff0c;横屏的页面完成操作后跳转页面后&#xff0c;自定义的tabbar样式乱了&#xff0c;跑到最顶了&#xff0c;真机调试后发现navbar跑到手机状态栏了&#xff0c;它正常应该跟右边胶囊一行。 知道问…

十五、K8s计划任务JobCronJob

K8s计划任务CronJob&Job 一、Job可以干什么 Job 控制器用于管理 Pod 对象运行一次性任务,比方说我们对数据库备份,可以直接在 k8s 上启动一个 mysqldump 备份程序,也可以启动一个 pod,这个 pod 专门用来备份用的,备份结束 pod 就可以终止了,不需要重启,而是将 Pod…

【开源】基于SpringBoot框架美容院管理系统(计算机毕业设计)+万字说明文档 T012

系统合集跳转 源码获取链接 一、系统环境 运行环境: 最好是java jdk 1.8&#xff0c;我们在这个平台上运行的。其他版本理论上也可以。 IDE环境&#xff1a; Eclipse,Myeclipse,IDEA或者Spring Tool Suite都可以 tomcat环境&#xff1a; Tomcat 7.x,8.x,9.x版本均可 操作系统…

基于卷积神经网络的垃圾分类系统实现(GUI应用)

1.摘要 本文主要实现了一个卷积神经网络模型进行垃圾图像分类&#xff0c;为了提高垃圾分类模型的准确率&#xff0c;使用使用Batch Normalization层、使用早期停止策略来防止过拟合等方法来优化模型&#xff0c;实验结果显示最终优化后的模型准确率较高90%左右。最终&#xf…

Qt Pro 常用配置

Part1: Summary Qt 开发中 Pro 文件的内容很多&#xff0c;需要不断的去学习和使用&#xff0c;现系统性的整理一下。以备录&#xff1b; 1.创建pro文件 1.1 步骤&#xff1a; Qt Creator--->New Project--->应用程序--->Qt Widgets Application--->名称为&…

软件测试--录制与回放脚本

准备工作 安装phpstudy 配置两个内容 放demo44文件夹 在浏览器输入http://localhost/demo44/index.html&#xff0c;出现如图所示的网站 输入用户名和密码 步骤一&#xff1a;打开Virtual User Generator&#xff0c;点击新建&#xff0c;点击new 步骤二&#xff1a;点击如下…

WEB安全基础知识

WAF全称为Web Application Firewall&#xff08;网页应用防火墙&#xff09;是一种专门设计用来保护web应用免受各种网络攻击的安全防护措施。它位于客户端与服务器之间&#xff0c;监控和过滤HTTP流量&#xff0c;从而拦截恶意请求、识别并防御常见的web攻击。 WAF的主要功能…

android studio kotlin 本地c++工程添加oboe库的方法

1.新建本地c++的kotlin工程hellohao 如图 2.把开源的oboe源文件src、include、CMakelists.txt、debug-utils复制hellohao目录下 3.修改hellohao\app目录下的CMakelists.txt cmake_minimum_required(VERSION 3.22.1) project(hellohao LANGUAGES C CXX)get_filename_component…