当前位置: 首页 > news >正文

12. ESP32-S3 WIFI AP模式TCP通信实战:从服务端到客户端的双向数据收发

ESP32-S3 WIFI AP模式TCP通信实战:从服务端到客户端的双向数据收发

最近好几个朋友在问,用ESP32-S3做智能家居设备或者无线调试工具时,怎么让设备之间直接通信,不经过路由器?这种场景其实挺常见的,比如两个设备要直接交换数据,或者手机直接连接设备进行配置。今天我就来分享一下ESP32-S3在AP模式下实现TCP通信的完整方案,包含服务端和客户端两种模式。

咱们这个教程会手把手教你搭建一个完整的通信系统:ESP32-S3作为热点(AP),手机或其他设备连接它,然后通过TCP协议进行双向数据收发。我会把代码拆开揉碎了讲,保证你跟着做一遍就能完全掌握。

1. 项目准备与环境搭建

1.1 硬件准备

首先需要一块ESP32-S3开发板,我用的是ESP32S3R8N8这个型号。这个板子内置了WiFi和蓝牙,性能足够我们做网络通信实验。如果你用的是其他ESP32-S3开发板,代码也是通用的。

1.2 软件环境

确保你已经搭建好了ESP-IDF开发环境。我用的是ESP-IDF v5.0版本,如果你用的是其他版本,大部分代码应该也能兼容,但建议尽量用较新的版本。

注意:如果你还没装ESP-IDF,可以去乐鑫官网下载安装,或者用VSCode的ESP-IDF插件,那个安装起来更方便。

1.3 项目结构

创建一个新的ESP-IDF项目,或者直接在现有的项目里添加我们的代码。整个项目主要包含以下几个部分:

  • WiFi AP模式初始化
  • TCP服务端/客户端创建
  • 数据收发任务
  • 错误处理和重连机制

2. WiFi AP模式初始化

2.1 什么是AP模式?

先简单说一下AP模式。AP就是Access Point(接入点),你可以把它理解成一个无线路由器。在AP模式下,ESP32-S3自己创建一个WiFi网络,其他设备(比如手机、电脑)可以连接这个网络。

和常见的STA模式(设备连接路由器)不同,AP模式不需要外部路由器,设备之间可以直接通信。这在很多物联网场景下很有用,比如设备配网、设备间直连通信等。

2.2 初始化代码详解

咱们先看看怎么初始化WiFi AP。下面这段代码是核心,我加了详细注释:

void wifi_init_softap(void) { // 创建事件组,用来同步WiFi连接状态 tcp_event_group = xEventGroupCreate(); // 初始化TCP/IP协议栈 ESP_ERROR_CHECK(esp_netif_init()); // 创建默认的事件循环,用来处理WiFi事件 ESP_ERROR_CHECK(esp_event_loop_create_default()); // 创建默认的WiFi AP网络接口 esp_netif_create_default_wifi_ap(); // WiFi初始化配置,使用默认参数 wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); 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)); // 配置AP参数 wifi_config_t wifi_config = { .ap = { .ssid = EXAMPLE_ESP_WIFI_SSID, // WiFi名称 .ssid_len = strlen(EXAMPLE_ESP_WIFI_SSID), // 名称长度 .password = EXAMPLE_ESP_WIFI_PASS, // WiFi密码 .max_connection = EXAMPLE_MAX_STA_CONN, // 最大连接数,这里设为5 .authmode = WIFI_AUTH_WPA2_PSK, // 加密方式,WPA2 .pmf_cfg = { .required = true, // 保护管理帧,增强安全性 }, }, }; // 如果没有设置密码,就用开放网络 if (strlen(EXAMPLE_ESP_WIFI_PASS) == 0) { wifi_config.ap.authmode = WIFI_AUTH_OPEN; } // 设置WiFi为AP模式 ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP)); // 应用AP配置 ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config)); // 启动WiFi ESP_ERROR_CHECK(esp_wifi_start()); ESP_LOGI(TAG, "wifi_init_softap finished. SSID:%s password:%s", EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS); }

这里有几个关键点需要注意:

  1. 事件组:我们用xEventGroupCreate()创建了一个事件组,后面会用它来同步TCP连接。当有设备连接上AP时,我们会设置一个事件位;当设备断开时,清除这个事件位。

  2. 网络接口初始化esp_netif_init()esp_event_loop_create_default()是必须的,它们初始化了底层的网络协议栈和事件处理机制。

  3. AP配置参数

    • SSID:WiFi名称,我设为"LCKFB-ESP32"
    • 密码:12345678
    • 最大连接数:5个设备
    • 加密方式:WPA2,这是目前比较安全的加密方式
  4. 事件处理:我们注册了wifi_event_handler函数来处理WiFi事件。这个函数很重要,它会在设备连接或断开时被调用。

2.3 WiFi事件处理

当有设备连接或断开我们的AP时,系统会触发事件。我们需要处理这些事件来知道什么时候可以建立TCP连接:

static void wifi_event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) { switch (event_id){ case WIFI_EVENT_AP_STACONNECTED: // 有设备连接成功 // 设置事件位,告诉TCP任务可以开始连接了 xEventGroupSetBits(tcp_event_group, WIFI_CONNECTED_BIT); break; case WIFI_EVENT_AP_STADISCONNECTED: // 有设备断开连接 // 标记需要重新建立TCP连接 g_rxtx_need_restart = true; // 清除事件位 xEventGroupClearBits(tcp_event_group, WIFI_CONNECTED_BIT); break; default: break; } }

这个机制很实用:有设备连上时,我们设置一个标志位;设备断开时,我们清除标志位并标记需要重连。这样TCP任务就知道什么时候该建立连接,什么时候该重连。

3. TCP服务端实现

3.1 服务端工作流程

服务端的工作流程是这样的:

  1. 创建socket(可以理解为一个通信端点)
  2. 绑定到指定的IP和端口
  3. 监听连接请求
  4. 接受客户端的连接
  5. 接收和发送数据

3.2 创建TCP服务端

下面是创建TCP服务端的核心函数,我把它拆开一步步讲:

esp_err_t create_tcp_server(bool isCreatServer) { // 如果是第一次创建服务器,需要创建socket并绑定 if (isCreatServer){ ESP_LOGI(TAG, "server socket....,port=%d", TCP_PORT); // 创建socket:AF_INET表示IPv4,SOCK_STREAM表示TCP server_socket = socket(AF_INET, SOCK_STREAM, 0); if (server_socket < 0){ show_socket_error_reason("create_server", server_socket); close(server_socket); return ESP_FAIL; } // 配置服务器地址信息 server_addr.sin_family = AF_INET; // IPv4 server_addr.sin_port = htons(TCP_PORT); // 端口号9527,htons转换字节序 server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有网卡 // 绑定socket到地址 if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0){ show_socket_error_reason("bind_server", server_socket); close(server_socket); return ESP_FAIL; } } // 开始监听,最多5个连接在队列中等待 if (listen(server_socket, 5) < 0){ show_socket_error_reason("listen_server", server_socket); close(server_socket); return ESP_FAIL; } // 接受客户端连接 connect_socket = accept(server_socket, (struct sockaddr *)&client_addr, &socklen); if (connect_socket < 0){ show_socket_error_reason("accept_server", connect_socket); close(server_socket); return ESP_FAIL; } ESP_LOGI(TAG, "tcp connection established!"); return ESP_OK; }

这里有几个技术细节需要注意:

  1. socket创建socket(AF_INET, SOCK_STREAM, 0)创建了一个TCP socket。AF_INET表示IPv4,SOCK_STREAM表示面向连接的TCP协议。

  2. 地址绑定bind()函数把socket绑定到指定的IP和端口。INADDR_ANY表示监听所有网卡,对于AP模式,ESP32-S3的IP默认是192.168.4.1。

  3. 监听和接受listen()开始监听连接请求,参数5表示最多可以有5个连接在队列中等待处理。accept()会阻塞等待,直到有客户端连接进来。

提示:htons()htonl()是字节序转换函数。网络字节序是大端序,而我们的ESP32-S3是小端序,所以需要转换。

3.3 数据收发任务

连接建立后,我们需要一个任务来处理数据收发:

void recv_data(void *pvParameters) { int len = 0; char databuff[1024]; // 接收缓冲区,1KB大小 while (1){ memset(databuff, 0x00, sizeof(databuff)); // 清空缓冲区 // 接收数据,recv是阻塞函数,会一直等待直到有数据到来 len = recv(connect_socket, databuff, sizeof(databuff), 0); g_rxtx_need_restart = false; if (len > 0){ ESP_LOGI(TAG, "recvData: %s", databuff); // 打印接收到的数据 // 把收到的数据原样发回去(回显) send(connect_socket, databuff, strlen(databuff), 0); }else{ // 接收出错,打印错误信息 show_socket_error_reason("recv_data", connect_socket); g_rxtx_need_restart = true; // 标记需要重连 vTaskDelete(NULL); // 删除当前任务 } } close_socket(); g_rxtx_need_restart = true; vTaskDelete(NULL); }

这个任务做了几件事:

  1. 创建一个1KB的缓冲区
  2. recv()接收数据(这个函数会阻塞等待)
  3. 如果收到数据,打印出来并原样发回(回显)
  4. 如果接收出错,标记需要重连并退出任务

3.4 主任务流程

TCP连接的主任务负责协调整个流程:

static void tcp_connect(void *pvParameters) { while (1){ g_rxtx_need_restart = false; // 等待WiFi连接事件,有设备连上AP才继续 xEventGroupWaitBits(tcp_event_group, WIFI_CONNECTED_BIT, false, true, portMAX_DELAY); ESP_LOGI(TAG, "start tcp connected"); TaskHandle_t tx_rx_task = NULL; vTaskDelay(3000 / portTICK_PERIOD_MS); // 延时3秒,给设备一些准备时间 // 创建TCP服务器 ESP_LOGI(TAG, "create tcp server"); int socket_ret = create_tcp_server(true); if (socket_ret == ESP_FAIL){ ESP_LOGI(TAG, "create tcp socket error,stop..."); continue; // 创建失败,重试 }else{ ESP_LOGI(TAG, "create tcp socket succeed..."); // 创建数据接收任务 if (pdPASS != xTaskCreate(&recv_data, "recv_data", 4096, NULL, 4, &tx_rx_task)){ ESP_LOGI(TAG, "Recv task create fail!"); }else{ ESP_LOGI(TAG, "Recv task create succeed!"); } } // 主循环,检查是否需要重连 while (1){ vTaskDelay(3000 / portTICK_PERIOD_MS); if (g_rxtx_need_restart){ ESP_LOGI(TAG, "tcp server error,some client leave,restart..."); // 重新建立连接(不是第一次创建,所以传false) if (ESP_FAIL != create_tcp_server(false)){ if (pdPASS != xTaskCreate(&recv_data, "recv_data", 4096, NULL, 4, &tx_rx_task)){ ESP_LOGE(TAG, "tcp server Recv task create fail!"); }else{ ESP_LOGI(TAG, "tcp server Recv task create succeed!"); g_rxtx_need_restart = false; // 清除重连标记 } } } } } vTaskDelete(NULL); }

这个任务的设计思路是:

  1. 等待有设备连接AP
  2. 创建TCP服务器
  3. 创建数据接收任务
  4. 定期检查是否需要重连(比如客户端断开)

4. TCP客户端实现

4.1 客户端与服务端的区别

客户端和服务端的主要区别在于连接方式:

  • 服务端:绑定端口,监听连接,接受连接
  • 客户端:知道服务端的IP和端口,主动发起连接

在我们的例子里,服务端IP是192.168.4.1(ESP32-S3 AP的默认IP),端口是9527。

4.2 创建TCP客户端

客户端代码和服务端类似,但更简单一些:

esp_err_t create_tcp_client(void) { ESP_LOGI(TAG, "will connect gateway ssid : %s port:%d", TCP_SERVER_ADRESS, TCP_PORT); // 创建socket connect_socket = socket(AF_INET, SOCK_STREAM, 0); if (connect_socket < 0){ show_socket_error_reason("create client", connect_socket); close(connect_socket); return ESP_FAIL; } // 配置服务器地址信息 server_addr.sin_family = AF_INET; server_addr.sin_port = htons(TCP_PORT); server_addr.sin_addr.s_addr = inet_addr(TCP_SERVER_ADRESS); // 服务端IP ESP_LOGI(TAG, "connectting server..."); // 连接服务器 if (connect(connect_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0){ show_socket_error_reason("client connect", connect_socket); ESP_LOGE(TAG, "connect failed!"); close(connect_socket); return ESP_FAIL; } ESP_LOGI(TAG, "connect success!"); return ESP_OK; }

客户端的关键步骤:

  1. 创建socket(和服务端一样)
  2. 配置要连接的服务端地址(IP:192.168.4.2,端口:9527)
  3. 调用connect()主动连接服务端

注意:这里的TCP_SERVER_ADRESS是"192.168.4.2",这是另一个ESP32-S3作为服务端的IP。在实际项目中,你需要根据实际情况修改这个地址。

4.3 客户端的主任务

客户端的主任务和服务端类似,但连接逻辑不同:

static void tcp_connect(void *pvParameters) { while (1){ g_rxtx_need_restart = false; // 等待WiFi连接事件 xEventGroupWaitBits(tcp_event_group, WIFI_CONNECTED_BIT, false, true, portMAX_DELAY); ESP_LOGI(TAG, "start tcp connected"); TaskHandle_t tx_rx_task = NULL; vTaskDelay(3000 / portTICK_PERIOD_MS); // 创建TCP客户端连接 ESP_LOGI(TAG, "create tcp server"); int socket_ret = create_tcp_client(); // 这里是客户端连接 if (socket_ret == ESP_FAIL){ ESP_LOGI(TAG, "create tcp socket error,stop..."); continue; }else{ ESP_LOGI(TAG, "create tcp socket succeed..."); // 创建数据接收任务(和服务器端一样) if (pdPASS != xTaskCreate(&recv_data, "recv_data", 4096, NULL, 4, &tx_rx_task)){ ESP_LOGI(TAG, "Recv task create fail!"); }else{ ESP_LOGI(TAG, "Recv task create succeed!"); } } // 检查重连 while (1){ vTaskDelay(3000 / portTICK_PERIOD_MS); if (g_rxtx_need_restart){ ESP_LOGI(TAG, "tcp server error,some client leave,restart..."); // 重新连接服务器 if (ESP_FAIL != create_tcp_client()){ if (pdPASS != xTaskCreate(&recv_data, "recv_data", 4096, NULL, 4, &tx_rx_task)){ ESP_LOGE(TAG, "tcp client Recv task create fail!"); }else{ ESP_LOGI(TAG, "tcp client Recv task create succeed!"); g_rxtx_need_restart = false; } } } } } vTaskDelete(NULL); }

4.4 按键触发连接

客户端代码里还有一个有趣的设计:用BOOT按键来触发TCP连接。这个在实际项目中很实用,比如设备启动后等待用户按键才开始连接。

void app_main(void) { // ... 初始化代码 ... // 配置GPIO0(BOOT按键)为输入模式 gpio_config_t io_conf; io_conf.intr_type = GPIO_INTR_ANYEDGE; // 下降沿和上升沿触发中断 io_conf.pin_bit_mask = 1 << 0; // GPIO0 io_conf.mode = GPIO_MODE_INPUT; // 输入模式 io_conf.pull_up_en = GPIO_PULLUP_ENABLE; // 使能上拉 gpio_config(&io_conf); while(1) { vTaskDelay(500/portTICK_PERIOD_MS); if(gpio_get_level(0)==0){ // 检测按键按下(低电平) // 创建TCP连接任务 xTaskCreate(&tcp_connect, "tcp_connect", 4096, NULL, 5, NULL); break; } } }

这样设计的好处是:设备启动后先创建AP,然后等待用户按下BOOT键才开始连接服务器。这在调试和测试时很方便。

5. 测试与验证

5.1 服务端测试

  1. 编译并下载服务端代码到ESP32-S3
  2. 手机搜索WiFi"LCKFB-ESP32",密码"12345678"
  3. 手机连接这个WiFi
  4. 在手机上打开TCP调试助手(比如"网络调试助手"APP)
  5. 设置为TCP客户端,IP填192.168.4.1,端口9527
  6. 点击连接,应该能看到连接成功的日志
  7. 发送数据,ESP32-S3会回显同样的数据

5.2 客户端测试

  1. 先确保有一个ESP32-S3运行服务端代码
  2. 编译并下载客户端代码到另一个ESP32-S3
  3. 手机连接客户端ESP32-S3的AP(同样是"LCKFB-ESP32")
  4. 在手机上打开TCP调试助手,设置为TCP服务器,端口9527
  5. 按下客户端ESP32-S3的BOOT键
  6. 客户端会连接服务端(192.168.4.2:9527)
  7. 在手机端发送数据,客户端收到后会回显

5.3 常见问题排查

在实际测试中可能会遇到一些问题,这里分享几个我踩过的坑:

问题1:连接失败

  • 检查IP地址是否正确:服务端是192.168.4.1,客户端要连接192.168.4.2
  • 检查端口号:两边都要是9527
  • 检查防火墙:有些手机会阻止网络调试助手的网络访问

问题2:数据收发不正常

  • 检查缓冲区大小:我们的代码用了1024字节缓冲区,如果发送的数据超过这个大小,需要分多次发送
  • 检查字符串结束符:recv()接收的是原始数据,如果当字符串处理,要确保有'\0'结束符

问题3:连接不稳定

  • 调整重连机制:代码里已经有重连逻辑,但你可以根据实际情况调整重连间隔
  • 检查信号强度:AP模式信号范围有限,确保设备在有效范围内

6. 实际项目应用建议

这个代码框架在实际项目中可以直接用,但可能需要根据具体需求做一些调整:

1. 数据协议设计现在的代码是简单的回显,实际项目需要定义通信协议。比如可以这样设计:

// 定义数据包结构 typedef struct { uint8_t header[2]; // 包头,比如0xAA 0x55 uint16_t length; // 数据长度 uint8_t cmd; // 命令字 uint8_t data[256]; // 数据内容 uint8_t checksum; // 校验和 } data_packet_t;

2. 多客户端支持当前服务端只支持一个客户端连接。如果需要支持多个客户端,可以用select()poll()实现多路复用,或者为每个客户端创建一个独立的任务。

3. 错误恢复增强代码里已经有基本的错误恢复机制,但可以进一步增强:

  • 添加心跳包检测连接状态
  • 实现断线自动重连
  • 添加日志记录,方便排查问题

4. 安全性考虑

  • 实际项目中不要用固定密码"12345678"
  • 可以考虑添加数据加密
  • 实现身份验证机制

这个AP模式的TCP通信框架我在好几个项目里都用过,比如无线调试工具、设备间直接数据同步、智能家居设备配网等场景。掌握了这个基础,你就能根据具体需求进行扩展和优化了。

代码里我尽量加了详细的注释,但嵌入式开发就是这样,看着简单,调试起来可能会遇到各种问题。如果测试中遇到问题,可以先检查日志输出,ESP32的日志系统很强大,能帮你快速定位问题所在。

http://www.cnnetsun.cn/news/1263974.html

相关文章:

  • Chord - Ink Shadow 与ComfyUI可视化工作流结合猜想
  • <蓝桥杯软件赛>零基础备赛20周--第18周--动态规划进阶:从“更小的数”到“接龙数列”
  • CogVideoX-2b精彩案例:消费级显卡生成流畅视频演示
  • 工业互联网场景:DAMOYOLO-S在产线视频流中的实时缺陷检测架构
  • 嵌入式音频接口实战:从I2S到TDM的多通道音频传输设计
  • PHP 8.9扩展模块安全加固:3小时内完成OpenSSL、cURL、GD三大高危组件强制TLS 1.3+与内存隔离配置
  • DeepAnalyze惊艳案例:DeepAnalyze从200页PDF财报中自动提取管理层讨论核心结论与隐含风险
  • 智能孕婴护理知识科普商城平台Python django flask
  • TexStudio 中解决 Latex 算法伪代码包冲突:从 Missing \endcsname inserted 到流畅编译
  • LRS2数据集预处理实战:从下载到人脸与音频提取
  • 立创ESP32非侵入式三相电能传感器:基于ADE7878与WiFi的NILM方案设计与实现
  • 基于SpringBoot Actuator与Kubernetes的优雅停机策略优化实践
  • Qwen3-ASR-1.7B与人工智能技术的融合创新
  • 高斯分布KL散度在变分自编码器中的应用解析
  • Steam成就管理神器:从困境到解决方案的技术指南
  • Qwen1.5-1.8B GPTQ性能调优全攻略:从参数配置到硬件选型
  • 海思ARM平台udev启动难题:从“uninitialized urandom read”到系统就绪
  • 3个效率革命:零代码自动化解决演示文稿制作痛点
  • 使用Anaconda和conda快速搭建YOLO开发环境
  • 《高效开发秘籍》Unity自动化UI框架ZMUIFramework的性能优化实践
  • MogFace人脸检测模型-WebUI效果对比:在WIDER FACE hard subset上mAP达86.4%
  • 基于ESP32-S3与PCM1822/PCM5102的立创开源无线领夹麦克风DIY全解析
  • LiuJuan20260223Zimage实战:构建一个全栈AI网站(前端+后端+模型)
  • 打破PDF笔记壁垒:Obsidian PDF Plus让文献管理效率提升300%的秘密
  • 3步搞定黑丝空姐-造相Z-Turbo:Git版本管理与模型迭代
  • 解锁yolov8全能力:借助快马平台ai助手玩转分割与姿态估计
  • MPh自动化仿真:3天掌握Python控制COMSOL的高效科研工具
  • Linux 6个超好用基础指令,10分钟搞定
  • Android Studio中文语言包:突破开发效率瓶颈的本地化解决方案 — 从安装配置到深度优化
  • MusePublic开源模型应用:AI生成艺术教育评估标准可视化图表