【保姆级IDF】ESP32使用WIFI作为AP模式TCP通信:连接客户端+一对多通信

#1024程序员节 | 征文#

Tips:

抛砖引玉,本文记录ESP32学习过程中遇到的收获。如有不对的地方,欢迎指正。

目录

1.前言

​编辑

2.实现功能

3.代码思路

4.具体代码说明

4.1 WIFI热点的建立

4.2 创建套接字及相关设置

4.3 一对多通信任务

4.4 主函数

4.5 相关函数介绍

        1.setsockopt函数在之前的文章已经介绍过了,参看:【保姆级IDF】ESP32使用WIFI作为STA模式与其他设备进行TCP通信

        2. bind函数

        3. listen函数

4.6 完整工程代码

5. 成果展示

6.总结


1.前言

        关于ESP32的WIFI这部分基础知识,在网上可以找到许多,包括TCP协议、套接字等等,博主之前的文章也有介绍,在此本文不再赘述,直接讲清楚标题功能如何实现,并说明代码思路以及详细解释代码具体内容。如不清楚基础知识可以查看我前面的文章,链接放下面了。

【保姆级IDF】ESP32使用WIFI作为STA模式实现:WIFI扫描串口输出+串口输入指定WIFI名称和密码+连接WIFIhttps://blog.csdn.net/zyZYzy9900/article/details/143066482?spm=1001.2014.3001.5501【保姆级IDF】ESP32使用WIFI作为STA模式与其他设备进行TCP通信https://blog.csdn.net/zyZYzy9900/article/details/143100401?spm=1001.2014.3001.5501

这里博主推荐大家使用vscode中的一个插件:CodeGeex,该插件接入ChatGPT,可以帮我们联想代码,并显示预览,按下Tab即可确认输入,非常好用!

博主使用合宙的ESP32C3开发板:

2.实现功能

        利用ESP32开启WIFI热点并允许其他设备接入同时初始化Socket进行监听,有设备接入后接受套接字的连接,建立连接后可以同时和多个设备通信,并能分辨出是哪个设备发来的消息,可以根据某些特定的消息类型来回复对应的特定消息。ESP32所使用的是开源的轻量化LwIP的TCP/IP协议栈,对于大多数嵌入式设备是足够用的。当有设备退出或中断连接时,也会清理掉相应的连接信息、Socket等信息,防止内存泄漏。

3.代码思路

        1.配置好WIFI驱动程序,并开启WIFI启动热点,确保作为热点处于可连接的状态。

        2.WIFI初始化完成后,创建Socket,并适当设置属性,绑定固定IP和端口号,启动监听由主动连接变为被动连接。

        3.在客户端接入事件的处理函数中调用accept函数去接受连接,并为接入的客户端分配信息表用以存储连接信息。

        4.多设备时,用不同的套接字描述符区分不同设备发来的消息,并根据不同消息特征回应不同的数据。

        5.在客户端断开事件的处理函数中清理该设备的连接信息,并释放内存。以备下一次的连接。

4.具体代码说明

4.1 WIFI热点的建立

typedef struct _STA_info{
    uint8_t deveci_mac[6];//连接设备的mac地址
    int sock;             //连接设备的套接字描述符
    struct sockaddr_storage source_addr;//存储连接设备的IP与端口号
}STA_info;
int listen_sock;//监听套接字,用于监听连接请求
STA_info sta_info[10];//连接设备信息表

static void wifi_event_handler(void* arg, esp_event_base_t event_base,
                                    int32_t event_id, void* event_data)
{                               
    if (event_id == WIFI_EVENT_AP_STACONNECTED) {
        wifi_event_ap_staconnected_t* event = (wifi_event_ap_staconnected_t*) event_data;
        ESP_LOGI(TAG, "station "MACSTR" join, AID=%d",
                 MAC2STR(event->mac), event->aid);//设备接入事件:打印连接设备的mac地址与AID
    for (int i = 0; i < 10; i++)//遍历连接设备信息表,查找是否有空闲位置
    {
        if (sta_info[i].sock == 0)//如果查找到空闲位置
        {
            memcpy(sta_info[i].deveci_mac, event->mac, 6);//将接入设备的mac地址存入连接设备信息表
            accept_func(listen_sock,i,(void *)&sta_info);//将连接成功后返回的套接字描述符存入连接设备信息表
            break;
        }
    }        
    } else if (event_id == WIFI_EVENT_AP_STADISCONNECTED) {
        wifi_event_ap_stadisconnected_t* event = (wifi_event_ap_stadisconnected_t*) event_data;
        ESP_LOGI(TAG, "station "MACSTR" leave, AID=%d",
                 MAC2STR(event->mac), event->aid);//设备退出事件:打印连接设备的mac地址与AID
        for(int j = 0; j < 10; j++)//遍历连接设备信息表,查找退出设备的mac地址
        {
            if(strcmp((char*)sta_info[j].deveci_mac, (char*)event->mac) == 0)//如果找到则:
            {
                shutdown(sta_info[j].sock, 0);//停止连接设备的套接字描述符的工作
                close(sta_info[j].sock);//关闭连接设备的套接字描述符
                sta_info[j].sock = 0;//将连接设备信息表中的套接字描述符置为0
                memset(&sta_info[j].deveci_mac, 0, sizeof(sta_info[j].deveci_mac));//将连接设备信息表中的mac地址置为0
                break;
            }
        }
    }
}

void wifi_init_softap(void)
{
    ESP_ERROR_CHECK(esp_netif_init());//初始化网络接口
    ESP_ERROR_CHECK(esp_event_loop_create_default());//创建默认的事件循环
    esp_netif_create_default_wifi_ap();//创建默认的AP网络接口

    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();//初始化wifi配置
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));//初始化wifi

    ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
                                                        ESP_EVENT_ANY_ID,
                                                        &wifi_event_handler,
                                                        NULL,
                                                        NULL));//注册wifi事件处理程序

    wifi_config_t wifi_config = {
        .ap = {
            .ssid = EXAMPLE_ESP_WIFI_SSID,              //热点名称
            .ssid_len = strlen(EXAMPLE_ESP_WIFI_SSID),  //热点名称长度
            .channel = EXAMPLE_ESP_WIFI_CHANNEL,        //热点信道
            .password = EXAMPLE_ESP_WIFI_PASS,          //热点密码
            .max_connection = EXAMPLE_MAX_STA_CONN,     //最大连接数
#ifdef CONFIG_ESP_WIFI_SOFTAP_SAE_SUPPORT
            .authmode = WIFI_AUTH_WPA3_PSK,             //WPA3-PSK
            .sae_pwe_h2e = WPA3_SAE_PWE_BOTH,           //H2E
#else /* CONFIG_ESP_WIFI_SOFTAP_SAE_SUPPORT */
            .authmode = WIFI_AUTH_WPA2_PSK,
#endif
            .pmf_cfg = {
                    .required = true,
            },
        },
    };
    if (strlen(EXAMPLE_ESP_WIFI_PASS) == 0) {//如果密码为空,则设置为无密码
        wifi_config.ap.authmode = WIFI_AUTH_OPEN;//无密码
    }

    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));//设置wifi模式为AP模式
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config));//设置wifi配置
    ESP_ERROR_CHECK(esp_wifi_start());//启动wifi

    ESP_LOGI(TAG, "wifi_init_softap finished. SSID:%s password:%s channel:%d",
             EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS, EXAMPLE_ESP_WIFI_CHANNEL);
}

        wifi_init_softap函数用来创建一个WIFI热点,wifi_event_handler函数是WIFI事件函数,用来处理设备的接入接出事件。WIFI热点的配置部分代码已经老生常谈了,直接copy示例代码过来就行了,注意名称和密码的设置就好。

        重点是在WIFI事件函数中,当设备接入事件发生时,我们查找设备信息表,找到空闲位置则将相关信息填入这个位置,mac地址会被填入,将sta_info结构体传入accept_func函数会获得对应的套接字描述符和设备信息。作用就是每当设备接入都会执行accept函数(在accept_func函数中调用)去等待客户端的连接,accept函数调用后会相当于阻塞一样等待接受连接。

        当设备接出事件发生时,我们同样查找设备信息表,只不过这次查询的是接出的设备的mac地址,然后关闭套接字描述符,并将内存释放,他所在sta_info结构体中的位置也会被重置变为空闲位置,防止内存泄漏。最后在主函数中调用wifi_init_softap函数即可创建好wifi热点。

4.2 创建套接字及相关设置

#define PORT                        3333
#define KEEPALIVE_IDLE              5
#define KEEPALIVE_INTERVAL          5
#define KEEPALIVE_COUNT             3

#define INADDR_ANY          ((u32_t)0xc0a80401UL)//192.168.4.1

static int tcp_server()
{
    int addr_family = AF_INET;//IPv4协议
    int ip_protocol = 0; //TCP协议
    struct sockaddr_storage dest_addr;//目标地址
#if 1
    if (addr_family == AF_INET) {
        struct sockaddr_in *dest_addr_ip4 = (struct sockaddr_in *)&dest_addr;//IPv4地址
        dest_addr_ip4->sin_addr.s_addr = htonl(INADDR_ANY);//设置本机IP
        dest_addr_ip4->sin_family = AF_INET;//IPv4协议
        dest_addr_ip4->sin_port = htons(PORT);//端口号
        ip_protocol = IPPROTO_IP;//IP协议:0
    }
#else CONFIG_EXAMPLE_IPV6
    if (addr_family == AF_INET6) {
        struct sockaddr_in6 *dest_addr_ip6 = (struct sockaddr_in6 *)&dest_addr;
        bzero(&dest_addr_ip6->sin6_addr.un, sizeof(dest_addr_ip6->sin6_addr.un));
        dest_addr_ip6->sin6_family = AF_INET6;
        dest_addr_ip6->sin6_port = htons(PORT);
        ip_protocol = IPPROTO_IPV6;
    }
#endif

    listen_sock = socket(AF_INET, SOCK_STREAM, ip_protocol);//创建socket
    if (listen_sock < 0) {//创建失败
        ESP_LOGE(TAG, "Unable to create socket: errno %d", errno);//打印错误信息
        vTaskDelete(NULL);//删除任务
        return -1;
    }
    int opt = 1;
    setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));//设置socket选项为:可重用
#if defined(CONFIG_EXAMPLE_IPV4) && defined(CONFIG_EXAMPLE_IPV6)
    // Note that by default IPV6 binds to both protocols, it is must be disabled
    // if both protocols used at the same time (used in CI)
    setsockopt(listen_sock, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt));
#endif

    ESP_LOGI(TAG, "Socket created");

    int err = bind(listen_sock, (struct sockaddr *)&dest_addr, sizeof(dest_addr));//绑定socket到目标地址
    if (err != 0) {//绑定失败
        ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno);//打印错误信息
        ESP_LOGE(TAG, "IPPROTO: %d", addr_family);//打印IP协议
        goto CLEAN_UP;//跳转到CLEAN_UP标签处
    }
    ESP_LOGI(TAG, "Socket bound, port %d", PORT);

    err = listen(listen_sock, 3);//监听socket
    if (err != 0) {//监听失败
        ESP_LOGE(TAG, "Error occurred during listen: errno %d", errno);//打印错误信息
        goto CLEAN_UP;//跳转到CLEAN_UP标签处
    }
    ESP_LOGI(TAG, "Socket listening");
    return 0;
CLEAN_UP:
    close(listen_sock);//关闭socket
    return -1;
}

        这里我们配置好套接字的参数,设置本机IP为:192.168.4.1,端口号为3333,协议为IPv4。然后创建套接字,判断创建失败作错误处理,然后设置套接字选项为可重用,就是允许其他多个套接字连接到自己。然后将这个套接字绑定到目标地址,同样的判断创建失败作错误处理,然后在开启监听,这让套接字由主动变为被动。一切做完后返回0。只有在设备接入接出的事件内才会去接受连接。setsockopt函数前面的文章有详细的介绍,如果不清楚可翻看之前的tcp通信文章。bind函数和listen函数下文会介绍。

4.3 一对多通信任务

        该任务会循环查询是否有套接字连接,方法就是去查找设备信息表中的sock成员,不为0则是存在连接,并使用这个套接字接收数据,以非阻塞方式。如果没有设备连接,则不会进行任何操作。注意WIFi这种无线通信方式传输的数据是小端模式!而单片机内存是以大端模式存放数据,如果让内存以1字节对齐,所以接收到WIFI数据后,会发现存储在数组中的数据的顺序是倒序的,读者需要注意这点,可以自行转换。

static void Task_do_retransmit()
{
    int len;//接收到的数据长度
    char rx_buffer[128];//接收缓冲区
    while(1){
        vTaskDelay(200 / portTICK_PERIOD_MS);//延时200ms
        for(int k = 0; k < 10; k++){//遍历所有连接的设备
            if(sta_info[k].sock != 0){//如果设备已连接
                len = recv(sta_info[k].sock, rx_buffer, sizeof(rx_buffer) - 1, 0x08);//接收数据
                if(len > 0){//如果接收到了数据
                    switch(rx_buffer[0])//根据数据包的第一个字节判断设备类型
                    {
                        case 0x01:
                            send(sta_info[k].sock, "Hi, ACER,I'm ESP32", 19, 0);//发送对应数据
                            printf("message from ACER: [%s]", rx_buffer);//串口调试打印
                            break;
                        case 0x02:
                            send(sta_info[k].sock, "Hi, IPhone,I'm ESP32", 21, 0);//发送对应数据
                            printf("message from IPhone: [%s]", rx_buffer);//串口调试打印
                            break;
                        case 0x03:
                            send(sta_info[k].sock, "Hi, OnePlus,I'm ESP32", 22, 0);//发送对应数据
                            printf("message from OnePlus: [%s]", rx_buffer);//串口调试打印
                            break;
                        default:
                        printf("unvaild message!\n");//如果没有正确格式的数据,则打印错误信息
                    }
                }
            }
        }
    }
}

        轮询需要一定间隔,如果没有间隔,博主实测会报内存错误。先查找有无设备连接,然后采用非阻塞方式接收,若没接收到,则继续查询,若接收到则进行数据内容判断,并能识别到数据来自哪一个设备,根据数据内容对相应发来消息的设备作出不同回应。

4.4 接受连接函数

void accept_func(int sockfd,int offset,STA_info *sta_info)
{
    int keepAlive = 1;
    int keepIdle = KEEPALIVE_IDLE;
    int keepInterval = KEEPALIVE_INTERVAL;
    int keepCount = KEEPALIVE_COUNT;
    socklen_t addr_len = sizeof(sta_info[offset].source_addr);
    sta_info[offset].sock = accept(sockfd, (struct sockaddr *)&sta_info[offset].source_addr, &addr_len);//接受socket连接
    if (sta_info[offset].sock < 0) {
        ESP_LOGE(TAG, "Unable to accept connection: errno %d", errno);
    }
        // Set tcp keepalive option
        setsockopt(sta_info[offset].sock, SOL_SOCKET, SO_KEEPALIVE, &keepAlive, sizeof(int));//设置socket选项为:保持连接
        setsockopt(sta_info[offset].sock, IPPROTO_TCP, TCP_KEEPIDLE, &keepIdle, sizeof(int));//设置socket选项为:保持空闲时间
        setsockopt(sta_info[offset].sock, IPPROTO_TCP, TCP_KEEPINTVL, &keepInterval, sizeof(int));//设置socket选项为:保持间隔时间
        setsockopt(sta_info[offset].sock, IPPROTO_TCP, TCP_KEEPCNT, &keepCount, sizeof(int));//设置socket选项为:保持计数
}

        准备接受套接字连接,连接成功后做相应套接字设置。 

4.5 主函数

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);

    for(int i = 0; i < 10; i++) {//初始化sta_info数组
        sta_info[i].sock = 0;
        memset(&sta_info[i].deveci_mac, 0, sizeof(sta_info[i].deveci_mac));
    }
    printf("init sta_info[] = 0\n");
    ESP_LOGI(TAG, "ESP_WIFI_MODE_AP");
    wifi_init_softap();//初始化wifi
    int err = tcp_server();//创建TCP服务器
    if(err != 0) {
        ESP_LOGE(TAG, "TCP server failed");
        return;
    }
    xTaskCreate(Task_do_retransmit, "Task_do_retransmit", 4096 * 5, NULL, 5, NULL);//创建任务
}

        代码中已有注释,这里不多解释了。 

4.5 相关函数介绍

        1.setsockopt函数在之前的文章已经介绍过了,参看:【保姆级IDF】ESP32使用WIFI作为STA模式与其他设备进行TCP通信

        2. bind函数

头文件:#include<sys/socket.h>
函数原型:int bind(int s,const struct sockaddr *name, socklen_t namelen)
返回: 0 ──成功, -1 ──失败

        参数s:指定地址与哪个套接字绑定,这是一个由之前的socket函数调用返回的套接字。调用bind的函数之后,该套接字与一个相应的地址关联,发送到这个地址的数据可以通过这个套接字来读取与使用。

        参数name:指定地址。这是一个地址结构,并且是一个已经经过填写的有效的地址结构。调用bind之后这个地址与参数s指定的套接字关联,从而实现上面所说的功能。

        参数namelen:正如大多数Socket接口一样,内核不关心地址结构,当它复制或传递地址给驱动的时候,它依据这个值来确定需要复制多少数据。这已经成为socket接口中最常见的参数之一了。

        bind函数并不是总是需要调用的,只有用户进程想与一个具体的地址或端口相关联的时候才需要调用这个函数。如果用户进程没有这个需要,那么程序可 以依赖内核的自动的选址机制来完成自动地址选择,而不需要调用bind的函数,同时也避免不必要的复杂度。在一般情况下,对于服务器进程问题需要调用 bind函数,对于客户进程则不需要调用bind函数。

        3. listen函数

头文件:#include<sys/socket.h>
函数原型:int listen(int s,int backlog)
返回: 0 ──成功, -1 ──失败

         参数s:想要用于监听的套接字名称,在使用socket函数创建套接字之后,返回的文件描述符,也就是套接字,处于一个主动连接的状态,需要使用connect去将他和其他设备连接,但此时我们用作服务器,自然是希望等待其他设备来连接我们,所以listen就会将这个传入的套接字由主动连接变为被动连接。

        参数backlog:代表在处理连接请求时,若存在其他多个连接请求,内核如何处理的一个过程。因为在连接中存在半连接的状态,所以当内核处理的连接请求过多时,无法快速及时的处理好连接请求,所以内核会设置维护一个队列来追踪这些请求,但是这样的队列,内核不会它无限膨胀,必须有一个阈值上限,而这个上限就是这个参数的值。通常来说,小于30。

4.6 完整工程代码

/*  WiFi softAP 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.
*/
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_mac.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"

#include "lwip/err.h"
#include "lwip/sys.h"

#include <sys/param.h>
#include "esp_system.h"
#include "esp_netif.h"
//#include "protocol_examples_common.h"

#include "lwip/sockets.h"
#include <lwip/netdb.h>

/* The examples use WiFi configuration that you can set via project configuration menu.

   If you'd rather not, just change the below entries to strings with
   the config you want - ie #define EXAMPLE_WIFI_SSID "mywifissid"
*/
#define EXAMPLE_ESP_WIFI_SSID      "此处替换成热点名称"
#define EXAMPLE_ESP_WIFI_PASS      "此处替换成热点密码"
#define EXAMPLE_ESP_WIFI_CHANNEL   CONFIG_ESP_WIFI_CHANNEL
#define EXAMPLE_MAX_STA_CONN       CONFIG_ESP_MAX_STA_CONN

#define PORT                        3333
#define KEEPALIVE_IDLE              5
#define KEEPALIVE_INTERVAL          5
#define KEEPALIVE_COUNT             3

static const char *TAG = "wifi softAP";
typedef struct _STA_info{
    uint8_t deveci_mac[6];//连接设备的mac地址
    int sock;             //连接设备的套接字描述符
    struct sockaddr_storage source_addr;//存储连接设备的IP与端口号
}STA_info;

static int tcp_server();
void accept_func(int sockfd,int offset,STA_info *sta_info);

uint8_t Ready_listen = 0;
int listen_sock;//监听套接字,用于监听连接请求
STA_info sta_info[10];//连接设备信息表

static void wifi_event_handler(void* arg, esp_event_base_t event_base,
                                    int32_t event_id, void* event_data)
{                               
    if (event_id == WIFI_EVENT_AP_STACONNECTED) {
        wifi_event_ap_staconnected_t* event = (wifi_event_ap_staconnected_t*) event_data;
        ESP_LOGI(TAG, "station "MACSTR" join, AID=%d",
                 MAC2STR(event->mac), event->aid);//设备接入事件:打印连接设备的mac地址与AID
    for (int i = 0; i < 10; i++)//遍历连接设备信息表,查找是否有空闲位置
    {
        if (sta_info[i].sock == 0)//如果查找到空闲位置
        {
            memcpy(sta_info[i].deveci_mac, event->mac, 6);//将接入设备的mac地址存入连接设备信息表
            accept_func(listen_sock,i,(void *)&sta_info);//将连接成功后返回的套接字描述符存入连接设备信息表
            break;
        }
    }        
    } else if (event_id == WIFI_EVENT_AP_STADISCONNECTED) {
        wifi_event_ap_stadisconnected_t* event = (wifi_event_ap_stadisconnected_t*) event_data;
        ESP_LOGI(TAG, "station "MACSTR" leave, AID=%d",
                 MAC2STR(event->mac), event->aid);//设备退出事件:打印连接设备的mac地址与AID
        for(int j = 0; j < 10; j++)
        {
            if(strcmp((char*)sta_info[j].deveci_mac, (char*)event->mac) == 0)
            {
                shutdown(sta_info[j].sock, 0);//停止连接设备的套接字描述符的工作
                close(sta_info[j].sock);//关闭连接设备的套接字描述符
                sta_info[j].sock = 0;//将连接设备信息表中的套接字描述符置为0
                memset(&sta_info[j].deveci_mac, 0, sizeof(sta_info[j].deveci_mac));//将连接设备信息表中的mac地址置为0
                break;
            }
        }
    }
}

void wifi_init_softap(void)
{
    ESP_ERROR_CHECK(esp_netif_init());//初始化网络接口
    ESP_ERROR_CHECK(esp_event_loop_create_default());//创建默认的事件循环
    esp_netif_create_default_wifi_ap();//创建默认的AP网络接口

    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();//初始化wifi配置
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));//初始化wifi

    ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
                                                        ESP_EVENT_ANY_ID,
                                                        &wifi_event_handler,
                                                        NULL,
                                                        NULL));//注册wifi事件处理程序

    wifi_config_t wifi_config = {
        .ap = {
            .ssid = EXAMPLE_ESP_WIFI_SSID,              //热点名称
            .ssid_len = strlen(EXAMPLE_ESP_WIFI_SSID),  //热点名称长度
            .channel = EXAMPLE_ESP_WIFI_CHANNEL,        //热点信道
            .password = EXAMPLE_ESP_WIFI_PASS,          //热点密码
            .max_connection = EXAMPLE_MAX_STA_CONN,     //最大连接数
#ifdef CONFIG_ESP_WIFI_SOFTAP_SAE_SUPPORT
            .authmode = WIFI_AUTH_WPA3_PSK,             //WPA3-PSK
            .sae_pwe_h2e = WPA3_SAE_PWE_BOTH,           //H2E
#else /* CONFIG_ESP_WIFI_SOFTAP_SAE_SUPPORT */
            .authmode = WIFI_AUTH_WPA2_PSK,
#endif
            .pmf_cfg = {
                    .required = true,
            },
        },
    };
    if (strlen(EXAMPLE_ESP_WIFI_PASS) == 0) {//如果密码为空,则设置为无密码
        wifi_config.ap.authmode = WIFI_AUTH_OPEN;//无密码
    }

    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));//设置wifi模式为AP模式
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config));//设置wifi配置
    ESP_ERROR_CHECK(esp_wifi_start());//启动wifi

    ESP_LOGI(TAG, "wifi_init_softap finished. SSID:%s password:%s channel:%d",
             EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS, EXAMPLE_ESP_WIFI_CHANNEL);
}

static void Task_do_retransmit()
{
    int len;//接收到的数据长度
    char rx_buffer[128];//接收缓冲区
    while(1){
        vTaskDelay(200 / portTICK_PERIOD_MS);//延时200ms
        for(int k = 0; k < 10; k++){//遍历所有连接的设备
            if(sta_info[k].sock != 0){//如果设备已连接
                len = recv(sta_info[k].sock, rx_buffer, sizeof(rx_buffer) - 1, 0x08);//接收数据
                if(len > 0){//如果接收到了数据
                    switch(rx_buffer[0])//根据数据包的第一个字节判断设备类型
                    {
                        case 0x01:
                            send(sta_info[k].sock, "Hi, ACER,I'm ESP32", 19, 0);//发送对应数据
                            printf("message from ACER: [%s]", rx_buffer);//串口调试打印
                            break;
                        case 0x02:
                            send(sta_info[k].sock, "Hi, IPhone,I'm ESP32", 21, 0);//发送对应数据
                            printf("message from IPhone: [%s]", rx_buffer);//串口调试打印
                            break;
                        case 0x03:
                            send(sta_info[k].sock, "Hi, OnePlus,I'm ESP32", 22, 0);//发送对应数据
                            printf("message from OnePlus: [%s]", rx_buffer);//串口调试打印
                            break;
                        default:
                        printf("unvaild message!\n");//如果没有正确格式的数据,则打印错误信息
                    }
                }
            }
        }
    }
}

static int tcp_server()
{
    int addr_family = AF_INET;//IPv4协议
    int ip_protocol = 0; //TCP协议
    struct sockaddr_storage dest_addr;//目标地址
#if 1
    if (addr_family == AF_INET) {
        struct sockaddr_in *dest_addr_ip4 = (struct sockaddr_in *)&dest_addr;//IPv4地址
        dest_addr_ip4->sin_addr.s_addr = htonl(INADDR_ANY);//设置本机IP
        dest_addr_ip4->sin_family = AF_INET;//IPv4协议
        dest_addr_ip4->sin_port = htons(PORT);//端口号
        ip_protocol = IPPROTO_IP;//IP协议:0
    }
#else CONFIG_EXAMPLE_IPV6
    if (addr_family == AF_INET6) {
        struct sockaddr_in6 *dest_addr_ip6 = (struct sockaddr_in6 *)&dest_addr;
        bzero(&dest_addr_ip6->sin6_addr.un, sizeof(dest_addr_ip6->sin6_addr.un));
        dest_addr_ip6->sin6_family = AF_INET6;
        dest_addr_ip6->sin6_port = htons(PORT);
        ip_protocol = IPPROTO_IPV6;
    }
#endif

    listen_sock = socket(AF_INET, SOCK_STREAM, ip_protocol);//创建socket
    if (listen_sock < 0) {//创建失败
        ESP_LOGE(TAG, "Unable to create socket: errno %d", errno);//打印错误信息
        vTaskDelete(NULL);//删除任务
        return -1;
    }
    int opt = 1;
    setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));//设置socket选项为:可重用
#if defined(CONFIG_EXAMPLE_IPV4) && defined(CONFIG_EXAMPLE_IPV6)
    // Note that by default IPV6 binds to both protocols, it is must be disabled
    // if both protocols used at the same time (used in CI)
    setsockopt(listen_sock, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt));
#endif

    ESP_LOGI(TAG, "Socket created");

    int err = bind(listen_sock, (struct sockaddr *)&dest_addr, sizeof(dest_addr));//绑定socket到目标地址
    if (err != 0) {//绑定失败
        ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno);//打印错误信息
        ESP_LOGE(TAG, "IPPROTO: %d", addr_family);//打印IP协议
        goto CLEAN_UP;//跳转到CLEAN_UP标签处
    }
    ESP_LOGI(TAG, "Socket bound, port %d", PORT);

    err = listen(listen_sock, 3);//监听socket
    if (err != 0) {//监听失败
        ESP_LOGE(TAG, "Error occurred during listen: errno %d", errno);//打印错误信息
        goto CLEAN_UP;//跳转到CLEAN_UP标签处
    }
    ESP_LOGI(TAG, "Socket listening");
    return 0;
CLEAN_UP:
    close(listen_sock);//关闭socket
    return -1;
}

void accept_func(int sockfd,int offset,STA_info *sta_info)
{
    int keepAlive = 1;
    int keepIdle = KEEPALIVE_IDLE;
    int keepInterval = KEEPALIVE_INTERVAL;
    int keepCount = KEEPALIVE_COUNT;
    socklen_t addr_len = sizeof(sta_info[offset].source_addr);
    sta_info[offset].sock = accept(sockfd, (struct sockaddr *)&sta_info[offset].source_addr, &addr_len);
    if (sta_info[offset].sock < 0) {
        ESP_LOGE(TAG, "Unable to accept connection: errno %d", errno);
    }
        // Set tcp keepalive option
        setsockopt(sta_info[offset].sock, SOL_SOCKET, SO_KEEPALIVE, &keepAlive, sizeof(int));
        setsockopt(sta_info[offset].sock, IPPROTO_TCP, TCP_KEEPIDLE, &keepIdle, sizeof(int));
        setsockopt(sta_info[offset].sock, IPPROTO_TCP, TCP_KEEPINTVL, &keepInterval, sizeof(int));
        setsockopt(sta_info[offset].sock, IPPROTO_TCP, TCP_KEEPCNT, &keepCount, sizeof(int));
}

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);

    for(int i = 0; i < 10; i++) {//初始化sta_info数组
        sta_info[i].sock = 0;
        memset(&sta_info[i].deveci_mac, 0, sizeof(sta_info[i].deveci_mac));
    }
    printf("init sta_info[] = 0\n");
    ESP_LOGI(TAG, "ESP_WIFI_MODE_AP");
    wifi_init_softap();//初始化wifi
    int err = tcp_server();//创建TCP服务器
    if(err != 0) {
        ESP_LOGE(TAG, "TCP server failed");
        return;
    }
    xTaskCreate(Task_do_retransmit, "Task_do_retransmit", 4096 * 5, NULL, 5, NULL);//创建任务
}

5. 成果展示

        以下三张图为博主的三台设备:一台安卓手机,一台IOS手机,一台Windows电脑连接上ESP32的WIFI的截图

        注意!!!

        每台设备连接上之后,请务必立即使用网络调试助手进行套接字连接,建立TCP通信!

        否则若是先将三台设备连接,再去一个个连接套接字,这可能会导致只有一个能连上,因为程序是在设备接入时调用accept函数去接受连接,若是三台先接入WIFI可能导致有两台设备的套接字连接请求被搁置,读者可以自行测试,博主这边测试结果是这样的。

        下面是三台设备都建立TCP连接后,发送不同的数据,ESP32回复的对应消息的截图。

        可以看到,ESP32根据不同的指令对设备进行识别,发送对应的数据,且数据发送不会“串台”,即回复电脑的数据不会回复到手机上,回复到手机上的数据亦不会回复到电脑上。

        读者可以发挥想象,自己定义一套指令,让ESP32做出不同反应。

6.总结

        以上就是利用ESP32的WIFI作为服务器对其他设备进行一对多通信的全部内容了,主要还是靠发挥想象力去写代码。通常WIFI外设的使用是基础,Socket的使用更进一步,利用它做其他更多功能是进阶。博主在这只是做简单示例,更多好玩功能读者可以自行开发,得益于ESP32的库函数丰富完善,将底层操作尽数抽象出来做成API供我们使用,这样我们只需关注应用层的开发。而且IDF有大量的示例代码供我们参考学习,非常好用!

        以上代码博主经过实测可用稳定,如有疑问欢迎留言交流。以上观点为个人理解,只想抛砖引玉,如有不对的地方欢迎指正,不喜轻喷。


2024.10.24-21:43

点赞收藏关注,一键三连就是对我最大的肯定!谢谢大家!


2024.10.26-15:28更新

添加4.4 接受连接函数

<think>嗯,用户现在想了解或实现BEV下的目标检测方法或模型,特别是算法的实现方案。首先,我需要回忆一下BEV目标检测的基本概念和相关技术。BEV,也就是鸟瞰图视角,在自动驾驶中非常重要,因为它能提供更直观的周围环境感知。不过,用户可能对具体的技术细节不太清楚,所以需要分步骤讲解。 首先,BEV目标检测的主要挑战是什么?根据之前的引用内容,保持BEV Grid的分辨率和计算负担之间需要权衡。如果分辨率高,计算和带宽压力大;如果降低分辨率,感知精度又会受影响。这可能涉及到如何高效地生成BEV特征,同时平衡计算资源。 接下来,常见的BEV目标检测算法有哪些?比如LSS(Lift, Splat, Shoot)、BEVFormer、PointPillars等。这些方法各有特点,需要简单介绍它们的原理和适用场景。例如,LSS通过预测深度分布将图像提升到3D空间,而BEVFormer利用Transformer进行时空特征融合。 然后是实现方案的关键步骤。数据预处理部分,可能需要多相机图像的输入,标定参数,以及点云数据(如果有的话)。网络结构设计部分,包括图像特征提取、视角转换模块、BEV特征处理,以及检测头。视角转换模块是关键,这里需要详细说明不同的方法,如基于深度估计或Transformer的跨注意力机制。 优化策略也很重要。用户可能关心如何平衡计算效率和精度,比如使用稀疏BEV特征或者轻量化网络结构。此外,工程优化如TensorRT加速和混合精度训练也是实际部署中需要考虑的。 在应用场景方面,需要区分车载平台和边缘设备的不同需求。车载可能有更强的计算能力,可以使用更复杂的模型,而边缘设备则需要轻量化设计。 用户可能还会遇到数据融合的问题,比如如何有效结合相机和激光雷达数据。此外,实时性要求和模型精度之间的权衡也是一个常见的挑战,需要根据具体应用场景调整模型结构。 最后,根据用户提供的引用内容,保持BEV Grid的分辨率与计算负担的矛盾,可能需要推荐采用稀疏BEV特征或者动态分辨率调整的方法,这可以作为优化建议的一部分。 总结下来,回答需要涵盖算法分类、实现步骤、优化策略、应用场景以及常见问题,同时结合引用中的关键点,帮助用户全面理解BEV目标检测的实现方案。</think>### BEV目标检测算法实现方案 BEV(鸟瞰图)目标检测是自动驾驶领域的核心技术,其核心挑战在于**如何高效融合多传感器数据并保持感知精度**,尤其在平衡计算资源与分辨率时需谨慎处理[^1]。以下为分步骤实现方案: --- #### 一、核心算法分类 1. **基于视觉的方法** - **LSS (Lift, Splat, Shoot)**: 通过预测像素深度分布将2D图像"提升"至3D空间,再投影到BEV平面生成特征图,适合多相机融合场景。 $$ z_{bev} = \sum_{i=1}^N \text{Softmax}(d_i) \cdot f_{img}^{(i)} $$ - **BEVFormer**: 使用Transformer的跨注意力机制聚合多视角特征,支持时序融合[^1]。 2. **基于点云的方法** - **PointPillars**: 将点云划分为垂直柱状体(pillar),提取特征后生成伪图像,计算效率较高。 3. **多模态融合方法** 结合相机与激光雷达数据,如**MVFusion**通过跨模态注意力机制对齐特征。 --- #### 二、实现关键步骤 1. **数据预处理** - 多相机图像输入(需标定内外参) - 点云数据体素化(若使用雷达) ```python # 点云体素化示例 from spconv.utils import VoxelGenerator voxel_generator = VoxelGenerator(voxel_size=[0.1,0.1,0.2], point_cloud_range=[-50,-50,-3,50,50,1]) ``` 2. **网络结构设计** ```mermaid graph TD A[多视角图像] --> B[CNN特征提取] B --> C[视角转换模块] C --> D[BEV特征图] D --> E[3D检测头] ``` 3. **视角转换模块实现** - **基于深度估计**(LSS方案): ```python class DepthDistribution(nn.Module): def forward(self, img_feat): depth_prob = self.conv(img_feat) # 预测深度概率分布 return depth_prob.softmax(dim=1) ``` - **基于Transformer**(BEVFormer方案): ```python class SpatialCrossAttention(nn.Module): def forward(self, bev_query, img_feat): attn = torch.einsum('bqc,bchw->bqhw', bev_query, img_feat) return torch.matmul(attn.softmax(dim=-1), img_feat) ``` 4. **优化策略** - **稀疏BEV特征**:仅对可能存在目标的区域进行高分辨率采样 - **轻量化设计**:使用MobileNetV3等轻量骨干网络 - **知识蒸馏**:用大模型指导小模型训练 --- #### 三、典型应用场景 | 场景类型 | 推荐算法 | 分辨率需求 | 硬件要求 | |---------|----------|------------|----------| | 车载计算平台 | BEVFormer | 0.1m/像素 | GPU >= 30TOPS | | 边缘设备 | PointPillars | 0.2m/像素 | FPGA/低功耗GPU | --- #### 四、常见问题与解决方案 1. **视角转换中的特征丢失** - 问题:图像到BEV的投影导致边缘信息丢失 - 方案:在转换模块中添加可变形卷积(Deformable Conv) 2. **多相机特征对齐困难** - 问题:不同相机的光照/遮挡差异 - 方案:引入基于注意力机制的特征校准模块 3. **实时性不足** - 问题:BEV特征图计算延迟高 - 方案:使用预计算深度分布+查找表加速 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_山岚_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值