缘由以及遇到的坑
项目需要一个上位机接收车体数据进行分析,如果采用串口方案,测试时会比较麻烦,而设计硬件的时候就配了一块 ESP32-S3,所以就用 wifi+TCP 来传输数据,数据量不算大,使用 TCP 带来的时延可以接受。
下位机向与上位机建立 TCP 连接需要先知道上位机的 IP 地址,因此基本思路为上位机通过 UDP 广播一个只有上位机与下位机知道的验证数据包,下位机接收到数据包后验证数据包的数据,验证通过后解析出数据包的源地址并回复上位机,最后建立 TCP 连接。
思路很简单,但还是拖了我两三天时间,问题出在了上位机的 UDP 广播地址上......
最开始我选择的是受限广播地址(255.255.255.255)。网络环境为电脑(即上位机)、ESP32-S3 接入手机热点。码好代码后发现 ESP32 接收不到上位机广播的数据包。
排查过程如下:
- 在电脑上用 netcat 监听目的端口,发现在本机上可以收到上位机发送的数据
- 用 netcat 向 ESP32 的 IP+端口发送,ESP32 可以接收到数据
- 网上有帖说要把 ESP32 设置为接收广播的模式,但测试发现还是不行
- 把上位机发送地址换成 ESP32 的 IP 地址,测试成功,说明是发送地址的问题
- 把发送地址换成直接广播地址(xxx. xxx. xxx. 255),成功
可问题是,使用受限广播地址理论上可以让手机热点下的所有设备接收到该数据包,毕竟是在同一网络下。猜测可能的原因为小米手机热点本身的限制(不广播以受限广播地址为目的地址的数据包),也有可能是安卓手机本身的限制,手头没有其他品牌或苹果手机,就没办法进一步测试原因了。
实现代码
上位机
上位机我用的是 winform,因为开发起来比较简单快捷。主体代码参考自:C#.net编写ESP8266 WIFI网络通信上位机 - 知乎,有较大幅度修改。
这个上位机不是完全体,发送数据、导出数据、用图表显示数据还没实现
界面如下,我用标号标出了控件对应代码中的名称。
using System;
using System.Net;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Net.Sockets;
using System.Threading;
using System.Text.RegularExpressions;
using System.Net.NetworkInformation;
namespace TwoWheeledSelfBalancingCar_host
{
public partial class Form1 : Form
{
//监听套接字
Socket socket_listen = null;
//通信套接字
Socket socket_connected = null;
//本机IP
string hostIP = null;
//带参委托,用于在线程中操作控件
private delegate void SetText(string text);
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
//获取本机的IP地址
System.Net.IPAddress[] addresses = Dns.GetHostAddresses(Dns.GetHostName());
//将获取到的IP地址显示在文本框中
foreach(IPAddress ip in addresses)
{
IP_maskedTextBox.Text = ip.ToString();
hostIP = ip.ToString();
}
port_maskedTextBox.Text = "8266";
}
//开启通信按钮按下时执行
private void Start_conn_button_Click(object sender, EventArgs e)
{
//判断端口号是否合法(1~99999),不严谨,有时间再改
Regex reg = new Regex(@"^[1-9]\d{0,4}$");
if (port_maskedTextBox.Text == "" || !reg.IsMatch(port_maskedTextBox.Text.Trim()))
{
func_ShowConn("端口号不合法");
return;
}
//判断端口号是否被占用
IPGlobalProperties ipProperties = IPGlobalProperties.GetIPGlobalProperties();
IPEndPoint[] iPEndPoints = ipProperties.GetActiveTcpListeners();
int port;
int.TryParse(port_maskedTextBox.Text,out port);
foreach(IPEndPoint endPoint1 in iPEndPoints)
{
if(endPoint1.Port == port)
{
func_ShowConn("端口已连接或被占用");
return;
}
}
func_ShowConn("开始监听");
//创建套接字实例,ipv4,数据流,TCP协议
socket_listen = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//创建网络终结点,包含IP地址和端口
IPEndPoint endPoint = new IPEndPoint(IPAddress.Parse(IP_maskedTextBox.Text.Trim()),
int.Parse(port_maskedTextBox.Text.Trim()));
//绑定网络终结点
socket_listen.Bind(endPoint);
//开始监听,监听队列长度为10
socket_listen.Listen(10);
//创建一个线程,用udp向下位机发送本机IP地址
Thread thread_CreateConn = new Thread(func_CreateConn);
thread_CreateConn.IsBackground = true;
thread_CreateConn.Start();
}
//向下位机广播,告知上位机IP地址
private void func_CreateConn()
{
//绑定本地端口
UdpClient udpClient = new UdpClient(8268);
//直接广播地址,目标端口
IPEndPoint endPoint_Send = new IPEndPoint(IPAddress.Parse(GetBroadcast(hostIP)), 8267);
//验证信息
byte[] sendBytes = Encoding.ASCII.GetBytes("TWSBC");
//接收回复用的网络终结点,即目的IP和端口
IPEndPoint endPoint_Receive = new IPEndPoint(IPAddress.Any,8267);
//每秒向下位机发送验证数据,直至收到预期回复
while (true)
{
//发送验证信息,下位机会解析出数据报的源IP,即上位机IP
udpClient.Send(sendBytes, sendBytes.Length, endPoint_Send);
func_ShowConn("已发送本机IP,等待下位机接入中...");
//接收下位机回复
Byte[] receiveBytes = udpClient.Receive(ref endPoint_Receive);
string reply = Encoding.ASCII.GetString(receiveBytes).Trim();
//判断是否是预期回复
if (reply == "OK")
{
func_ShowConn("下位机已回复");
//创建一个后台线程,监听客户端接入
Thread thread_Listen = new Thread(func_ListenConn);
thread_Listen.IsBackground = true;
thread_Listen.Start();
//退出、结束本线程
break;
}
Thread.Sleep(1000);
}
}
//监听客户端接入
private void func_ListenConn()
{
while (true)
{
socket_connected = socket_listen.Accept();
func_ShowConn("客户端" + socket_connected.RemoteEndPoint.ToString() + "已连接");
Thread thread2 = new Thread(new ParameterizedThreadStart(func_ReceiveMsg));
thread2.IsBackground = true;
thread2.Start(socket_connected);
}
}
//显示连接信息
private void func_ShowConn(string str)
{
if(show_textBox.InvokeRequired)
{
SetText d = new SetText(func_ShowConn);
this.Invoke(d, new object[] { str });
}
else
{
//显示连接信息及时间
show_textBox.AppendText(DateTime.Now.ToString("hh:mm:ss") + ":" + str + "\r\n");
}
}
//显示接收到的数据
private void func_ShowReceive(string str)
{
if (receive_textBox.InvokeRequired)
{
SetText d = new SetText(func_ShowReceive);
this.Invoke(d, new object[] { str });
}
else
{
receive_textBox.AppendText(DateTime.Now.ToString("hh:mm:ss") + ":" + str + "\r\n");
}
}
//接收小车发来的数据
private void func_ReceiveMsg(object socketClientPara)
{
Socket socket = socketClientPara as Socket;
while (true)
{
//创建一个内存缓冲区 其大小为1024*1024字节 即1M
byte[] arrServerRecMsg = new byte[1024 * 1024 * 1];
//将接收到的信息存入到内存缓冲区,并返回其字节数组的长度
int length = socket.Receive(arrServerRecMsg);
//把接收到的字节数组转成字符串strSRecMsg
string strSRecMsg = Encoding.UTF8.GetString(arrServerRecMsg, 0, length);
func_ShowReceive(strSRecMsg);
}
}
private void IP_maskedTextBox_MaskInputRejected(object sender, MaskInputRejectedEventArgs e)
{
}
//清空连接情况文本框
private void Clr_show_button_Click(object sender, EventArgs e)
{
show_textBox.Clear();
}
//清空接收文本框
private void Clr_receive_button_Click(object sender, EventArgs e)
{
receive_textBox.Clear();
}
//获取直接广播地址
private string GetBroadcast(string ip_str)
{
string[] subIPs = ip_str.Split('.');
string subnetMask = subIPs[0] + "." + subIPs[1] + "." + subIPs[2] + "." + "255";
return subnetMask;
}
}
}
ESP32-S3
主体代码参考自乐鑫官方的两个例程: \protocols\sockets\udp_server
和 \wifi\getting_started\station
,其他见注释
#include "../inc/communication.h"
#include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"
#include "esp_netif.h"
#include "esp_event.h"
#include "esp_wifi.h"
#include "lwip/err.h"
#include "lwip/sockets.h"
#define WIFI_CONNECTED_BIT BIT0
#define WIFI_FAIL_BIT BIT1
#define CONN_KEY "TWSBC"
static EventGroupHandle_t wifi_event_group;
static int retry_num = 0;
//上位机IP
char tcp_server_ip[128];
//TCP通信端口
int tcp_server_port = 8266;
//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_STA_START)
{
esp_wifi_connect();
}else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED)
{
if (retry_num < MAXIMUM_RETRY)
{
esp_wifi_connect();
retry_num++;
}
else
{
//超过了最大连接次数还连不上,则置位事件组的WIFI_FAIL_BIT标志
xEventGroupSetBits(wifi_event_group, WIFI_FAIL_BIT);
}
}else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP)
{
//重置重试次数
retry_num = 0;
//成功获取了IP,置位事件组中的WIFI_CONNECTED_BIT标志
xEventGroupSetBits(wifi_event_group, WIFI_CONNECTED_BIT);
}
}
void communication_task(void *pvParameter){
int addr_family = 0;
int ip_protocol = 0;
while (1){
struct sockaddr_in dest_addr;
dest_addr.sin_addr.s_addr = inet_addr(tcp_server_ip);
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(tcp_server_port);
addr_family = AF_INET;
ip_protocol = IPPROTO_IP;
//创建套接字
int sock = socket(addr_family,SOCK_STREAM,ip_protocol);
if(sock < 0){
//套接字创建失败
break;
}
//开始连接服务器
int err = connect(sock,(struct sockaddr*)&dest_addr, sizeof(struct sockaddr_in6));
if(err != 0){
//连接失败
break;
}
static const char *str = "Hello from ESP32-S3";
while (1) {
int err = send(sock,str, strlen(str),0);
if(err < 0 ){
//发送时出错
break;
}
vTaskDelay(2000/portTICK_PERIOD_MS);
}
if(sock != -1){
shutdown(sock,0);
close(sock);
}
}
vTaskDelete(NULL);
}
void udp_task(void *pvParameter){
//创建套接字
int udp_sock = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
if(udp_sock < 0)
return;
//绑定本地地址和端口
struct sockaddr_in local_addr;
local_addr.sin_family = AF_INET;
local_addr.sin_addr.s_addr = INADDR_ANY;
local_addr.sin_port = htons(8267);
if(bind(udp_sock,(struct sockaddr*)&local_addr, sizeof(local_addr)) < 0)
return;
while (1){
//远程端口和地址
struct sockaddr_storage remote_addr;
socklen_t socklen = sizeof(remote_addr);
//接收缓冲区
char buffer[128];
//接收上位机信息,收到之前会一直阻塞
int rev_len = recvfrom(udp_sock,buffer, sizeof(buffer),0,(struct sockaddr*)&remote_addr,&socklen);
//接收成功
if(rev_len > 0){
//添加字符串结束符
buffer[rev_len] = 0;
//确定是否是上位机发送的数据报
if(!strcmp(buffer,CONN_KEY)){
//解析上位机IP,并保存为字符串
if(remote_addr.ss_family == PF_INET){
inet_ntoa_r(((struct sockaddr_in*)&remote_addr)->sin_addr,tcp_server_ip, sizeof(tcp_server_ip)-1);
}else if(remote_addr.ss_family == PF_INET6){
inet6_ntoa_r(((struct sockaddr_in6*)&remote_addr)->sin6_addr,tcp_server_ip, sizeof(tcp_server_ip)-1);
}
//发送回复
char reply[2] = {'O','K'};
sendto(udp_sock,reply, 2,0,(struct sockaddr*)&remote_addr, sizeof(remote_addr));
vTaskDelay(1000/portTICK_PERIOD_MS);
//开始建立TCP链接
xTaskCreate(communication_task,"comm_task",4096,NULL,5,NULL);
//退出本任务,并删除本任务
break;
}
}
}
vTaskDelete(NULL);
}
void communication_init(){
wifi_event_group = xEventGroupCreate();
//初始化TCP/IP堆栈
esp_netif_init();
//创建默认事件循环
esp_event_loop_create_default();
//创建默认WIFI STA
esp_netif_create_default_wifi_sta();
//按默认参数初始化wifi
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
esp_wifi_init(&cfg);
esp_event_handler_register(WIFI_EVENT,ESP_EVENT_ANY_ID,&wifi_event_handler,NULL);
//拿到IP时调用事件处理函数
esp_event_handler_register(IP_EVENT,IP_EVENT_STA_GOT_IP,&wifi_event_handler,NULL);
//配置WIFI,包括密码、wifi名
wifi_config_t wifi_cfg = {
.sta = {
.ssid = WIFI_SSID,
.password = WIFI_PASSWORD,
.threshold.authmode = WIFI_AUTH_WPA2_PSK,
.pmf_cfg = {
.capable = true,
.required = false
}
}
};
esp_wifi_set_mode(WIFI_MODE_STA);
esp_wifi_set_config(ESP_IF_WIFI_STA,&wifi_cfg);
esp_wifi_start();
EventBits_t bits = xEventGroupWaitBits(wifi_event_group,
WIFI_CONNECTED_BIT|WIFI_FAIL_BIT,
pdFALSE,
pdFALSE,
portMAX_DELAY);
if(bits & WIFI_CONNECTED_BIT){
//连接成功,通过udp接收上位机IP,创建TCP连接
xTaskCreate(&udp_task,"udp_task",2048,NULL,5,NULL);
} else if(bits & WIFI_FAIL_BIT){
//连接失败
} else{
}
esp_event_handler_unregister(IP_EVENT,IP_EVENT_STA_GOT_IP,&wifi_event_handler);
esp_event_handler_unregister(WIFI_EVENT,ESP_EVENT_ANY_ID,&wifi_event_handler);
vEventGroupDelete(wifi_event_group);
}
运行效果
ESP32 通电后,点击开启通信,结果如下: