Tips:
抛砖引玉,本文记录ESP32学习过程中遇到的收获。如有不对的地方,欢迎指正。
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则是存在连接,并使用这个套接字接收数据,以非阻塞方式。如果没有设备连接,则不会进行任何操作。
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