介绍
测试使用的是一个集成esp - wroom -32模块的开发板:FireBeetle ESP32。代码开发是在MicroPython IDE uPyCraft上完成的。 本文旨在介绍如何在ESP32上运行的Websocket服务器上接收和解析JSON消息。我们将以Arduino内核作为编程框架。 文中对Websocket服务器的操作代码将会以先前的教程内容为基础。本教程介绍了如何安装Arduino库文件以及Python模块,其中Arduino库文件是操作Websocket服务器所必需的文件,而Python模块是测试本文中创建的客户端所必需的模块。 另外,此教程还介绍了JSON解析功能,叙述了如何安装Arduino代码所需的JSON解析库文件。
Python客户端代码 在代码的开始部分,我们将导入websocket模块,以便能够访问连接ESP32 Websocket服务器所需的所有函数。
[mw_shl_code=applescript,true]1 import websocket[/mw_shl_code]
我们还将导入JSON模块,以便能够将Python字典(Python语言的基本数据结构之一)转换为JSON字符串。
[mw_shl_code=applescript,true]1 import json[/mw_shl_code]
接下来,我们将创建一个WebSocket类对象,利用它连接服务器并与服务器交换数据的方法。
[mw_shl_code=applescript,true]1 ws = websocket.WebSocket()[/mw_shl_code]
为了连接ESP32 websocket服务器,我们调用了该对象的connect方法,将目标服务器作为其传递参数,输入格式为“ws:// {ESP32 IP} /”,其中,{ESP32 IP} 表示WiFi网络中分配给ESP32的本地IP地址。 请注意,我们将在Arduino代码中打印出ESP32在网络中的IP地址,所以现在您可以先虚拟一个IP地址并在稍后进行更改。
[mw_shl_code=applescript,true]
1 ws.connect("ws://192.168.1.78/")[/mw_shl_code]
现在,我们将创建一个Python字典,它的某些“键-值”对用于表示传感器的测量值。请注意,这只是一个用作测试目的的数据结构,在实际应用中,ESP32可以充当网关,它可以从底层传感器接收数据,对数据进行处理并将处理结果发送到云端。
[mw_shl_code=applescript,true]myDict = {"sensor": "temperature", "identifier":"SENS123456789", "value":10, "timestamp": "20/10/2017 10:10:25"}[/mw_shl_code]
然后,我们将通过调用WebSocket对象的send方法把数据发送至Websocket服务器。此方法的输入参数是我们需要发送的数据。 在此示例中,需要发送的数据为JSON字符串,它用于表示先前声明的Python字典。为了将字典转换为JSON字符串,我们需要调用JSON模块的dumps函数,将字典数据作为该函数的输入参数。
[mw_shl_code=applescript,true]1 ws.send(json.dumps(myDict))[/mw_shl_code]
在此之后,我们将通过调用WebSocker对象的recv方法来获取服务器响应结果并将其打印出来。
[mw_shl_code=applescript,true]result = ws.recv()
print(result)[/mw_shl_code]
最后,我们将通过调用该对象的close方法来关闭服务器连接。
[mw_shl_code=applescript,true]ws.close()[/mw_shl_code]
完整的Python代码如下所示。
[mw_shl_code=applescript,true]
import websocket
import json
ws = websocket.WebSocket()
ws.connect("ws://192.168.1.78/")
myDict = {"sensor": "temperature", "identifier":"SENS123456789", "value":10, "timestamp": "20/10/2017 10:10:25"}
ws.send(json.dumps(myDict))
result = ws.recv()
print(result)
ws.close()[/mw_shl_code]
Arduino代码 引用的库文件及全局变量
按照惯例,我们将从引用一些库文件开始编写Arduino代码。在本教程中,我们需要使用以下库文件: WiFi.h:将ESP32连接到WiFi网络所需的库文件,它用于实现从websocket客户端访问服务器。 WebSocketServer.h:设置Websocket服务器以及与客户端交换数据所需的库文件。 ArduinoJson.h:解析和访问客户端发送的JSON字符串数据所需的库文件。
[mw_shl_code=applescript,true]#include <WiFi.h>
#include <WebSocketServer.h>
#include <ArduinoJson.h>[/mw_shl_code]
为了运行Websocket服务器,我们需要声明一个WiFiServer类对象,利用它来设置TCP服务器。然后,在这个TCP服务器上运行Websocket服务器。 WiFiServer对象的构造函数以TCP服务器的端口编号为输入参数,利用该端口侦听客户端连接状况。我们将使用端口80,即默认的HTTP端口。 为了使用所有与Websocket协议相关的函数,我们还需要一个WebSocketServer类对象。
[mw_shl_code=applescript,true]WiFiServer server(80);
WebSocketServer webSocketServer;[/mw_shl_code]
最后,我们需要一些变量来存储待连接WiFi网络的证书。请注意,我们可以将WiFi网络证书直接作为参数传递给WiFi网络连接函数,但在此处声明变量可以让代码更清晰,更易于维护。
[mw_shl_code=applescript,true]const char* ssid = "yourNetworkName";
const char* password = "yourNetworkPassword";[/mw_shl_code]
客户端消息处理函数 我们需要解析来自客户端的JSON消息,为此,我们可以创建一个专用函数。通过这种方式,我们就可以将代码封装起来,不需要在Arduino主循环函数中穿插大量的逻辑代码,从而使整个代码非常清晰整洁。 处理客户端数据的函数将以待解析的String字符串数据为输入参数。请注意,之前“如何在ESP32上设置Websockets服务器”的教程中已强调,当从客户端接收数据时,数据类型必须是String字符串。
[mw_shl_code=applescript,true]void handleReceivedMessage(String message){
// message handling code
}[/mw_shl_code]
现在,为了解析此消息,我们首先需要声明一个StaticJsonBuffer类对象。JSON库文件将把此对象作为存储JSON对象树的预分配内存池。 我们需要指定此内存池的大小,这可以通过模板参数工具完成。通过使用此工具,我们可以根据JSON的数据结构更精确地计算出所需的缓冲区大小。在这个简单的示例中,我们把此内存池的大小设置为500,对于待解析的数据结构而言,这个大小已经足够了。
[mw_shl_code=applescript,true]StaticJsonBuffer<500> JSONBuffer; //Memory pool[/mw_shl_code]
接下来,为了把待解析的JSON消息作为函数的输入参数,我们将调用刚刚创建的StaticJsonBuffer对象的parseObject方法。 此方法将待解析的消息作为输入参数,并返回一个JsonObject类引用对象。我们将使用这个对象来访问已解析的值,但在此之前,首先要检查解析过程是否已成功完成。 为此,我们只需在刚刚获得的JsonObject引用对象上调用success方法即可。此方法没有任何输入参数,它的返回值为一个布尔值,用于指示解析成功(true)或失败(false)。 如果解析失败,那么函数执行过程将会结束,因为我们无法访问此消息的任何值。
[mw_shl_code=applescript,true]JsonObject& parsed = JSONBuffer.parseObject(message); //Parse message
if (!parsed.success()) { //Check for errors in parsing
Serial.println("Parsing failed");
return;
}[/mw_shl_code]
如果解析成功,我们就可以访问消息中的值了。在此案例中,我们可以简单地从JSON数据结构中获取每个变量并将它们打印到串口控制台。 为了访问已解析JSON消息中的每个值,我们只需简单地使用下标运算符(方括号)以及带有JSON对象中键名称的字符串即可。 请注意,我们已经从Python代码中了解了JSON消息的数据结构,因此我们知道数据结构中的每个键,可以轻松地将每个变量映射到合适的数据类型。
[mw_shl_code=applescript,true]const char * sensorType = parsed["sensor"]; //Get sensor type value
const char * sensorIdentifier = parsed["identifier"]; //Get sensor type value
const char * timestamp = parsed["timestamp"]; //Get timestamp
int value = parsed["value"]; //Get value of sensor measurement
[/mw_shl_code]
在获取每个值后,我们将以一种友好的格式将其打印到串行端口。消息处理函数的完整代码如下所示,其中包含了这些打印。
[mw_shl_code=applescript,true]
void handleReceivedMessage(String message){
StaticJsonBuffer<500> JSONBuffer; //Memory pool
JsonObject& parsed = JSONBuffer.parseObject(message); //Parse message
if (!parsed.success()) { //Check for errors in parsing
Serial.println("Parsing failed");
return;
}
const char * sensorType = parsed["sensor"]; //Get sensor type value
const char * sensorIdentifier = parsed["identifier"]; //Get sensor type value
const char * timestamp = parsed["timestamp"]; //Get timestamp
int value = parsed["value"]; //Get value of sensor measurement
Serial.println();
Serial.println("----- NEW DATA FROM CLIENT ----");
Serial.print("Sensor type: ");
Serial.println(sensorType);
Serial.print("Sensor identifier: ");
Serial.println(sensorIdentifier);
Serial.print("Timestamp: ");
Serial.println(timestamp);
Serial.print("Sensor value: ");
Serial.println(value);
Serial.println("------------------------------");
}[/mw_shl_code]
Setup设置函数 我们的Arduino setup设置函数用于执行初始化任务。首先,我们将建立一个串行连接以打印程序的输出结果。 接下来,我们将把ESP32连接到WiFi网络。我们将使用之前教程中多次使用的相同代码,您可以点击这里查看每个函数的详细说明。 请注意,在建立网络连接之后,我们将把分配给ESP32的本地IP地址打印出来,这个IP地址就是我们应该在Python代码Websocket connect方法中使用的IP地址。 在完成WiFi网络连接之后,我们将通过调用程序开头声明的WiFiServer全局对象的begin方法来初始化TCP服务器。此函数没有任何输入参数,其返回值为空(void)。 完整的setup函数参数代码如下所示,其中包含了之前提到的所有初始化内容。
[mw_shl_code=applescript,true]void setup() {
Serial.begin(115200);
delay(2000);
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主循环中完成。 我们首先需要检查是否存在可用的客户端。为此,我们将调用已全局声明并已通过setup函数初始化的WiFiServer对象的可用方法。 正如之前教程中所述,我们仍然在TCP层级执行任务,只有在该层级我们才能在Websockets协议层中执行任务。
这些可调用的方法没有任何输入参数,其返回值为WiFiClient类对象。返回的对象将以变量的形式存储,随后,我们需要调用connected方法,该方法将返回一个布尔值,用于指示客户端是否连接成功。 如果客户端已连接,我们将需要执行Websocket handshake握手过程,它是协议的初始部分。为此,我们只需简单地调用程序开头声明的WebSocketServer对象的handshake方法即可。 Handshake方法的输入参数为之前获得的WiFiClient对象,在程序执行时,利用此WiFiClient对象与客户端通信。如果握手过程正确完成,则此方法调用的返回值为true,否则返回false。 因此,如果handshake方法返回true,我们就可以尝试与客户端交换数据了。
[mw_shl_code=applescript,true]
WiFiClient client = server.available();
if (client.connected() && webSocketServer.handshake(client)) {
// Handling data exchange with the Websocket client
}[/mw_shl_code]
在先前的条件程序块内,我们将声明一个String缓冲区来保存从客户端接收的数据,然后在客户端连接状态下执行循环操作。我们可以通过再次调用WiFiClient对象的connected方法来检查客户端是否仍然处于连接状态,将其返回值作为while循环的条件。 然后,在循环中,我们将通过调用WebSocketServer对象的getData方法来获取客户端发送的数据。此方法没有任何输入参数,其返回值是一个包含了客户端发送数据的String数据。 如果此数据大小大于零(客户端可能没有发送任何数据),我们将调用先前定义的消息处理函数对其进行解析。 仅出于测试目的,我们还将通过调用sendData方法将消息发送回客户端,从而可以在Python程序上将其打印出来。当然,在实际应用场景中,只需要返回一条用于指示所有内容是否已正确解析的消息。 完整的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) {
handleReceivedMessage(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]
最终完整代码 最终的ESP32 Arduino源代码如下所示。
[mw_shl_code=applescript,true]
#include <WiFi.h>
#include <WebSocketServer.h>
#include <ArduinoJson.h>
WiFiServer server(80);
WebSocketServer webSocketServer;
const char* ssid = "yourNetworkName";
const char* password = "yourNetworkPassword";
void handleReceivedMessage(String message){
StaticJsonBuffer<500> JSONBuffer; //Memory pool
JsonObject& parsed = JSONBuffer.parseObject(message); //Parse message
if (!parsed.success()) { //Check for errors in parsing
Serial.println("Parsing failed");
return;
}
const char * sensorType = parsed["sensor"]; //Get sensor type value
const char * sensorIdentifier = parsed["identifier"]; //Get sensor type value
const char * timestamp = parsed["timestamp"]; //Get timestamp
int value = parsed["value"]; //Get value of sensor measurement
Serial.println();
Serial.println("----- NEW DATA FROM CLIENT ----");
Serial.print("Sensor type: ");
Serial.println(sensorType);
Serial.print("Sensor identifier: ");
Serial.println(sensorIdentifier);
Serial.print("Timestamp: ");
Serial.println(timestamp);
Serial.print("Sensor value: ");
Serial.println(value);
Serial.println("------------------------------");
}
void setup() {
Serial.begin(115200);
delay(2000);
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) {
handleReceivedMessage(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代码并将其上传到ESP32。在完成此过程后,只需打开串口监视器并等待连接至WiFi网络。 网络连接完成后,ESP32的本地IP地址将会被打印至控制台。复制此IP地址并在connect方法的Python代码中使用。 随后,运行Python代码。此时您将获得类似于图1的输出结果,我们发送到服务器的JSON消息被返回并显示出来。
图1 - JSON消息回显在Python WebSocket客户端上
然后,在Arduino串口监视器上,您将得到类似于图2的结果,JSON消息的解析值被打印到控制台。
注:本文作者是Nuno Santos,他是一位和蔼可亲的电子和计算机工程师,住在葡萄牙里斯本 (Lisbon)。 他写了200多篇有关ESP32、ESP8266的有用的教程和项目。
查看更多ESP32/ESP8266教程和项目,请点击 : ESP32教程汇总贴
|