Kindle+ESP32-S3实现TCP温湿度采集与灯光控制系统

Kindle+ESP32-S3实现TCP温湿度采集与灯光控制系统

项目概述本项目基于 ESP32-S3 搭建 TCP 服务端,外接 AHT20 温湿度传感器、WS2812 彩灯;Kindle 8(Linux 环境)作为 TCP 客户端,采用交互式命令行菜单,通过 TCP 网络实现远程查询温湿度、控制彩灯模式。通信采用自定义文本协议,以换行符作为消息边界,解决 TCP 流式粘包、单次指令连接断开问题。

硬件清单

  1. ESP32-S3 开发板
  2. AHT20 数字温湿度传感器
  3. WS2812 8 路 RGB 灯带
  4. Kindle 8(越狱 Linux 环境,arm-gcc 编译运行 TCP 客户端)

一、硬件设计

1.1 硬件接线定义

ESP32-S3

外设

说明

GPIO1

WS2812 DIN

彩灯数据引脚

GPIO10

AHT20 SDA

I2C1 数据

GPIO11

AHT20 SCL

I2C1 时钟

3.3V

AHT20、WS2812 电源

共电源

GND

AHT20、WS2812 GND

必须共地

1.2 硬件注意事项

  1. WS2812 工作电压 5V,若使用 3.3V 驱动容易色彩异常、频闪;长时间点亮建议灯带外接 5V 供电,信号端串联 330Ω 电阻。
  2. AHT20 模块自带 I2C 上拉电阻,无需额外焊接。
  3. ESP32 模块供电充足,大电流负载会导致电压跌落,WiFi 断连。

二、通信协议规范

2.1 传输层

TCP IPv4,ESP32 作为服务端,监听端口:8899客户端:Kindle8 Linux TCP Client,一问一答模式。

2.2 报文格式

  • 通信编码:ASCII 文本
  • 消息分隔符:\n(换行符)
  • 每条发送指令末尾必须追加\n
  • 服务端响应末尾追加\n

客户端 → ESP32 服务端(指令集)

指令

功能描述

ESP32 返回应答

GET

查询温湿度

TEMP:XX.XX,HUM:XX.XX

CMD1

WS2812 红色全亮

OK:RED

CMD2

WS2812 绿色全亮

OK:GREEN

CMD3

WS2812 黄色全亮

OK:YELLOW

CMD4

WS2812 彩虹跑马灯

OK:RUNLIGHT

CMD5

WS2812 全部熄灭

OK:OFF

其他非法指令

-

ERR:Unknown Command

2.3 通信容错设计

  1. ESP32 服务端:逐字符非阻塞解析指令,废弃阻塞函数readStringUntil(),防止 WiFi 任务卡死、TCP 主动断开。
  2. Linux 客户端:循环接收数据直到捕获换行符,避免 TCP 流式粘包、读取历史缓存数据。
  3. 客户端 Socket 设置接收超时,防止网络异常程序永久阻塞。

三、ESP32-S3 TCP 服务端软件设计(Arduino)

3.1 开发环境

Arduino IDE,平台:esp32 v2.x 依赖库:

  • FastLED(WS2812 驱动)
  • Adafruit AHTX0(AHT20 温湿度驱动)

3.2 软件架构

  1. WiFi STA 模式连接路由器,自动获取 IP,启动 TCP Server (8899)
  2. 非阻塞轮询接收 TCP 字符,缓冲区缓存,识别\n切割完整指令
  3. 指令分发处理:温湿度读取 / WS2812 模式切换
  4. 独立定时器驱动:
    • 1 秒串口打印温湿度
    • 150ms 刷新跑马灯效果
  1. 客户端断线自动清空灯带、释放资源

3.3 完整源代码(直接可用)

#include <Arduino.h> #include <WiFi.h> #include <FastLED.h> #include <Adafruit_AHTX0.h> //==================== 配置区 ==================== const char* WIFI_SSID = "WIFI名称"; const char* WIFI_PASSWORD = "WIFI密码"; const uint16_t TCP_PORT = 8899; #define WS2812_PIN 1 #define LED_COUNT 8 #define AHT_SDA_PIN 10 #define AHT_SCL_PIN 11 #define RECV_BUF_LEN 64 //================================================ CRGB leds[LED_COUNT]; Adafruit_AHTX0 aht; WiFiServer tcpServer(TCP_PORT); WiFiClient client; char recvBuf[RECV_BUF_LEN]; uint16_t bufIndex = 0; enum LedMode { MODE_OFF, MODE_RED, MODE_GREEN, MODE_YELLOW, MODE_RUNLIGHT }; LedMode currentMode = MODE_OFF; uint8_t hueValue = 0; void setAllLed(CRGB color) { for(int i=0; i<LED_COUNT; i++){ leds[i] = color; } FastLED.show(); } void handleTcpCommand(String cmd) { cmd.trim(); cmd.toUpperCase(); Serial.print("收到TCP指令: "); Serial.println(cmd); if(cmd == "GET"){ sensors_event_t humEvent, tempEvent; String resp; if(aht.getEvent(&humEvent, &tempEvent)){ resp = "TEMP:" + String(tempEvent.temperature,2) + ",HUM:" + String(humEvent.relative_humidity,2); }else{ resp = "TEMP:ERR,HUM:ERR"; } client.print(resp + "\n"); } else if(cmd == "CMD1"){ currentMode = MODE_RED; setAllLed(CRGB::Red); client.print("OK:RED\n"); } else if(cmd == "CMD2"){ currentMode = MODE_GREEN; setAllLed(CRGB::Green); client.print("OK:GREEN\n"); } else if(cmd == "CMD3"){ currentMode = MODE_YELLOW; setAllLed(CRGB::Yellow); client.print("OK:YELLOW\n"); } else if(cmd == "CMD4"){ currentMode = MODE_RUNLIGHT; client.print("OK:RUNLIGHT\n"); } else if(cmd == "CMD5"){ currentMode = MODE_OFF; setAllLed(CRGB::Black); client.print("OK:OFF\n"); } else{ client.print("ERR:Unknown Command\n"); } } void setup() { Serial.begin(115200); while(!Serial) delay(10); FastLED.addLeds<WS2812, WS2812_PIN, GRB>(leds, LED_COUNT); FastLED.setBrightness(64); FastLED.clear(); FastLED.show(); Wire1.begin(AHT_SDA_PIN, AHT_SCL_PIN); if (!aht.begin(&Wire1)) { Serial.println("【错误】AHT20初始化失败 SDA:10 SCL:11"); } else { Serial.println("AHT20 初始化成功"); } Serial.print("连接WiFi:"); Serial.println(WIFI_SSID); WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println("\nWiFi连接成功"); Serial.print("本机IP: "); Serial.println(WiFi.localIP()); tcpServer.begin(); Serial.printf("TCP服务器启动,端口:%d\r\n",TCP_PORT); Serial.println("================================"); } void loop() { if (!client.connected()) { if (client) { client.stop(); bufIndex = 0; memset(recvBuf, 0, RECV_BUF_LEN); currentMode = MODE_OFF; FastLED.clear(); FastLED.show(); } client = tcpServer.accept(); if(client){ Serial.println("新客户端接入"); } } else { while(client.available() > 0) { char c = client.read(); if (c == '\n') { if(bufIndex > 0) { recvBuf[bufIndex] = '\0'; String cmdStr(recvBuf); handleTcpCommand(cmdStr); bufIndex = 0; memset(recvBuf, 0, RECV_BUF_LEN); } } else { if(bufIndex < RECV_BUF_LEN-1) { recvBuf[bufIndex++] = c; } } } } static uint32_t printTimer = 0; if(millis() - printTimer > 1000){ printTimer = millis(); sensors_event_t humEvent, tempEvent; if (aht.getEvent(&humEvent, &tempEvent)) { Serial.print("温度:"); Serial.print(tempEvent.temperature, 2); Serial.print(" ℃ | 湿度:"); Serial.print(humEvent.relative_humidity, 2); Serial.println(" %RH"); } else { Serial.println("读取AHT20数据失败"); } } static uint32_t ledTimer = 0; if(millis() - ledTimer > 150){ ledTimer = millis(); if(currentMode == MODE_RUNLIGHT){ static uint8_t pos = 0; FastLED.clear(); leds[pos] = CHSV(hueValue,255,255); FastLED.show(); pos++; if(pos >= LED_COUNT) pos = 0; hueValue +=3; } } delay(10); }

四、Kindle8 Linux TCP 客户端软件设计

4.1 运行环境

Kindle 8 越狱系统(Linux),gcc 编译器;纯 C 语言标准 POSIX Socket,无第三方依赖。 功能:交互式菜单、TCP 连接、一发一收、超时保护、完整接收单行应答,解决粘包问题。

4.2 客户端源代码tcp_client.c

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <errno.h> #include <sys/time.h> #define SERVER_IP "192.168.1.11" // 修改为ESP32 IP #define SERVER_PORT 8899 #define BUF_SIZE 256 #define RECV_TIMEOUT_SEC 3 // 接收超时2秒 int sock_fd = -1; /* 设置socket接收超时 */ static void set_sock_timeout(int fd, int sec) { struct timeval timeout; timeout.tv_sec = sec; timeout.tv_usec = 0; setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); } /* 建立TCP连接 */ int connect_server(void) { struct sockaddr_in serv_addr; if (sock_fd > 0) { printf("已经连接,无需重连\n"); return 0; } sock_fd = socket(AF_INET, SOCK_STREAM, 0); if (sock_fd < 0) { perror("socket create failed"); return -1; } set_sock_timeout(sock_fd, RECV_TIMEOUT_SEC); memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(SERVER_PORT); if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) { printf("IP地址格式错误\n"); close(sock_fd); sock_fd = -1; return -1; } if (connect(sock_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) { perror("connect failed"); close(sock_fd); sock_fd = -1; return -1; } printf("✅ 成功连接 ESP32 TCP Server %s:%d\n", SERVER_IP, SERVER_PORT); return 0; } /* * 发送指令,循环接收直到读到换行符,获取完整一行应答 * 返回 0成功,-1失败/超时/断开 */ int send_command(const char *cmd, char *resp, int resp_len) { char send_buf[BUF_SIZE]; char tmp[BUF_SIZE]; int total = 0; int i = 0; ssize_t ret; memset(resp, 0, resp_len); if (sock_fd < 0) { printf("❌ 未连接服务器,请先连接!\n"); return -1; } // 指令必须带换行,匹配ESP32 readStringUntil('\n') snprintf(send_buf, sizeof(send_buf), "%s\n", cmd); if (write(sock_fd, send_buf, strlen(send_buf)) < 0) { perror("send fail"); close(sock_fd); sock_fd = -1; return -1; } // 循环读取,直到收到 \n 代表一条应答结束 while (1) { ret = read(sock_fd, tmp, sizeof(tmp)-1); if (ret <= 0) { printf("❌ 读取应答失败 / 超时 / 服务器断开\n"); close(sock_fd); sock_fd = -1; return -1; } for (i ; i < ret; i++) { if (total < resp_len - 1) { resp[total++] = tmp[i]; } // 遇到换行,本条响应接收完成,直接退出 if (tmp[i] == '\n') { resp[total] = '\0'; return 0; } } } } void show_menu(void) { printf("\n==================== MENU ====================\n"); printf(" 1 - 连接ESP32服务端\n"); printf(" 2 - 查询温湿度 (GET)\n"); printf(" 3 - WS2812 红色全亮 CMD1\n"); printf(" 4 - WS2812 绿色全亮 CMD2\n"); printf(" 5 - WS2812 黄色全亮 CMD3\n"); printf(" 6 - WS2812 开启跑马灯 CMD4\n"); printf(" 7 - WS2812 全部关灯 CMD5\n"); printf(" 0 - 退出程序\n"); printf("==============================================\n"); printf("请输入选项: "); } int main(void) { int choice; char response[BUF_SIZE]; while (1) { show_menu(); if (scanf("%d", &choice) != 1) { // 清空错误输入 while (getchar() != '\n'); printf("输入错误,请输入数字!\n"); continue; } switch (choice) { case 1: connect_server(); break; case 2: if (0 == send_command("GET", response, sizeof(response))) { printf("📩 服务器回复: %s", response); } break; case 3: if (0 == send_command("CMD1", response, sizeof(response))) { printf("📩 服务器回复: %s", response); } break; case 4: if (0 == send_command("CMD2", response, sizeof(response))) { printf("📩 服务器回复: %s", response); } break; case 5: if (0 == send_command("CMD3", response, sizeof(response))) { printf("📩 服务器回复: %s", response); } break; case 6: if (0 == send_command("CMD4", response, sizeof(response))) { printf("📩 服务器回复: %s", response); } break; case 7: if (0 == send_command("CMD5", response, sizeof(response))) { printf("📩 服务器回复: %s", response); } break; case 0: printf("程序退出\n"); if (sock_fd > 0) close(sock_fd); return 0; default: printf("无效选项,请重新选择\n"); break; } } }

4.3 Kindle8 编译与运行命令

  1. 修改代码内SERVER_IP为 ESP32 打印出的局域网 IP
  2. 编译,在 Linux 主机上使用交叉编译器进行编译,生成 kindle8 架构的执行程序
arm-linux-gnueabi-gcc -march=armv7-a -mtune=cortex-a9 -msoft-float -mabi=aapcs-linux tcp_client.c -o tcp_client
  1. 上传程序 ,ssh 登录到 kindle 主机,切换到/mnt/us/gcc-test 目录(gcc-test 要自行创建)
[root@kindle gcc-test]# scp tony@192.168.1.12:/home/tony/kindle-esp32/kindle-tcp ./ kindle-tcp 100% 13KB 2.2MB/s 00:00
  1. 运行
[root@kindle gcc-test]#./tcp_client

五、整体测试流程

  1. ESP32 上电,串口监视器查看 WiFi 连接状态,记录 IP 地址;
  2. 修改 Kindle 客户端代码内 ESP32 IP,编译;
  3. Kindle 运行客户端,选项【1】连接服务端;
  4. 依次调用功能:
    • 2 查询温湿度
    • 3~7 控制彩灯
  1. 重复多次切换,验证 TCP 连接不会断开、指令应答不乱序。

六、文件目录结构

ESP32_TCP_LIGHT_AHT20/ ├── doc/ │ └── 开发设计文档.md # 本文档 ├── esp32_server/ │ └── esp32_server.ino # ESP32 Arduino源码 ├── kindle_client/ │ ├── tcp_client.c │ └── Makefile # 可选,一键编译脚本 └── README.md # 使用说明、接线说明