简介 本文主要介绍如何使用Arduino内核作为编程架构在ESP32上创建Websocket server(服务器)。所创建的Websocket server(服务器)将作为回发服务器使用,也就是说它会把接收自客户端的数据回发给客户端。
为了对服务器进行测试,我们将使用Python开发一个非常简单的客户端。即便是对从没用过Python的人来说,这也是一种非常简单(也非常强大)的程序设计语言,因此以下代码肯定不难理解。
当然,如果你仍然不习惯使用Python,你也可以参考上一篇帖子使用另一个ESP32开发板上运行的ESP 32 Websocket客户端或者使用其他编程语言开发的Websocket API对代码进行测试。 测试使用的是一个集成esp - wroom -32模块的开发板:FireBeetle ESP32。代码开发是在MicroPython IDE uPyCraft上完成的。
Arduino库 需要注意的是,我在撰写本文时,上面提到的Websockets库尚未得到ESP32的官方支持(官方支持仅限ESP8266)。尽管如此,经过略微修改之后,仍可在ESP32上使用这个库。 下面所示的Arduino代码就是对库的例程进行修改所得到的,可以成功用在ESP32上。
[mw_shl_code=applescript,true]
#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[0] == 'd';
int pin = dataString[1] - '0';
int value;
value = dataString[2] - '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);
}
[/mw_shl_code] Python模块 我们还需要在Python上安装一个Websockets模块,同样可以使我们免于处理Websockets的底层实现。 因此,要安装上文提到的库,我们只需要在Windows命令行执行以下命令即可: [mw_shl_code=applescript,true]pip install websocket-client[/mw_shl_code] 请注意,根据您所使用的Python具体版本的不同,在发送命令之前可能需要导航到pip所在的文件夹。 Python代码 首先,导入刚刚安装的websocket client模块。此外,还需要导入time模块,以便在程序中加入延时。 然后,基于之前导入的模块,创建一个WebSocket类的对象实例。 [mw_shl_code=applescript,true]import websocket
import time[/mw_shl_code] 接下来,调用WebSocket对象的connect方法(使用服务器地址作为其输入参数)。 [mw_shl_code=applescript,true]ws = websocket.WebSocket()[/mw_shl_code] 请注意,由于这是一个websocket连接,我们需要将目标设为“ws://{serverIP}/“,其中服务器IP是ESP32连接到WiFi网络时所分配的地址。 代码中使用的是我的ESP32开发板的IP地址,在连接到我的家庭网络时我已经知道了具体的IP地址。但是,如果你还不知道你的IP地址,那么我们会在Arduino代码中将其值打印出来。 [mw_shl_code=applescript,true]ws.connect("ws://192.168.1.78/")[/mw_shl_code] 上述函数调用之后,我们就应该成功连接到了服务器上。请注意,为简便起见,我在代码中没有进行任何错误处理,但是在实际应用中对可能出现的错误进行处理非常重要。 接下来,为了向服务器发送数据,我们只需调用WebSocket对象的send方法即可(将包含数据内容的字符串作为输入参数)。 [mw_shl_code=applescript,true]ws.send("data to send")[/mw_shl_code] 为了从服务器接收数据,我们可以调用WebSocket对象的recv方法。该方法会从服务器返回可用的数据,我们应该把这些数据保存到变量中。
[mw_shl_code=applescript,true]
result = ws.recv()[/mw_shl_code] 最后,调用WebSocket对象的close方法即可从服务器断开。 [mw_shl_code=applescript,true]result = ws.close()
[/mw_shl_code] Python Websocket client的完整源代码如下所示。请注意,代码中使用循环发送和接收请求,并将从服务器接受到的数据打印出来,以确认服务器真的会将接受到的内容进行回发。 你可以修改nrOfMessages变量和sleep时间,以测试服务器在面对更多请求和更短间隔时的灵活性。 [mw_shl_code=applescript,true] 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()
[/mw_shl_code]Arduino代码 文件包含和全局变量 在Arduino代码中,首先要将一些库包含进来。我们将需要WiFi.h库(用于将ESP32连接到WiFi网络)和WebSocketServer.h库(负责提供设置websocket server所需的功能)。
[mw_shl_code=applescript,true] #include <WiFi.h>
#include <WebSocketServer.h>[/mw_shl_code]然后,我们需要一个WiFiServer类的对象,所以我们将创建一个TCP服务器并使其监听接受到的请求。websocket server(服务器)也将在这个服务器之上构造。[mw_shl_code=applescript,true] /*
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_ */
[/mw_shl_code]这个类的构造函数有一个可选参数,可指定服务器将要监听的端口号。尽管其默认值就是80,但是我们仍将显式地传递该输入参数,以说明其用途。 [mw_shl_code=applescript,true]WiFiServer server(80);[/mw_shl_code] [mw_shl_code=applescript,true]WebSocketServer webSocketServer;[/mw_shl_code] 在全局变量声明的最后,我们需要将WiFi网络名称(ssid)及其密码保存起来,以便稍后联网时使用。 [mw_shl_code=applescript,true]const char* ssid = "yourNetworkName";
const char* password = "yourNetworkPassword;
[/mw_shl_code]
setup函数 在setup函数中,首先需要打开一个串行连接,以输出程序运行结果。 然后,将ESP32连接到WiFi网络,并将分配给它的本地IP地址打印出来。这个IP地址正是在前文所述Python代码中使用的地址。 [mw_shl_code=applescript,true] 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());[/mw_shl_code]在setup函数的最后,通过调用WiFiServer对象的begin方法对TCP服务器进行初始化。该方法没有输入参数,返回值为空。 [mw_shl_code=applescript,true]server.begin();[/mw_shl_code] 完整的setup函数代码如下所示。
[mw_shl_code=applescript,true]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);
}
[/mw_shl_code] 主循环 在Arduino主循环函数中,我们将对客户端连接和数据交换进行处理。 首先,通过调用WiFiServer对象的available方法对客户端连接进行监听。该方法会返回一个WiFiClient类的对象。 请注意,我们目前仍然处于TCP客户端层次,尚未涉及websocket客户端。 [mw_shl_code=applescript,true]WiFiClient client = server.available();[/mw_shl_code] 接下来,为了确认TCP客户端是否已连接,需要在先前返回的WiFiClient对象上调用connected方法,如果客户端已连接,则该方法返回值为true,否则返回值为false。 我们还需要调用WebSocketServer对象(该对象是我们在代码一开始所声明的一个全局变量)的handshake方法,其输入参数是我们的WiFiClient对象,这个handshake方法负责在底层完成协议握手。 Handshake方法会返回一个Boolean(布尔)值,表示握手是否成功,在进一步与客户端进行实际通信之前,应该对这个返回值进行验证(以确保握手成功)。 [mw_shl_code=applescript,true]if (client.connected() && webSocketServer.handshake(client)) {
// Communication with the client
}[/mw_shl_code] 鉴于我们将使用非常简单的websocket回发服务器,因此只需要一个数据缓冲区,用于存放首次接收的客户端数据。在接下来的代码中,所有方法使用的参数都是字符串类型,与我们的缓冲区数据类型一致。 [mw_shl_code=applescript,true]String data;[/mw_shl_code] 客户端随时可能断开,因为我们将使用一个while循环,只要客户端仍处于连接状态,这个循环就会持续运行。 在循环的每次迭代之间,需要加入一小段延时。这一点非常重要,如果没有延时,那么在收到最开始的几个字节之后,代码将会停止从客户端接收数据。 到目前为止,条件判断部分的代码如下所示。[mw_shl_code=applescript,true]
if (client.connected() && webSocketServer.handshake(client)) {
String data;
while (client.connected()) {
// Handle communication
delay(10); // Delay needed for receiving the data correctly
}
}[/mw_shl_code]在循环内,我们通过webSocketServer对象的getData方法接收数据。该方法不需要输入参数,返回String(字符串)输出,我们会把这个返回值赋值给之前定义的数据缓冲区。 [mw_shl_code=applescript,true]data = webSocketServer.getData();[/mw_shl_code] 在接收到数据之后,我们会将其打印到串口,并回发给客户端。 通过调用WebSocketServer对象的sendData方法可以将数据发送给客户端。该方法接收String(字符串)输入(该字符串的内容就是要发送给客户端的数据),返回值为空。 客户端有可能不发送任何数据,所以我们要先确认数据缓冲区的长度,并通过条件判断确定是否执行上述方法调用。数据缓冲区的长度应该大于0,这样我们才会把数据回发给客户端。 [mw_shl_code=applescript,true] if (data.length() > 0) {
Serial.println(data);
webSocketServer.sendData(data);
}
[/mw_shl_code]
完整的Arduino循环函数如下所示。请注意,我们增加了当客户端从服务器断开时打印相关消息的额外代码,位于循环内判断客户端是否连接的相关代码之后。 [mw_shl_code=applescript,true] 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);
}
[/mw_shl_code]
最终的Arduino代码 最终的Arduino代码如下所示。您可以直接复制粘贴到自己的Arduino环境内。不要忘了在全局变量中改为你自己的WiFi网络认证信息。 [mw_shl_code=applescript,true]
#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);
}
[/mw_shl_code] 测试代码 使用你的Arduino IDE对代码进行编译并上传到ESP32开发板,即可对代码进行测试。然后,打开serial monitor(使用Serial.Begin函数中定义的波特率)。 连接到WiFi网络时,控制台会打印出ESP32的本地IP。Python程序中执行connect方法时使用的就是这个IP。 在Python程序中写入正确的IP并且连接ESP32之后,运行Python程序。发送到ESP32的消息全部被回发并打印到了Python控制台,如图1所示。 图1 - Python客户端发送的消息被回发时的程序运行截图。
在Arduino IDE serial monitor上,您将看到所打印的相同消息,如图2所示。 图2 - ESP32接收到的消息。
在所有消息发送完之后,Python客户端会从服务器断开。Arduino循环会检测到客户端已断开,因此在控制台上会打印出一条消息,说明客户端已经断开,如图3所示。 图3 - 客户端已断开。
注:本文作者是Nuno Santos,他是一位和蔼可亲的电子和计算机工程师,住在葡萄牙里斯本 (Lisbon)。 他写了200多篇有关ESP32、ESP8266的有用的教程和项目。
查看更多ESP32/ESP8266教程和项目,请点击 : ESP32教程汇总贴
|