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

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

时间:2024-10-24 21:48:54浏览次数:3  
标签:info wifi sta ESP ESP32 WIFI 通信 event

#1024程序员节 | 征文#

Tips:

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

1.前言

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

【保姆级IDF】ESP32使用WIFI作为STA模式实现:WIFI扫描串口输出+串口输入指定WIFI名称和密码+连接WIFIicon-default.png?t=O83Ahttps://blog.csdn.net/zyZYzy9900/article/details/143066482?spm=1001.2014.3001.5501【保姆级IDF】ESP32使用WIFI作为STA模式与其他设备进行TCP通信icon-default.png?t=O83Ahttps://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则是存在连接,并使用这个套接字接收数据,以非阻塞方式。如果没有设备连接,则不会进行任何操作。

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

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

标签:info,wifi,sta,ESP,ESP32,WIFI,通信,event
From: https://blog.csdn.net/zyZYzy9900/article/details/143199817

相关文章

  • 微信小程序中组件通信的性能优化方法有哪些?
    减少不必要的数据传递原理:组件间传递的数据量越小,通信的开销就越小。每次数据传递都涉及到数据的序列化、传输和反序列化等过程,过多或过大的数据传递会增加这些操作的频率和资源消耗。示例:比如在父子组件通信中,如果子组件只需要使用父组件中一个数据字段的部......
  • 如何在微信小程序中使用事件总线进行组件通信?
    创建事件总线(EventBus)模块目的:事件总线是一个独立的模块,用于管理事件的发布和订阅。它提供了一个集中的机制,使得组件之间可以通过发布和订阅事件来进行通信,而不需要依赖组件之间的父子关系或其他复杂的层级结构。代码实现:创建一个名为event-bus.js的文件,......
  • 如何避免在微信小程序中使用事件总线进行组件通信时出现内存泄漏?
    理解内存泄漏问题的产生原因在微信小程序中使用事件总线进行组件通信时,内存泄漏可能是由于组件在销毁后仍然被事件总线持有引用,导致无法被垃圾回收机制正常回收。例如,组件订阅了事件总线的某个事件,当组件被销毁时,如果没有正确地取消订阅,那么事件总线中仍然保存着对该组件......
  • 【读书笔记-《网络是怎样连接的》- 2】Chapter2_1-协议栈通信详细过程
    第二章从协议栈这部分来看网络中的通信如何实现,准备从两部分来进行分解。本篇是第一部分:详细介绍TCP协议栈收发数据的过程。首先来看下面的图。从应用程序到网卡需要经过如下几部分,上面的部分通过委托下面的部分来完成工作。首先是应用程序,通过Socket库来委托协议栈完成工......
  • UVM中Seq-Seqr-Drv之间的通信
    Wediscussedsequece_item,sequence,sequencer,anddriverindependently.Inthissection,wewilldiscusshowtheytalkwitheachotherandprovidesequenceitemsfromsequencetodriverviathesequencer.Beforeyoustartreadingthissection,makes......
  • 笔记本wifi图标消失不见,如何解决
    今天碰见一个有意思的小电脑bug,不知道各位有没有遇见过,就是笔记本上的wifi图标消失不见了,导致无法操作网络,导致电脑无法联网进行操作。具体就是如下,wifi图标不见了。在网上查了半天资料,最后解决了,分享下解决过程。1.首先肯定想到的是强制重启看看,毕竟重启解决百分之98的问题。但......
  • C++调试经验(4):Linux下调试CAN通信的方法
    1.CAN通信介绍         CAN(ControllerAreaNetwork,控制器局域网)是一种现代的通信协议,用于在各种应用中的不同设备之间进行高速通信。它最初由德国的Bosch公司于20世纪80年代开发,用于汽车中的电子系统之间的通信,目前已广泛应用于汽车行业以及其他工业领域。CAN通信......
  • 基于FPGA的64QAM基带通信系统,包含testbench,高斯信道模块,误码率统计模块,可以设置不
    1.算法仿真效果      本课题是在博主以前写的文章《m基于FPGA的64QAM调制解调通信系统verilog实现,包含testbench,不包含载波同步》的升级,升级内容包括信道模块(可以设置SNR),误码率统计,同时修正了数据输入频率问题,从而提升了系统的仿真效率。 vivado2019.2仿真结果如下(完......
  • 【Linux】进程间通信(匿名管道)
     ......
  • KerasCV YOLOv8实现交通信号灯检测
    关注底部公众号,回复暗号:13,免费获取600多个深度学习项目资料,快来加入社群一起学习吧。1.项目简介本项目旨在通过深度学习模型实现交通信号灯的检测,以提高交通管理系统的智能化水平,增强驾驶辅助功能。随着智能交通系统的快速发展,准确地识别交通信号灯对于无人驾驶汽车和高......