ESP32 Arduino教程:Websocket server(服务器)
简介本文主要介绍如何使用Arduino内核作为编程架构在ESP32上创建Websocket server(服务器)。所创建的Websocket server(服务器)将作为回发服务器使用,也就是说它会把接收自客户端的数据回发给客户端。
为了对服务器进行测试,我们将使用Python开发一个非常简单的客户端。即便是对从没用过Python的人来说,这也是一种非常简单(也非常强大)的程序设计语言,因此以下代码肯定不难理解。
当然,如果你仍然不习惯使用Python,你也可以参考上一篇帖子使用另一个ESP32开发板上运行的ESP 32 Websocket客户端或者使用其他编程语言开发的Websocket API对代码进行测试。测试使用的是一个集成esp - wroom -32模块的开发板:FireBeetle ESP32。代码开发是在MicroPython IDE uPyCraft上完成的。
Arduino库在ESP32上需要安装一个Websockets库,这样就不需要从头编写底层代码了。我们将使用这个基于WiFiServer的库(https://github.com/morrissinger/ESP8266-Websocket )来创建一个TCP服务器(这也是Arduino内核的常见做法)。需要注意的是,我在撰写本文时,上面提到的Websockets库尚未得到ESP32的官方支持(官方支持仅限ESP8266)。尽管如此,经过略微修改之后,仍可在ESP32上使用这个库。在此前的这篇教程中:ESP32 Arduino教程:Websocket客户端 已经详细介绍了库的安装过程。请参阅此篇教程,因为在使用库之前有几点技巧需要考虑。下面所示的Arduino代码就是对库的例程进行修改所得到的,可以成功用在ESP32上。
#include
#include
#include
// Enabe debug tracing to Serial port.
#define DEBUGGING
// Here we define a maximum framelength to 64 bytes. Default is 256.
#define MAX_FRAME_LENGTH 64
// Define how many callback functions you have. Default is 1.
#define CALLBACK_FUNCTIONS 1
#include
WiFlyServer server(80);
WebSocketServer webSocketServer;
// Called when a new message from the WebSocket is received
// Looks for a message in this form:
//
// DPV
//
// Where:
// D is either 'd' or 'a' - digital or analog
// P is a pin #
// V is the value to apply to the pin
//
void handleClientData(String &dataString) {
bool isDigital = dataString == 'd';
int pin = dataString - '0';
int value;
value = dataString - '0';
pinMode(pin, OUTPUT);
if (isDigital) {
digitalWrite(pin, value);
} else {
analogWrite(pin, value);
}
Serial.println(dataString);
}
// send the client the analog value of a pin
void sendClientData(int pin) {
String data = "a";
pinMode(pin, INPUT);
data += String(pin) + String(analogRead(pin));
webSocketServer.sendData(data);
}
void setup() {
Serial.begin(9600);
SC16IS750.begin();
WiFly.setUart(&SC16IS750);
WiFly.begin();
// This is for an unsecured network
// For a WPA1/2 network use auth 3, and in another command send 'set wlan phrase PASSWORD'
// For a WEP network use auth 2, and in another command send 'set wlan key KEY'
WiFly.sendCommand(F("set wlan auth 1"));
WiFly.sendCommand(F("set wlan channel 0"));
WiFly.sendCommand(F("set ip dhcp 1"));
server.begin();
Serial.println(F("Joining WiFi network..."));
// Here is where you set the network name to join
if (!WiFly.sendCommand(F("join arduino_wifi"), "Associated!", 20000, false)) {
Serial.println(F("Association failed."));
while (1) {
// Hang on failure.
}
}
if (!WiFly.waitForResponse("DHCP in", 10000)) {
Serial.println(F("DHCP failed."));
while (1) {
// Hang on failure.
}
}
// This is how you get the local IP as an IPAddress object
Serial.println(WiFly.localIP());
// This delay is needed to let the WiFly respond properly
delay(100);
}
void loop() {
String data;
WiFlyClient client = server.available();
if (client.connected() && webSocketServer.handshake(client)) {
while (client.connected()) {
data = webSocketServer.getData();
if (data.length() > 0) {
handleClientData(data);
}
sendClientData(1);
sendClientData(2);
sendClientData(3);
}
}
// wait to fully let the client disconnect
delay(100);
}
Python模块我们还需要在Python上安装一个Websockets模块,同样可以使我们免于处理Websockets的底层实现。我们将使用一个叫做websocket-client的Python模块。幸运的是,Python自带了一个叫做pip的软件包安装程序,可以大大简化模块的安装过程。因此,要安装上文提到的库,我们只需要在Windows命令行执行以下命令即可:pip install websocket-client请注意,根据您所使用的Python具体版本的不同,在发送命令之前可能需要导航到pip所在的文件夹。Python代码首先,导入刚刚安装的websocket client模块。此外,还需要导入time模块,以便在程序中加入延时。然后,基于之前导入的模块,创建一个WebSocket类的对象实例。import websocket
import time接下来,调用WebSocket对象的connect方法(使用服务器地址作为其输入参数)。ws = websocket.WebSocket()请注意,由于这是一个websocket连接,我们需要将目标设为“ws://{serverIP}/“,其中服务器IP是ESP32连接到WiFi网络时所分配的地址。代码中使用的是我的ESP32开发板的IP地址,在连接到我的家庭网络时我已经知道了具体的IP地址。但是,如果你还不知道你的IP地址,那么我们会在Arduino代码中将其值打印出来。ws.connect("ws://192.168.1.78/")上述函数调用之后,我们就应该成功连接到了服务器上。请注意,为简便起见,我在代码中没有进行任何错误处理,但是在实际应用中对可能出现的错误进行处理非常重要。接下来,为了向服务器发送数据,我们只需调用WebSocket对象的send方法即可(将包含数据内容的字符串作为输入参数)。ws.send("data to send")为了从服务器接收数据,我们可以调用WebSocket对象的recv方法。该方法会从服务器返回可用的数据,我们应该把这些数据保存到变量中。
result = ws.recv()最后,调用WebSocket对象的close方法即可从服务器断开。result = ws.close()
Python Websocket client的完整源代码如下所示。请注意,代码中使用循环发送和接收请求,并将从服务器接受到的数据打印出来,以确认服务器真的会将接受到的内容进行回发。你可以修改nrOfMessages变量和sleep时间,以测试服务器在面对更多请求和更短间隔时的灵活性。import websocket
import time
ws = websocket.WebSocket()
ws.connect("ws://192.168.1.78/")
i = 0
nrOfMessages = 200
while i<nrOfMessages:
ws.send("message nr: " + str(i))
result = ws.recv()
print(result)
i=i+1
time.sleep(1)
ws.close()
Arduino代码文件包含和全局变量在Arduino代码中,首先要将一些库包含进来。我们将需要WiFi.h库(用于将ESP32连接到WiFi网络)和WebSocketServer.h库(负责提供设置websocket server所需的功能)。
#include <WiFi.h>
#include <WebSocketServer.h>然后,我们需要一个WiFiServer类的对象,所以我们将创建一个TCP服务器并使其监听接受到的请求。websocket server(服务器)也将在这个服务器之上构造。 /*
Server.h - Server class for Raspberry Pi
Copyright (c) 2016 Hristo Gochkov All right reserved.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef _WIFISERVER_H_
#define _WIFISERVER_H_
#include "Arduino.h"
#include "Server.h"
#include "WiFiClient.h"
class WiFiServer : public Server {
private:
int sockfd;
int _accepted_sockfd = -1;
uint16_t _port;
uint8_t _max_clients;
bool _listening;
bool _noDelay = false;
public:
void listenOnLocalhost(){}
WiFiServer(uint16_t port=80, uint8_t max_clients=4):sockfd(-1),_accepted_sockfd(-1),_port(port),_max_clients(max_clients),_listening(false),_noDelay(false){}
~WiFiServer(){ end();}
WiFiClient available();
WiFiClient accept(){return available();}
void begin(uint16_t port=0);
void setNoDelay(bool nodelay);
bool getNoDelay();
bool hasClient();
size_t write(const uint8_t *data, size_t len);
size_t write(uint8_t data){
return write(&data, 1);
}
using Print::write;
void end();
void close();
void stop();
operator bool(){return _listening;}
int setTimeout(uint32_t seconds);
void stopAll();
};
#endif /* _WIFISERVER_H_ */
这个类的构造函数有一个可选参数,可指定服务器将要监听的端口号。尽管其默认值就是80,但是我们仍将显式地传递该输入参数,以说明其用途。WiFiServer server(80);我们还需要一个WebSocketServer类的对象,它负责提供从客户端接收请求以及对数据交换进行处理时所需要的方法。WebSocketServer webSocketServer;在全局变量声明的最后,我们需要将WiFi网络名称(ssid)及其密码保存起来,以便稍后联网时使用。const char* ssid = "yourNetworkName";
const char* password = "yourNetworkPassword;
setup函数在setup函数中,首先需要打开一个串行连接,以输出程序运行结果。然后,将ESP32连接到WiFi网络,并将分配给它的本地IP地址打印出来。这个IP地址正是在前文所述Python代码中使用的地址。有关如何将ESP32连接到WiFi网络的详细说明,请参阅此前的这篇帖子:ESP32 MicroPython教程:连接Wi-Fi网络。Serial.begin(115200);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.println("Connecting to WiFi..");
}
Serial.println("Connected to the WiFi network");
Serial.println(WiFi.localIP());在setup函数的最后,通过调用WiFiServer对象的begin方法对TCP服务器进行初始化。该方法没有输入参数,返回值为空。server.begin();完整的setup函数代码如下所示。
void setup() {
Serial.begin(115200);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.println("Connecting to WiFi..");
}
Serial.println("Connected to the WiFi network");
Serial.println(WiFi.localIP());
server.begin();
delay(100);
}
主循环在Arduino主循环函数中,我们将对客户端连接和数据交换进行处理。首先,通过调用WiFiServer对象的available方法对客户端连接进行监听。该方法会返回一个WiFiClient类的对象。请注意,我们目前仍然处于TCP客户端层次,尚未涉及websocket客户端。WiFiClient client = server.available();接下来,为了确认TCP客户端是否已连接,需要在先前返回的WiFiClient对象上调用connected方法,如果客户端已连接,则该方法返回值为true,否则返回值为false。我们还需要调用WebSocketServer对象(该对象是我们在代码一开始所声明的一个全局变量)的handshake方法,其输入参数是我们的WiFiClient对象,这个handshake方法负责在底层完成协议握手。Handshake方法会返回一个Boolean(布尔)值,表示握手是否成功,在进一步与客户端进行实际通信之前,应该对这个返回值进行验证(以确保握手成功)。if (client.connected() && webSocketServer.handshake(client)) {
// Communication with the client
}鉴于我们将使用非常简单的websocket回发服务器,因此只需要一个数据缓冲区,用于存放首次接收的客户端数据。在接下来的代码中,所有方法使用的参数都是字符串类型,与我们的缓冲区数据类型一致。String data;客户端随时可能断开,因为我们将使用一个while循环,只要客户端仍处于连接状态,这个循环就会持续运行。在循环的每次迭代之间,需要加入一小段延时。这一点非常重要,如果没有延时,那么在收到最开始的几个字节之后,代码将会停止从客户端接收数据。到目前为止,条件判断部分的代码如下所示。
if (client.connected() && webSocketServer.handshake(client)) {
String data;
while (client.connected()) {
// Handle communication
delay(10); // Delay needed for receiving the data correctly
}
}在循环内,我们通过webSocketServer对象的getData方法接收数据。该方法不需要输入参数,返回String(字符串)输出,我们会把这个返回值赋值给之前定义的数据缓冲区。data = webSocketServer.getData();在接收到数据之后,我们会将其打印到串口,并回发给客户端。通过调用WebSocketServer对象的sendData方法可以将数据发送给客户端。该方法接收String(字符串)输入(该字符串的内容就是要发送给客户端的数据),返回值为空。客户端有可能不发送任何数据,所以我们要先确认数据缓冲区的长度,并通过条件判断确定是否执行上述方法调用。数据缓冲区的长度应该大于0,这样我们才会把数据回发给客户端。if (data.length() > 0) {
Serial.println(data);
webSocketServer.sendData(data);
}
完整的Arduino循环函数如下所示。请注意,我们增加了当客户端从服务器断开时打印相关消息的额外代码,位于循环内判断客户端是否连接的相关代码之后。void loop() {
WiFiClient client = server.available();
if (client.connected() && webSocketServer.handshake(client)) {
String data;
while (client.connected()) {
data = webSocketServer.getData();
if (data.length() > 0) {
Serial.println(data);
webSocketServer.sendData(data);
}
delay(10); // Delay needed for receiving the data correctly
}
Serial.println("The client disconnected");
delay(100);
}
delay(100);
}
最终的Arduino代码最终的Arduino代码如下所示。您可以直接复制粘贴到自己的Arduino环境内。不要忘了在全局变量中改为你自己的WiFi网络认证信息。
#include <WiFi.h>
#include <WebSocketServer.h>
WiFiServer server(80);
WebSocketServer webSocketServer;
const char* ssid = "yourNetworkName";
const char* password ="yourNetworkPassword;
void setup() {
Serial.begin(115200);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.println("Connecting to WiFi..");
}
Serial.println("Connected to the WiFi network");
Serial.println(WiFi.localIP());
server.begin();
delay(100);
}
void loop() {
WiFiClient client = server.available();
if (client.connected() && webSocketServer.handshake(client)) {
String data;
while (client.connected()) {
data = webSocketServer.getData();
if (data.length() > 0) {
Serial.println(data);
webSocketServer.sendData(data);
}
delay(10); // Delay needed for receiving the data correctly
}
Serial.println("The client disconnected");
delay(100);
}
delay(100);
}
测试代码使用你的Arduino IDE对代码进行编译并上传到ESP32开发板,即可对代码进行测试。然后,打开serial monitor(使用Serial.Begin函数中定义的波特率)。连接到WiFi网络时,控制台会打印出ESP32的本地IP。Python程序中执行connect方法时使用的就是这个IP。在Python程序中写入正确的IP并且连接ESP32之后,运行Python程序。发送到ESP32的消息全部被回发并打印到了Python控制台,如图1所示。http://mc.dfrobot.com.cn/data/attachment/forum/201904/08/181936eqqctqiaa3q37eww.png图1 - Python客户端发送的消息被回发时的程序运行截图。
在Arduino IDE serial monitor上,您将看到所打印的相同消息,如图2所示。http://mc.dfrobot.com.cn/data/attachment/forum/201904/08/181936yceg05kkmcp5z08s.png图2 - ESP32接收到的消息。
在所有消息发送完之后,Python客户端会从服务器断开。Arduino循环会检测到客户端已断开,因此在控制台上会打印出一条消息,说明客户端已经断开,如图3所示。http://mc.dfrobot.com.cn/data/attachment/forum/201904/08/181936wo3ucc0cnfffzntz.png图3 - 客户端已断开。
注:本文作者是Nuno Santos,他是一位和蔼可亲的电子和计算机工程师,住在葡萄牙里斯本 (Lisbon)。他写了200多篇有关ESP32、ESP8266的有用的教程和项目。
查看更多ESP32/ESP8266教程和项目,请点击 : ESP32教程汇总贴英文版教程 : ESP32 tutorial
页:
[1]