嵌入式 | 基于ESP32实现的串口服务器

十念  金牌会员 | 2024-8-16 04:12:26 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 567|帖子 567|积分 1701

手上有一块ESP32板子,这段时间以来组装装备为了更便携一些,上位机与装备、装备与装备间通讯均用到了串口服务器,但是成品串口服务器总有接口限定,传输上线限定等问题,同时也没法做到须要的多口模数转换等功能因此自己摸索了一下,模数转换功能还没做。使用了Arduino情况,ESP_dev_module库。实当代码在地址:xiangxu05/ESP32_WIFI_Serial: 基于ESP32实现的串口服务器
0x00 WIFI设置

设置ESP32的WIFI,主要用到的就是<WiFi.h>中的方法,以下对使用到的方法进行简朴先容。WiFi.h头文件中界说了一个WiFiClass类,并存在一个界说了的全局变量extern WiFiClass WiFi。这个类继承了WiFiGenericClass、WiFiSTAClass、WiFiScanClass、WiFiAPClass,因此可以使用这几个类的方法,设置一个APSTA模式的WiFi便使用了此中的方法。
使用前记得包罗该库。
  1. #include <WiFi.h>
复制代码

  • 设置WIFI模式
    1. static bool mode(wifi_mode_t);
    2. @param wifi_mode_t           WIFI模式,模式列表:
    3.         WIFI_OFF             不作定义
    4.         WIFI_STA             定义为STA模式,相当于无线终端,不接受无线的接入
    5.         WIFI_AP              定义为AP模式,提供无线接入服务,允许其它设备通过WIFI接入
    6.         WIFI_AP_STA          定义为STA和AP共存模式
    7. @return                     成功返回1,失败返回0
    复制代码
    要使用APSTA模式,因此须要将WIFI设置为WIFI_AP_STA:
    1. WiFi.mode(WIFI_AP_STA);
    复制代码
  • WIFI-AP设置
    1. bool softAP(const char* ssid, const char* passphrase = NULL, int channel = 1, int ssid_hidden = 0, int max_connection = 4, bool ftm_responder = false);
    2. @param ssid                         指向SSID字符串的指针(最大63字节)。
    3. @param passphrase                   可选,默认 = NULL,用于WPA2加密的WIFI密码,最少8字节,开放WIFI可以用NULL。
    4. @param channel                      可选,默认 = 1,WIFI频道号码,1-13。
    5. @param ssid_hidden                  可选,默认 = 0,网络隐藏(0 = 广播SSID,1 = 隐藏SSID)。
    6. @param max_connection               可选,默认 = 4, 最大同时连接的客户端, 1 - 4。
    7. @ftm_responder                      可选,默认 = false,一种高速传输模式可以d在高带宽且低延迟的情况下与另一个 ESP32 设备进行通信。
    8. @return                             成功返回true,不成功返回false
    复制代码
    可以通过这个方法来对ESP32的AP参数进行设置。
    1. WiFi.softAP(ssid_name, security_key, 3, 1);
    复制代码
    接着还须要对AP的内部IP、掩码、网关进行设置。
    1. bool softAPConfig(IPAddress local_ip, IPAddress gateway, IPAddress subnet, IPAddress dhcp_lease_start = (uint32_t) 0);
    2. @param local_ip                配置AP的IP地址
    3. @param gateway                 配置AP的网关IP地址
    4. @param subnet                  配置AP的子网掩码
    5. @dhcp_lease_start              配置AP的DHCP租约开始
    6. @return                        成功返回true,失败返回false
    复制代码
    此时仅对AP的local_ip、gateway、subnet进行设置。
    1. WiFi.softAPConfig(lan_ip, lan_gateway, lan_subnet);
    复制代码
  • WIFI-STA设置
    1. wl_status_t begin(const char* wpa2_ssid, wpa2_auth_method_t method, const char* wpa2_identity=NULL, const char* wpa2_username=NULL, const char *wpa2_password=NULL, const char* ca_pem=NULL, const char* client_crt=NULL, const char* client_key=NULL, int32_t channel=0, const uint8_t* bssid=0, bool connect=true);
    2. @param wpa2_ssid                                用于连接的WiFi网络的SSID。
    3. @param method                                        使用的WPA2认证方法。
    4. @param wpa2_identity                        可选,用于WPA2 Enterprise认证的身份。
    5. @param wpa2_username                        可选,用于WPA2 Enterprise认证的用户名。
    6. @param wpa2_password                        可选,用于WPA2 Enterprise认证的密码。
    7. @param ca_pem                                        可选,CA证书,用于验证服务器的证书。
    8. @param client_crt                                可选,客户端证书。
    9. @param client_key                                可选,客户端私钥。
    10. @param channel                                        可选,指定要连接的WiFi信道,默认为0(自动选择)。
    11. @param bssid                                        可选,指定要连接的接入点的BSSID(MAC地址)。
    12. @param connect                                        可选,是否立即连接到指定的网络,默认为true。
    13. @return                                                        返回连接状态,类型为 wl_status_t。
    14. wl_status_t begin(const char* ssid, const char *passphrase = NULL, int32_t channel = 0, const uint8_t* bssid = NULL, bool connect = true);
    15. @param ssid                                                用于连接的WiFi网络的SSID。
    16. @param passphrase                                可选,用于连接WiFi网络的密码。
    17. @param channel                                        可选,指定要连接的WiFi信道,默认为0(自动选择)。
    18. @param bssid                                        可选,指定要连接的接入点的BSSID(MAC地址)。
    19. @param connect                                        可选,是否立即连接到指定的网络,默认为true。
    20. @return                                                        返回连接状态,类型为 wl_status_t。
    21. wl_status_t begin(char* ssid, char *passphrase = NULL, int32_t channel = 0, const uint8_t* bssid = NULL, bool connect = true);
    22. @param ssid                                                用于连接的WiFi网络的SSID。
    23. @param passphrase                                可选,用于连接WiFi网络的密码。
    24. @param channel                                        可选,指定要连接的WiFi信道,默认为0(自动选择)。
    25. @param bssid                                        可选,指定要连接的接入点的BSSID(MAC地址)。
    26. @param connect                                        可选,是否立即连接到指定的网络,默认为true。
    27. @return                                                        返回连接状态,类型为 wl_status_t。
    28. wl_status_t begin();                        无参数,该方法尝试连接到上一次配置的WiFi网络。
    29. @return                                                        返回连接状态,类型为 wl_status_t。
    复制代码
    此时,仅传入对应的ssid与password就行。
    1. WiFi.begin(ssid, password);
    复制代码
    至此,可以写一个设置函数如下:
    1. int WIFI_SET_APSTA(String ssid_name,String security_key,String ssid,String password,String local_ip,String subnet,String gateway){ long iRetval = -1; //标志是否设置乐成,这边仅对AP启动判断,因为AP模式关乎到是否能够进行连接设置。 WiFi.mode(WIFI_AP_STA);
    2. //设置为AP_STA模式 IPAddress lan_gateway,lan_subnet,lan_ip; lan_ip.fromString(local_ip); lan_gateway.fromString(gateway); lan_subnet.fromString(subnet); //这边IPAddress的界说也可以用如下方法,参数间用逗号隔开 //IPAddress local_IP(192,168,1,1); //IPAddress subnet(255,255,255,0); //IPAddress gateway(192,168,1,1);     WiFi.softAPConfig(lan_ip, lan_gateway, lan_subnet);
    3. WiFi.softAP(ssid_name, security_key, 3, 1);
    4. //对AP进行设置并启动     iRetval = WiFi.softAP(ssid_name, security_key);     //输出提示 if (iRetval) {​    Serial.println("Soft AP started successfully.");​    Serial.print("SSID: ");​    Serial.println(ssid_name);​    Serial.print("IP Address: ");​    Serial.println(WiFi.softAPIP());  } else {​    Serial.println("Failed to start Soft AP.");​    return WIFIAPUNBUILED;  }  // 连接到WiFi网络  WiFi.begin(ssid, password);
    5.   Serial.println("Connecting to WiFi");  while (WiFi.status() != WL_CONNECTED) {​    Serial.print(".");​    delay(500);  }  Serial.println();  Serial.print("WiFi connected. Local IP address: ");  Serial.println(WiFi.localIP());  return iRetval;}
    复制代码
  • WiFiServer
    要作为TCP Server使用,就须要使用到WiFiServer类,因此我们须要首先界说一个该对象。
    1. WiFiServer server;
    复制代码
    首先启动该对象。
    1. void begin(uint16_t port=0);
    2. @param port                                                可选,服务器监听的端口号,默认为0。如果设置为0,服务器将选择一个默认端口。
    3. @return                                                        无返回值。
    4. void begin(uint16_t port, int reuse_enable);
    5. @param port                                                服务器监听的端口号。
    6. @param  reuse_enable                        是否启用端口重用,通常用于服务器在重启时可以立即绑定到同一端口。非零值表示启用,零值表示禁用。
    7. @return                                                        无返回值。
    复制代码
    简朴的,此时仅传入一个开放端口。
    1. server.begin((uint16_t)server_port.toInt());
    复制代码
    该对象有一个方法会用到。
    1. WiFiClient available();
    2. @return                                                        返回一个WiFiClient类型
    复制代码
  • WiFiClient
    使用到了三个方法。
    1. int available();
    2. @return                                                        可用返回1,不可用返回0
    3. uint8_t connected();
    4. @return                                                        返回 1 表示当前 WiFiClient 对象与服务器或客户端保持连接。返回 0 表示连接已经断开。
    5. int read();
    6. @return                                                        返回一个收到的数据,不能收到数返回-1。
    复制代码
    至此,可以实现一个监听串口与TCP端口的方法。
    1. static void TCP_Server_user()
    2. {
    3. String RxBuff;
    4. String txBuff;
    5. if(connected == 0){
    6.   Serial.println("\nwaiting for connect...");
    7.   connected = 1;
    8. }
    9. client=server.available();
    10. if(client)
    11. {
    12.   Serial.println("get client,welcome!");
    13.   
    14.   while(client.connected()||client.available()||Serial.available())
    15.   {
    16.    if(client.available())
    17.    {
    18. ​    while(client.available()){
    19. ​     char c=client.read();
    20. ​     RxBuff +=c;
    21. ​    }
    22. ​    if(RxBuff == "AT_DigitalUp"){
    23. ​     digitalWrite(18, HIGH);
    24. ​     digitalWrite(19, HIGH);
    25. ​    }else if(RxBuff == "AT_DigitalDown"){//这部分是额外的设备控制,可以删掉。
    26. ​     digitalWrite(18, LOW);
    27. ​     digitalWrite(19, LOW);
    28. ​    }else{
    29. ​     Serial.print(RxBuff);
    30. ​    }
    31. ​    RxBuff="";
    32.    }
    33.    if(Serial.available()){
    34. ​    while(Serial.available()){
    35. ​     char c=Serial.read();
    36. ​     txBuff+=c;
    37. ​    }
    38. ​    if(txBuff == "AT_DigitalUp"){
    39. ​     digitalWrite(18, HIGH);
    40. ​     digitalWrite(19, HIGH);
    41. ​    }else if(txBuff == "AT_DigitalDown"){
    42. ​     digitalWrite(18, LOW);
    43. ​     digitalWrite(19, LOW);
    44. ​    }
    45. ​    else{
    46. ​     client.print(txBuff);
    47. ​    }
    48. ​    txBuff="";
    49.    }
    50.   }
    51.   client.stop();
    52.   connected = 0;
    53.   Serial.println("no clinet now.");
    54. }
    55. }
    复制代码
0x01 NVS非易失寄存器

对于传统的单片机来说我们假如要固化保存小批量的数据的话通常会使用EEPROM,在Arduino core for the ESP32中也有相关的功能。不过对于ESP32来说官方还提供了一种叫做 Preferences 的功能,这个功能也可以用来固化保存数据,并且使用上比EEPROM更加方便。
为了实现对WIFI以及TCP监听端口设置,因此须要一个网页来进行,同时网页将更改存储于NVS的参数。ESP32官方在Flash上创建了一个叫做nvs的分区,而Preferences功能就是创建在该分区上的。Arduino core for the ESP32中默认分区( Partition Scheme: “Default 4MB with spiffs (1.2MB APP /1.5MB SPIFFS)” )情况下nvs分区的大小为 20480 字节,实际可存放的数据大小要小于这个值( 单个数据来说最大为496K或者97%的nvs分区大小 )。
Preferences中数据以键值对(key - value)的方式存储。在键值对之上另有一层定名空间(namespace),不同定名空间中可以有雷同的键名存在。在Preferences中定名空间和键名均为字符串,并且长度不大于15个字节。
使用前记得包罗该库。
  1. #include <Preferences.h>
复制代码
Preferences支持多种存储的数据类型,为了统一格式因此我们均使用String类型存储,功能实现用到了以下几个方法。
  1. bool begin(const char *name, bool readOnly);
  2. @name: 用于标识命名空间的字符串。这个命名空间将包含一组相关的键值对。
  3. @readOnly: 如果设置为 `true`,则以只读模式打开首选项;如果设置为 `false`,则以读写模式打开首选项。
  4. @return: 成功返回 `true`,失败返回 `false`。
  5. void clear();
  6. 无参数。这个方法会清除当前命名空间中的所有键值对。
  7. @return: 无返回值。
  8. void end();
  9. 无参数。这个方法会结束对当前命名空间的操作,释放相关资源。
  10. @return: 无返回值。
  11. String getString(const char* key, const String& defaultValue = String());
  12. @key: 要读取的字符串键。
  13. @defaultValue: (可选)如果指定的键不存在,返回的默认字符串值。
  14. @return: 返回与键关联的字符串值。如果键不存在,则返回 `defaultValue`。
  15. bool putString(const char* key, const String& value);
  16. @key: 要写入的字符串键。
  17. @value: 要写入的字符串值。
  18. @return: 成功返回 `true`,失败返回 `false`。
复制代码
因此在单片机启动时,做了如下操作以实现默认值启动WIFI。
  1. Preferences preferences; //全局变量,不在setup()中
  2. // 打开 NVS
  3. preferences.begin("settings", false);
  4. String ssid_name = preferences.getString("SSID_NAME", "ESP_TCPserverV1");
  5. String security_key = preferences.getString("SECURITY_KEY", "12345678.");
  6. String sta_ssid = preferences.getString("ssid", "znlhsys_2.4G");
  7. String sta_password = preferences.getString("password", "znlhsysbistu");
  8. String local_ip = preferences.getString("LOCAL_IP", "192.168.1.1");
  9. String subnet = preferences.getString("SUBNET", "255.255.255.0");
  10. String gateway = preferences.getString("GATEWAY", "192.168.1.1");
  11. String server_port = preferences.getString("SERVERPORT", "8899");
  12. // 关闭 NVS
  13. preferences.end();
  14. WIFI_SET_APSTA(ssid_name,security_key,sta_ssid,sta_password,local_ip,subnet,gateway)
复制代码
后续仅须要修改这些键值对,即可在装备重启时修改WIFI参数。
0x02 WebServer

ESP32提供了一个轻便使用的WebServer,包罗于头文件<WebServer.h>中,因此使用时记得包罗库。
  1. #include <WebServer.h>
复制代码
该对象存在以下几个方法供使用。
  1. WebServer webserver(uint16_t port);
  2. @port: 用于初始化 `WebServer` 对象的端口号。服务器将在该端口上监听HTTP请求。
  3. @return: 无返回值(这是构造函数,用于创建 `WebServer` 对象)。
  4. void on(const Uri &uri, THandlerFunction handler);
  5. @uri: 用于匹配请求路径的字符串。例如,`"/"` 表示根路径。
  6. @handler: 与该路径关联的处理函数。当该路径收到HTTP请求时,将调用此函数。
  7. @return: 无返回值。
  8. void onNotFound(THandlerFunction fn);
  9. @fn: 处理未找到页面的函数。当请求的路径未被任何处理函数匹配时,将调用此函数。
  10. @return: 无返回值。
  11. void begin();
  12. 无参数。这个方法启动Web服务器,开始监听传入的HTTP请求。
  13. @return: 无返回值。
  14. void handleClient();
  15. 无参数。这个方法处理传入的客户端请求。应在 `loop()` 函数中周期性地调用,以确保服务器能够响应客户端请求。
  16. @return: 无返回值。
  17. void send(int code, const char* content_type, const String& content);
  18. @code: HTTP状态码(例如,200表示成功,404表示未找到)。
  19. @content_type: 响应内容的MIME类型(例如,`"text/html"`,`"application/json"`)。
  20. @content: 要发送的响应内容。
  21. @return: 无返回值。
复制代码
特别的是该对象返回给Client的页面以String类型输入,因此须要将HTML文件以字符串类型界说。
实现的思路则是,首先界说一个根路径,处置处罚逻辑是访问时读取NVS存储器中的参数,并返回页面。提交时转到/submit页面,将发送来的参数存入存储器,并重新指向根目次。
因此对WebServer设置。
  1. webserver.on("/", handleRoot);
  2. webserver.on("/submit", handleSubmit);
  3. webserver.onNotFound([](){webserver.send(200,"text/html;charset=utf-8","没有找到页面!");});
  4. webserver.begin();
复制代码
以下是处置处罚函数:
  1. void handleRoot(){
  2. preferences.begin("settings", false);
  3. String sta_ssid = preferences.getString("ssid", "znlhsys_2.4G");
  4. String sta_password = preferences.getString("password", "znlhsysbistu");
  5. String ssid_name = preferences.getString("SSID_NAME", "ESP_TCPserverV1");
  6. String security_key = preferences.getString("SECURITY_KEY", "12345678.");
  7. String local_ip = preferences.getString("LOCAL_IP", "192.168.1.1");
  8. String subnet = preferences.getString("SUBNET", "255.255.255.0");
  9. String gateway = preferences.getString("GATEWAY", "192.168.1.1");
  10. String server_port = preferences.getString("SERVERPORT", "8899");
  11. preferences.end();
  12. // 格式化HTML页面
  13. const int bufferSize = 4096;
  14. char html[bufferSize];
  15. // 使用 snprintf 格式化HTML内容
  16. snprintf(html, bufferSize, root_html,
  17. ​      sta_ssid.c_str(), sta_password.c_str(), ssid_name.c_str(), security_key.c_str(),
  18. ​      local_ip.c_str(), subnet.c_str(), gateway.c_str(), server_port.c_str());
  19. webserver.send(200, "text/html", html);
  20. }
  21. void handleSubmit() {
  22. if (webserver.hasArg("ssid") && webserver.hasArg("password") && webserver.hasArg("apSSID") &&
  23.    webserver.hasArg("apPassword") && webserver.hasArg("localIP") && webserver.hasArg("subnetMask") &&
  24.    webserver.hasArg("gateWay") && webserver.hasArg("tcpPort")) {
  25.   
  26.   preferences.begin("settings", false);
  27.   // 存储每个参数到Preferences
  28.   preferences.putString("ssid", webserver.arg("ssid"));
  29.   preferences.putString("password", webserver.arg("password"));
  30.   preferences.putString("SSID_NAME", webserver.arg("apSSID"));
  31.   preferences.putString("SECURITY_KEY", webserver.arg("apPassword"));
  32.   preferences.putString("LOCAL_IP", webserver.arg("localIP"));
  33.   preferences.putString("SUBNET", webserver.arg("subnetMask"));
  34.   preferences.putString("GATEWAY", webserver.arg("gateWay"));
  35.   preferences.putString("SERVERPORT", webserver.arg("tcpPort"));
  36.   preferences.end();
  37.   // 重定向回根页面
  38.   webserver.sendHeader("Location", "/");
  39.   webserver.send(303);
  40. } else {
  41.   webserver.send(400, "text/plain", "Missing required parameters");
  42. }
  43. }
复制代码
0x03 循环

为了实现既能应对串口、TCP端口消息,又能办理网页的显示,因此在loop函数中分别调用了两个handle。
  1. void loop()
  2. {
  3.         TCP_Server_user();
  4.           webserver.handleClient();
  5. }
复制代码
0x04 小结

以上,就实现了一个简朴的串口服务器,同时这个方法还存在如下问题:


  • 对于串口通讯来说,还少了一个设置通讯速率的参数,实现的串口端口也较少。
  • 多端口对多TCP连接的逻辑还没实现。
  • 直接在loop函数中使用两个handle,在这个逻辑里会导致TCP连接后,网页不响应的问题。
  • 两个功能在同一个方法中实现,效率不高,可以引入freertos,这个本来也要做的,在代码中也可以看到相关注释,临时先这样后续再完善。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

十念

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表