ESP32教程(Arduino Websocket服务器):接收和解析JSON内容

介绍

测试使用的是一个集成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服务器所需的所有函数。

1 import websocket

我们还将导入JSON模块,以便能够将Python字典(Python语言的基本数据结构之一)转换为JSON字符串。

1 import json

接下来,我们将创建一个WebSocket类对象,利用它连接服务器并与服务器交换数据的方法。

1 ws = websocket.WebSocket()

为了连接ESP32 websocket服务器,我们调用了该对象的connect方法,将目标服务器作为其传递参数,输入格式为“ws:// {ESP32 IP} /”,其中,{ESP32 IP} 表示WiFi网络中分配给ESP32的本地IP地址。
请注意,我们将在Arduino代码中打印出ESP32在网络中的IP地址,所以现在您可以先虚拟一个IP地址并在稍后进行更改。

1 ws.connect("ws://192.168.1.78/")

现在,我们将创建一个Python字典,它的某些“键-值”对用于表示传感器的测量值。请注意,这只是一个用作测试目的的数据结构,在实际应用中,ESP32可以充当网关,它可以从底层传感器接收数据,对数据进行处理并将处理结果发送到云端。

myDict = {"sensor": "temperature", "identifier":"SENS123456789", "value":10, "timestamp": "20/10/2017 10:10:25"}

然后,我们将通过调用WebSocket对象的send方法把数据发送至Websocket服务器。此方法的输入参数是我们需要发送的数据。
在此示例中,需要发送的数据为JSON字符串,它用于表示先前声明的Python字典。为了将字典转换为JSON字符串,我们需要调用JSON模块的dumps函数,将字典数据作为该函数的输入参数。

1 ws.send(json.dumps(myDict))

在此之后,我们将通过调用WebSocker对象的recv方法来获取服务器响应结果并将其打印出来。

result = ws.recv()
print(result)

最后,我们将通过调用该对象的close方法来关闭服务器连接。

ws.close()

完整的Python代码如下所示。

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()

Arduino代码

引用的库文件及全局变量
按照惯例,我们将从引用一些库文件开始编写Arduino代码。在本教程中,我们需要使用以下库文件:
WiFi.h:将ESP32连接到WiFi网络所需的库文件,它用于实现从websocket客户端访问服务器。
WebSocketServer.h:设置Websocket服务器以及与客户端交换数据所需的库文件。
ArduinoJson.h:解析和访问客户端发送的JSON字符串数据所需的库文件。

#include 
#include 
#include 

为了运行Websocket服务器,我们需要声明一个WiFiServer类对象,利用它来设置TCP服务器。然后,在这个TCP服务器上运行Websocket服务器。
WiFiServer对象的构造函数以TCP服务器的端口编号为输入参数,利用该端口侦听客户端连接状况。我们将使用端口80,即默认的HTTP端口。
为了使用所有与Websocket协议相关的函数,我们还需要一个WebSocketServer类对象。

WiFiServer server(80);
WebSocketServer webSocketServer;

最后,我们需要一些变量来存储待连接WiFi网络的证书。请注意,我们可以将WiFi网络证书直接作为参数传递给WiFi网络连接函数,但在此处声明变量可以让代码更清晰,更易于维护。

const char* ssid = "yourNetworkName";
const char* password = "yourNetworkPassword";

客户端消息处理函数
我们需要解析来自客户端的JSON消息,为此,我们可以创建一个专用函数。通过这种方式,我们就可以将代码封装起来,不需要在Arduino主循环函数中穿插大量的逻辑代码,从而使整个代码非常清晰整洁。
处理客户端数据的函数将以待解析的String字符串数据为输入参数。请注意,之前“如何在ESP32上设置Websockets服务器”的教程中已强调,当从客户端接收数据时,数据类型必须是String字符串。

void handleReceivedMessage(String message){
  // message handling code
}

现在,为了解析此消息,我们首先需要声明一个StaticJsonBuffer类对象。JSON库文件将把此对象作为存储JSON对象树的预分配内存池。
我们需要指定此内存池的大小,这可以通过模板参数工具完成。通过使用此工具,我们可以根据JSON的数据结构更精确地计算出所需的缓冲区大小。在这个简单的示例中,我们把此内存池的大小设置为500,对于待解析的数据结构而言,这个大小已经足够了。

StaticJsonBuffer<500> JSONBuffer; //Memory pool

接下来,为了把待解析的JSON消息作为函数的输入参数,我们将调用刚刚创建的StaticJsonBuffer对象的parseObject方法。
此方法将待解析的消息作为输入参数,并返回一个JsonObject类引用对象。我们将使用这个对象来访问已解析的值,但在此之前,首先要检查解析过程是否已成功完成。
为此,我们只需在刚刚获得的JsonObject引用对象上调用success方法即可。此方法没有任何输入参数,它的返回值为一个布尔值,用于指示解析成功(true)或失败(false)。
如果解析失败,那么函数执行过程将会结束,因为我们无法访问此消息的任何值。

JsonObject& parsed = JSONBuffer.parseObject(message); //Parse message
if (!parsed.success()) {   //Check for errors in parsing
    Serial.println("Parsing failed");
    return;
}

如果解析成功,我们就可以访问消息中的值了。在此案例中,我们可以简单地从JSON数据结构中获取每个变量并将它们打印到串口控制台。
为了访问已解析JSON消息中的每个值,我们只需简单地使用下标运算符(方括号)以及带有JSON对象中键名称的字符串即可。
请注意,我们已经从Python代码中了解了JSON消息的数据结构,因此我们知道数据结构中的每个键,可以轻松地将每个变量映射到合适的数据类型。

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

在获取每个值后,我们将以一种友好的格式将其打印到串行端口。消息处理函数的完整代码如下所示,其中包含了这些打印。

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("------------------------------");
}

Setup设置函数

我们的Arduino setup设置函数用于执行初始化任务。首先,我们将建立一个串行连接以打印程序的输出结果。
接下来,我们将把ESP32连接到WiFi网络。我们将使用之前教程中多次使用的相同代码,您可以点击这里查看每个函数的详细说明。
请注意,在建立网络连接之后,我们将把分配给ESP32的本地IP地址打印出来,这个IP地址就是我们应该在Python代码Websocket connect方法中使用的IP地址。
在完成WiFi网络连接之后,我们将通过调用程序开头声明的WiFiServer全局对象的begin方法来初始化TCP服务器。此函数没有任何输入参数,其返回值为空(void)。
完整的setup函数参数代码如下所示,其中包含了之前提到的所有初始化内容。

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);
}

主循环

在此示例的最后,我们需要创建用于处理客户端数据的代码,这些工作将在Arduino主循环中完成。
我们首先需要检查是否存在可用的客户端。为此,我们将调用已全局声明并已通过setup函数初始化的WiFiServer对象的可用方法。
正如之前教程中所述,我们仍然在TCP层级执行任务,只有在该层级我们才能在Websockets协议层中执行任务。

这些可调用的方法没有任何输入参数,其返回值为WiFiClient类对象。返回的对象将以变量的形式存储,随后,我们需要调用connected方法,该方法将返回一个布尔值,用于指示客户端是否连接成功。
如果客户端已连接,我们将需要执行Websocket handshake握手过程,它是协议的初始部分。为此,我们只需简单地调用程序开头声明的WebSocketServer对象的handshake方法即可。
Handshake方法的输入参数为之前获得的WiFiClient对象,在程序执行时,利用此WiFiClient对象与客户端通信。如果握手过程正确完成,则此方法调用的返回值为true,否则返回false。
因此,如果handshake方法返回true,我们就可以尝试与客户端交换数据了。

WiFiClient client = server.available();
if (client.connected() && webSocketServer.handshake(client)) {
   // Handling data exchange with the Websocket client
}

在先前的条件程序块内,我们将声明一个String缓冲区来保存从客户端接收的数据,然后在客户端连接状态下执行循环操作。我们可以通过再次调用WiFiClient对象的connected方法来检查客户端是否仍然处于连接状态,将其返回值作为while循环的条件。
然后,在循环中,我们将通过调用WebSocketServer对象的getData方法来获取客户端发送的数据。此方法没有任何输入参数,其返回值是一个包含了客户端发送数据的String数据。
如果此数据大小大于零(客户端可能没有发送任何数据),我们将调用先前定义的消息处理函数对其进行解析。
仅出于测试目的,我们还将通过调用sendData方法将消息发送回客户端,从而可以在Python程序上将其打印出来。当然,在实际应用场景中,只需要返回一条用于指示所有内容是否已正确解析的消息。
完整的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) {
         handleReceivedMessage(data);
         webSocketServer.sendData(data);
      }
      delay(10); // Delay needed for receiving the data correctly
   }
   Serial.println("The client disconnected");
   delay(100);
  }
  delay(100);
}

最终完整代码

最终的ESP32 Arduino源代码如下所示。

#include 
#include 
#include 
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);
}

测试代码

为了测试整个系统,我们首先编译了Arduino代码并将其上传到ESP32。在完成此过程后,只需打开串口监视器并等待连接至WiFi网络。
网络连接完成后,ESP32的本地IP地址将会被打印至控制台。复制此IP地址并在connect方法的Python代码中使用。
随后,运行Python代码。此时您将获得类似于图1的输出结果,我们发送到服务器的JSON消息被返回并显示出来。

ESP32教程(Arduino Websocket服务器):接收和解析JSON内容_第1张图片
图1 - JSON消息回显在Python WebSocket客户端上

然后,在Arduino串口监视器上,您将得到类似于图2的结果,JSON消息的解析值被打印到控制台。

ESP32教程(Arduino Websocket服务器):接收和解析JSON内容_第2张图片图2 - JSON消息的解析值

注:本文作者是Nuno Santos,他是一位和蔼可亲的电子和计算机工程师,住在葡萄牙里斯本 (Lisbon)。
他写了200多篇有关ESP32、ESP8266的有用的教程和项目。涉及arduino、micropython、 Picoweb、Espruino、Bluetooth、RFID、IDF……等等非常广泛,说是最全的完全不为过。

精华教程:

ESP32 MicroPython教程:uPyCraft IDE入门
ESP32 MicroPython教程:解析JSON
ESP32 MicroPython教程:MicroPython支持
ESP32 MicroPython教程:连接Wi-Fi网络
ESP32 / ESP8266 MicroPython教程:自动连接WiFi
ESP32 / ESP8266 MicroPython教程:从文件系统运行脚本
ESP32 / ESP8266 MicroPython教程:HTTP GET请求
ESP32 Arduino教程:用于构建ESP32编译环境的Arduino IDE软件
ESP32 Arduino教程:FreeRTOS队列性能测试
ESP32 RFID教程:打印MFRC522固件版本
ESP32 Picoweb教程:获取请求的HTTP方法
……

还有更多教程: ESP32教程 合集

英文版 :ESP32 tutorial合集

你可能感兴趣的:(ESP32,arduino)