Tips:
抛砖引玉,本文记录ESP32学习过程中遇到的收获。如有不对的地方,欢迎指正。
目录
4.1 AP_STA模式下的其他设备接入以及接入其他设备的WIFI配置
1. 前言
本文介绍ESP32使用WIFI时设置为AP+STA模式下的数据通信,并进行两台设备的数据中继服务。WIFI的基础知识以及ESP32用作单一STA模式或AP模式的文章在我的专栏中可以查看到,这里顺便贴上链接:
【保姆级IDF】ESP32使用WIFI作为AP模式TCP通信:连接客户端+一对多通信
【保姆级IDF】ESP32使用WIFI作为STA模式与其他设备进行TCP通信
【保姆级IDF】ESP32使用WIFI作为STA模式实现:WIFI扫描串口输出+串口输入指定WIFI名称和密码+连接WIFI
读者自行查阅即可,不再赘述。
博主使用合宙的ESP32C3开发板:
2. 实现功能
1.可作为WIFI热点被其他设备连接并进行通信,可作为STA站点连接其他设备并进行通信
2.连接到自身的两台设备可以互传数据,自身作为STA站点所连接到的AP热点发来的消息可以广播给所有连接到自身热点的STA站点设备
3.实现数据中继站的功能,将一台设备发送数据发送到另一台设备,两台设备数据可互相转发
3. 代码思路
1.使用ESP32的示例代码:softap_sta为基础开发,该代码提供让设备接入自身热点以及让自身连接其他热点的功能。
2.当自身作为STA模式时,连接上其它WIFI后配置套接字,建立TCP连接。
3.当自身作为AP模式时,创建套接字并进行相关配置,在WIFI事件:设备接入事件中进行accept接受连接。
4.创建一个任务去处理两台设备发来的数据。
4. 代码讲解
4.1 AP_STA模式下的其他设备接入以及接入其他设备的WIFI配置
static void wifi_event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data)
{
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STACONNECTED) {
wifi_event_ap_staconnected_t *event = (wifi_event_ap_staconnected_t *) event_data;
ESP_LOGI(TAG_AP, "Station "MACSTR" joined, AID=%d",
MAC2STR(event->mac), event->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(ap_scok,i,(void *)&sta_info);//将连接成功后返回的套接字描述符存入连接设备信息表
break;
}
}
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STADISCONNECTED) {
wifi_event_ap_stadisconnected_t *event = (wifi_event_ap_stadisconnected_t *) event_data;
ESP_LOGI(TAG_AP, "Station "MACSTR" left, AID=%d",
MAC2STR(event->mac), event->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;
}
}
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
esp_wifi_connect();//连接wifi
ESP_LOGI(TAG_STA, "Station started");
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
ip_event_got_ip_t *event = (ip_event_got_ip_t *) event_data;
ESP_LOGI(TAG_STA, "Got IP:" IPSTR, IP2STR(&event->ip_info.ip));
s_retry_num = 0;
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);//设置wifi连接标志位
}
}
esp_netif_t *wifi_init_softap(void)
{
esp_netif_t *esp_netif_ap = esp_netif_create_default_wifi_ap();//创建默认的wifi热点
wifi_config_t wifi_ap_config = {
.ap = {
.ssid = EXAMPLE_ESP_WIFI_AP_SSID,
.ssid_len = strlen(EXAMPLE_ESP_WIFI_AP_SSID),
.channel = EXAMPLE_ESP_WIFI_CHANNEL,
.password = EXAMPLE_ESP_WIFI_AP_PASSWD,
.max_connection = EXAMPLE_MAX_STA_CONN,
.authmode = WIFI_AUTH_WPA2_PSK,
.pmf_cfg = {
.required = false,
},
},
};
if (strlen(EXAMPLE_ESP_WIFI_AP_PASSWD) == 0) {
wifi_ap_config.ap.authmode = WIFI_AUTH_OPEN;
}
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_ap_config));
ESP_LOGI(TAG_AP, "wifi_init_softap finished. SSID:%s password:%s channel:%d",
EXAMPLE_ESP_WIFI_AP_SSID, EXAMPLE_ESP_WIFI_AP_PASSWD, EXAMPLE_ESP_WIFI_CHANNEL);
return esp_netif_ap;
}
esp_netif_t *wifi_init_sta(void)
{
esp_netif_t *esp_netif_sta = esp_netif_create_default_wifi_sta();//创建默认的wifi站
wifi_config_t wifi_sta_config = {
.sta = {
.ssid = EXAMPLE_ESP_WIFI_STA_SSID,
.password = EXAMPLE_ESP_WIFI_STA_PASSWD,
.scan_method = WIFI_ALL_CHANNEL_SCAN,
.failure_retry_cnt = EXAMPLE_ESP_MAXIMUM_RETRY,
/* Authmode threshold resets to WPA2 as default if password matches WPA2 standards (pasword len => 8).
* If you want to connect the device to deprecated WEP/WPA networks, Please set the threshold value
* to WIFI_AUTH_WEP/WIFI_AUTH_WPA_PSK and set the password with length and format matching to
* WIFI_AUTH_WEP/WIFI_AUTH_WPA_PSK standards.
*/
.threshold.authmode = ESP_WIFI_SCAN_AUTH_MODE_THRESHOLD,
.sae_pwe_h2e = WPA3_SAE_PWE_BOTH,
},
};
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_sta_config) );
ESP_LOGI(TAG_STA, "wifi_init_sta finished.");
return esp_netif_sta;
}
WIFI的两种模式的配置,等待主函数的调用。代码中有注释,且是例程代码,很好理解。
4.2 自身作为AP和STA模式下的不同套接字的创建及配置
uint8_t sta_socket_init(void)//自身作为STA模式时套接字的创建以及配置
{
/* 初始化sta套接字 */
sta_scok = socket(AF_INET, SOCK_STREAM, 0);
if (sta_scok < 0) {//若返回值为-1,则表示创建失败,处理创建失败的情况
perror("STA_socket");//以socket:形式输出错误信息
exit(EXIT_FAILURE);//退出
return -1; //返回-1
}
struct sockaddr_in server_addr;//创建一个结构体变量,用于存储服务器的地址信息
memset(&server_addr, 0, sizeof(server_addr));//清零初始化
server_addr.sin_family = AF_INET;//设置地址族为IPv4
server_addr.sin_port = htons(8080);//设置端口号为8080
inet_pton(AF_INET, "替换成要连接的目标IP地址如:192.168.0.0", &server_addr.sin_addr); //将点分十进制的IP地址转换为二进制形式,并存储在server_addr.sin_addr中
if (connect(sta_scok, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {//连接服务器
perror("connect");//以connect:形式输出错误信息
closesocket(sta_scok);//关闭套接字
exit(EXIT_FAILURE);//退出
vTaskDelay(6 / portTICK_PERIOD_MS);//延时6ms
}
return 0;
}
uint8_t ap_socket_init(void)//自身作为AP模式时套接字的创建以及配置
{
struct sockaddr_storage dest_addr;//目标地址
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);//端口号
/* 初始化ap套接字 */
ap_scok = socket(AF_INET, SOCK_STREAM, 0);
if (ap_scok < 0) {//创建失败
ESP_LOGE(TAG_AP, "Unable to create socket: errno %d", errno);//打印错误信息
vTaskDelete(NULL);//删除任务
return -1;
}
int opt = 1;
setsockopt(ap_scok, 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_AP, "Socket created");
int err = bind(ap_scok, (struct sockaddr *)&dest_addr, sizeof(dest_addr));//绑定socket到目标地址
if (err != 0) {//绑定失败
ESP_LOGE(TAG_AP, "Socket unable to bind: errno %d", errno);//打印错误信息
//ESP_LOGE(TAG_AP, "IPPROTO: %d", addr_family);//打印IP协议
goto CLEAN_UP;//跳转到CLEAN_UP标签处
}
ESP_LOGI(TAG_AP, "Socket bound, port %d", PORT);
err = listen(ap_scok, 3);//监听socket
if (err != 0) {//监听失败
ESP_LOGE(TAG_AP, "Error occurred during listen: errno %d", errno);//打印错误信息
goto CLEAN_UP;//跳转到CLEAN_UP标签处
}
ESP_LOGI(TAG_AP, "Socket listening");
return 0;
CLEAN_UP:
close(ap_scok);//关闭socket
return -1;
}
根据不同模式下的工作特性,进行合理的配置。该部分内容许多都在前几期文章中详细解释过,读者可借助代码中的注释进行理解。
4.3 WIFI事件中被调用的接受连接函数
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_AP, "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选项为:保持计数
}
只有在有设备接入自己时才去接受连接,accept会有阻塞,所以需要在连接到ESP32的设备立即建立TCP连接。
4.4 数据转发实现中继功能函数
static void Task_do_retransmit()
{
int AP_len;//ap接收到的数据长度
int STA_len;//sta接收到的数据长度
char rx_buffer[128];//接收缓冲区
while(1){
vTaskDelay(200 / portTICK_PERIOD_MS);//延时200ms
STA_len = recv(sta_scok, rx_buffer, sizeof(rx_buffer) - 1, 0x08);//接收数据
if(STA_len > 0){//如果接收到了数据,注意WIFi接收到的数据是小端模式,我们单片机内存使用大端模式存储数据!
for(int x = 0; x < 10; x++){//遍历所有连接的设备
if(sta_info[x].sock != 0){//如果设备已连接
send(sta_info[x].sock, rx_buffer, STA_len, 0);//转发数据
}
}
}
for(int k = 0; k < 10; k++){//遍历所有连接的设备
if(sta_info[k].sock != 0){//如果设备已连接
AP_len = recv(sta_info[k].sock, rx_buffer, sizeof(rx_buffer) - 1, 0x08);//接收数据
if(AP_len > 0){//如果接收到了数据
int err = send(sta_scok, rx_buffer, AP_len, 0);//转发数据
printf("\nrecvive data: %s\n",rx_buffer);//打印接收到的数据
if(err > 0)
printf("\ntransaction OK!\n");//完成转发
}
}
}
}
}
代码注释解释的很清楚了,唯一要注意的是WIFI发送数据过程中使用的是小端模式,而在单片机内存中一般都以大端模式存储。说白了就是你想发0x1234,那么WIFI转发出去后设备收到的是0x3412(以1字节对齐的情况下)。这里我们没有对大小端模式进行处理,因为中继数据需要转发,ESP32第一次收到WIFI数据后原封不动的转发成WIFI数据,那么数据的存储方式就会这样转化:
当收到其他设备的数据后,这个数据在缓冲区以小端模式存放,ESP32并不做处理,原封不动的在用WIFI转发给其他设备之后,WIFI又会再一次的将这个"小端模式数据"作处理,在进行一次小端模式的发送。此时这个数据会被转化为大端模式,WIFI发送数据方式并不会准确识别数据的意义,而是直接进行字节序的转换。所以在经过WIFI两次发送之后,原本的大端模式数据会被小端模式倒两次变回最原本的数据。
4.5 主函数初始化WIFI外设
void app_main(void)
{
ESP_ERROR_CHECK(esp_netif_init());//初始化网络接口
ESP_ERROR_CHECK(esp_event_loop_create_default());//创建事件循环
//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());//清除flash
ret = nvs_flash_init();//重新初始化
}
ESP_ERROR_CHECK(ret);
/* Initialize event group */
s_wifi_event_group = xEventGroupCreate();//创建事件组
/* Register Event handler */
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,//注册WIFI事件
ESP_EVENT_ANY_ID,
&wifi_event_handler,
NULL,
NULL));
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,//注册IP事件
IP_EVENT_STA_GOT_IP,
&wifi_event_handler,
NULL,
NULL));
/*Initialize WiFi */
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));//初始化wifi
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_APSTA));//设置wifi模式为AP+STA
/* Initialize AP */
ESP_LOGI(TAG_AP, "ESP_WIFI_MODE_AP");
esp_netif_t *esp_netif_ap = wifi_init_softap();//初始化AP
/* Initialize STA */
ESP_LOGI(TAG_STA, "ESP_WIFI_MODE_STA");
esp_netif_t *esp_netif_sta = wifi_init_sta();//初始化STA
/* Start WiFi */
ESP_ERROR_CHECK(esp_wifi_start() );//启动wifi
/*
* Wait until either the connection is established (WIFI_CONNECTED_BIT) or
* connection failed for the maximum number of re-tries (WIFI_FAIL_BIT).
* The bits are set by event_handler() (see above)
*/
EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,//等待事件组
WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
pdFALSE,
pdFALSE,
portMAX_DELAY);
/* xEventGroupWaitBits() returns the bits before the call returned,
* hence we can test which event actually happened. */
if (bits & WIFI_CONNECTED_BIT) {
ESP_LOGI(TAG_STA, "connected to ap SSID:%s password:%s",
EXAMPLE_ESP_WIFI_STA_SSID, EXAMPLE_ESP_WIFI_STA_PASSWD);
} else if (bits & WIFI_FAIL_BIT) {
ESP_LOGI(TAG_STA, "Failed to connect to SSID:%s, password:%s",
EXAMPLE_ESP_WIFI_STA_SSID, EXAMPLE_ESP_WIFI_STA_PASSWD);
} else {
ESP_LOGE(TAG_STA, "UNEXPECTED EVENT");
return;
}
/* Set sta as the default interface */
esp_netif_set_default_netif(esp_netif_sta);//设置sta为默认接口
/* Enable napt on the AP netif */
if (esp_netif_napt_enable(esp_netif_ap) != ESP_OK) {//启用NAPT
ESP_LOGE(TAG_STA, "NAPT not enabled on the netif: %p", esp_netif_ap);
}
sta_socket_init();//初始化作为sta模式的套接字
ap_socket_init();//初始化作为ap模式套接字
xTaskCreate(Task_do_retransmit, "Task_do_retransmit", 4096 * 5, NULL, 5, NULL);//创建任务
}
4.6 头文件以及宏定义等内容
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_mac.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_netif_net_stack.h"
#include "esp_netif.h"
#include "nvs_flash.h"
#include "lwip/inet.h"
#include "lwip/netdb.h"
#include "lwip/sockets.h"
#include "lwip/lwip_napt.h"
#include "lwip/err.h"
#include "lwip/sys.h"
#define EXAMPLE_ESP_WIFI_STA_SSID "替换成要连接的热点名称"//CONFIG_ESP_WIFI_REMOTE_AP_SSID
#define EXAMPLE_ESP_WIFI_STA_PASSWD "替换成要连接的热点密码"//CONFIG_ESP_WIFI_REMOTE_AP_PASSWORD
#define EXAMPLE_ESP_MAXIMUM_RETRY CONFIG_ESP_MAXIMUM_STA_RETRY
#if CONFIG_ESP_WIFI_AUTH_OPEN
#define ESP_WIFI_SCAN_AUTH_MODE_THRESHOLD WIFI_AUTH_OPEN
#elif CONFIG_ESP_WIFI_AUTH_WEP
#define ESP_WIFI_SCAN_AUTH_MODE_THRESHOLD WIFI_AUTH_WEP
#elif CONFIG_ESP_WIFI_AUTH_WPA_PSK
#define ESP_WIFI_SCAN_AUTH_MODE_THRESHOLD WIFI_AUTH_WPA_PSK
#elif CONFIG_ESP_WIFI_AUTH_WPA2_PSK
#define ESP_WIFI_SCAN_AUTH_MODE_THRESHOLD WIFI_AUTH_WPA2_PSK
#elif CONFIG_ESP_WIFI_AUTH_WPA_WPA2_PSK
#define ESP_WIFI_SCAN_AUTH_MODE_THRESHOLD WIFI_AUTH_WPA_WPA2_PSK
#elif CONFIG_ESP_WIFI_AUTH_WPA3_PSK
#define ESP_WIFI_SCAN_AUTH_MODE_THRESHOLD WIFI_AUTH_WPA3_PSK
#elif CONFIG_ESP_WIFI_AUTH_WPA2_WPA3_PSK
#define ESP_WIFI_SCAN_AUTH_MODE_THRESHOLD WIFI_AUTH_WPA2_WPA3_PSK
#elif CONFIG_ESP_WIFI_AUTH_WAPI_PSK
#define ESP_WIFI_SCAN_AUTH_MODE_THRESHOLD WIFI_AUTH_WAPI_PSK
#endif
/* AP Configuration */
#define EXAMPLE_ESP_WIFI_AP_SSID "创建的热点名称"//CONFIG_ESP_WIFI_AP_SSID
#define EXAMPLE_ESP_WIFI_AP_PASSWD "创建的热点密码"//CONFIG_ESP_WIFI_AP_PASSWORD
#define EXAMPLE_ESP_WIFI_CHANNEL CONFIG_ESP_WIFI_AP_CHANNEL
#define EXAMPLE_MAX_STA_CONN CONFIG_ESP_MAX_STA_CONN_AP
/* The event group allows multiple bits for each event, but we only care about two events:
* - we are connected to the AP with an IP
* - we failed to connect after the maximum amount of retries */
#define WIFI_CONNECTED_BIT BIT0
#define WIFI_FAIL_BIT BIT1
static const char *TAG_AP = "WiFi SoftAP";
static const char *TAG_STA = "WiFi Sta";
static int s_retry_num = 0;
/* FreeRTOS event group to signal when we are connected/disconnected */
static EventGroupHandle_t s_wifi_event_group;
int sta_scok = 0;//STA模式的套接字
int ap_scok = 0;//AP模式的套接字
typedef struct _STA_info{
uint8_t deveci_mac[6];//连接设备的mac地址
int sock; //连接设备的套接字描述符
struct sockaddr_storage source_addr;//存储连接设备的IP与端口号
}STA_info;
void accept_func(int sockfd,int offset,STA_info *sta_info);
STA_info sta_info[10];//连接设备信息表
#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
以上函数中没找到的变量和宏都可以在这里找到。
5.成果展示
电脑端作为ESP32所连接的热点,收到手机发来的数据,这个数据由ESP32中继转发出来,电脑也可以向手机发送数据,ESP32同样可以中继转发。
6. 总结
以上就是本次分享的全部内容,用好AP+STA模式的基础是将单独的AP和STA模式使用好,包括WIFI的配置和套接字的熟练使用,融会贯通之后便会更上一层楼。如何使用AP模式、STA模式在前几期文章中有详细解释。链接放在上文了。大家如果觉得有用,还请多多点赞收藏关注,你的支持就是对我最大的肯定。谢谢大家!
如有疑问欢迎留言交流。以上观点为个人理解,只想抛砖引玉,如有不对的地方欢迎指正,不喜轻喷。
2024.10.26-18:00
WIFI部分完结撒花,下期开始更新蓝牙的使用
标签:sta,ESP,ESP32,WIFI,AP,event,STA From: https://blog.csdn.net/zyZYzy9900/article/details/143256473