본문 바로가기

ESP32

ESP32 Arduino Websocket 서버: JSON 콘텐츠 수신 및 파싱

반응형

 

 

ESP32 Arduino Websocket 서버: JSON 콘텐츠 수신 및 파싱

 

이 게시물의 목적은 ESP32에서 실행되는 Websocket 서버에서 JSON 메시지를 수신하고 파싱하는 방법을 설명하는 것입니다. Arduino 코어를 프로그래밍 프레임워크로 사용합니다. 이 ESP32 튜토리얼의 테스트는 ESP32 FireBeetle 보드에 통합된 DFRobot의 ESP-WROOM-32 장치를 사용하여 수행되었습니다.

 

소개

 

이 esp32 튜토리얼의 목적은 ESP32에서 실행되는 Websocket 서버에서 JSON 메시지를 수신하고 파싱하는 방법을 설명하는 것입니다. Arduino 코어를 프로그래밍 프레임워크로 사용합니다.

 

Websocket 측면에서 여기에 표시된 코드는 이전 튜토리얼을 기반으로 합니다. 이 튜토리얼은 Websocket 작업에 필요한 Arduino 라이브러리와 여기서 만들 테스트 클라이언트에 필요한 Python 모듈을 설치하는 방법을 설명합니다.

 

JSON 파싱 기능에 대해서는 이 튜토리얼을 확인할 수 있습니다. 또한 Arduino 코드에 필요한 JSON 파싱 라이브러리를 설치하는 방법도 설명합니다.

 

이 ESP32 튜토리얼의 테스트는 ESP32 FireBeetle 보드에 통합된 DFRobot의 ESP-WROOM-32 장치를 사용하여 수행되었습니다.

 

Python 클라이언트 코드

 

웹소켓 모듈을 임포트하여 코드를 시작하겠습니다. 그러면 ESP32 웹소켓 서버에 연결하는 데 필요한 모든 기능에 액세스할 수 있습니다.

 

import websocket

 

또한 json 모듈을 임포트하여 Python 사전(언어의 기본 구조 중 하나)을 JSON 문자열로 변환할 수 있습니다.

 

import json

 

다음으로, 서버에 연결하고 데이터를 교환하는 데 필요한 메서드가 있는 WebSocket 클래스의 객체를 만듭니다.

 

ws = websocket.WebSocket()

 

ESP32 웹소켓 서버에 연결하려면 이 객체에서 connect 메서드를 호출하여 대상 서버가 있는 문자열을 입

 

력으로 전달합니다. 형식은 "ws://{ESP32 IP}/"이며, {ESP32 IP}를 WiFi 네트워크에서 ESP32에 할당될 로컬 IP로 변경합니다.

 

Arduino 코드에서 네트워크에 있는 ESP32의 IP를 인쇄할 것이므로 지금은 더미 값을 남겨두고 나중에 변경할 수 있습니다.

 

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 서버로 데이터를 보냅니다. 이 메서드는 보내려는 데이터를 입력으로 받습니다.

 

우리의 경우, 데이터는 이전에 선언된 Python 사전의 JSON 문자열 표현이 될 것입니다. 사전을 JSON 문자열로 변환하려면 json 모듈의 dumps 함수를 호출하여 사전을 입력으로 전달합니다.

 

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 코드

 

라이브러리 include 및 전역 변수

 

평소처럼 Arduino 코드를 일부 라이브러리 include로 시작합니다. 이 튜토리얼에서는 다음이 필요합니다.

 

>> WiFi.h: ESP32를 WiFi 네트워크에 연결하여 웹소켓 클라이언트가 서버에 도달할 수 있도록 하는 데 필요한 라이브러리입니다.

 

>> WebSocketServer.h: Websocket 서버를 설정하고 클라이언트와의 데이터 교환을 처리하는 데 필요한 라이브러리입니다.

 

>> ArduinoJson.h: 클라이언트가 보낸 JSON 콘텐츠의 데이터를 구문 분석하고 액세스하는 데 필요한 라이브러리입니다.

 

#include <WiFi.h>
#include <WebSocketServer.h>
#include <ArduinoJson.h>

 

Websocket 서버를 실행하려면 TCP 서버를 설정하는 데 사용할 WiFiServer 클래스 객체를 선언해야 합니다. 그러면 Websocket 서버가 TCP 서버 위에서 작동합니다.

 

WiFiServer 객체의 생성자는 TCP 서버가 들어오는 클라이언트 연결을 수신할 포트 번호를 입력으로 받습니다. 기본 HTTP 포트인 포트 80을 사용합니다.

 

또한 모든 Websocket 프로토콜 관련 기능을 위한 WebSocketServer 클래스 객체가 필요합니다.

 

WiFiServer server(80);

WebSocketServer webSocketServer;

 

마지막으로 연결할 WiFi 네트워크의 자격 증명을 저장할 변수가 필요합니다. WiFi 네트워크에 연결하는 데 사용되는 함수에 직접 전달할 수도 있지만 여기에 선언하면 더 깔끔하고 유지 관리가 쉽습니다.

 

const char* ssid = "yourNetworkName";

const char* password = "yourNetworkPassword";

 

클라이언트 메시지 처리 함수

 

클라이언트에서 온 JSON 메시지를 파싱해야 하므로 이를 위한 전담 함수를 만들 것입니다. 이렇게 하면 Arduino 메인 루프 함수에 많은 로직을 분산시키는 대신 코드를 깔끔하고 캡슐화할 수 있습니다.

 

따라서 클라이언트의 데이터를 처리하는 함수는 파싱할 데이터가 있는 문자열을 매개변수로 받습니다. ESP32에서 Websocket 서버를 설정하는 것에 대한 이전 게시물에서 클라이언트에서 데이터를 수신할 때 데이터 유형이 문자열이라는 것을 기억하세요.

 

void handleReceivedMessage(String message)

{

// 메시지 처리 코드

}

 

이제 메시지를 파싱하기 위해 StaticJsonBuffer 클래스의 객체를 선언하는 것으로 시작합니다. 이 객체는 JSON 라이브러리에서 JSON 객체 트리를 저장하기 위한 사전 할당된 메모리 풀로 사용됩니다.

 

이 메모리 풀의 크기를 지정해야 하며, 이는 템플릿 매개변수로 수행됩니다. 이 도구를 사용하여 JSON 구조에 따라 이 버퍼에 필요한 크기를 계산하는 보다 정확한 방법을 확인할 수 있습니다. 그럼에도 불구하고 이 간단한 예제에서는 500이라는 값을 사용하는데, 이는 파싱하려는 구조에 충분하고도 남습니다.

 

StaticJsonBuffer<500> JSONBuffer; //메모리 풀

 

다음으로, 함수의 입력으로 받은 JSON 메시지를 파싱하기 위해 방금 만든 StaticJsonBuffer 객체의 parseObject 메서드를 호출합니다.

 

이 메서드는 파싱하려는 메시지를 입력으로 받고 JsonObject 클래스 객체에 대한 참조를 반환합니다. 아래에서 이 객체를 사용하여 파싱된 값에 액세스하지만 그 전에 먼저 파싱 절차가 성공적으로 수행되었는지 확인합니다.

 

이를 위해 방금 얻은 이 JsonObject 참조에서 success 메서드를 호출하기만 하면 됩니다. 이 메서드는 인수를 받지 않고 파싱이 성공했는지(참) 또는 그렇지 않은지(거짓)를 나타내는 부울 값을 반환합니다.

 

파싱이 성공적이지 않으면 메시지의 어떤 값에도 접근할 수 없으므로 함수 실행을 마칩니다.

 

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"]; // 센서 유형 값 가져오기

const char * sensorIdentifier = parsed["identifier"]; // 센서 유형 값 가져오기

const char * timestamp = parsed["timestamp"]; // 타임스탬프 가져오기

int value = parsed["value"]; //센서 측정값 가져오기

 

각 값을 얻은 후에는 잘 포맷된 방식으로 직렬 포트에 간단히 인쇄합니다. 아래에서 메시지 처리 함수의 전체 코드를 확인할 수 있으며, 이미 이러한 인쇄가 포함되어 있습니다.

 

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

 

 

The Setup

 

Arduino 설정 함수는 초기화에 사용됩니다. 먼저 직렬 연결을 열어 프로그램의 출력을 인쇄할 수 있습니다.

 

다음으로 ESP32를 WiFi 네트워크에 연결합니다. 이전 튜토리얼에서 많이 사용했던 코드를 사용할 예정이며, 여기에서 각 함수에 대한 자세한 설명을 확인할 수 있습니다.

 

네트워크에 연결한 후에는 ESP32에 할당된 로컬 IP를 인쇄합니다. 이는 Python 코드 Websocket 연결 메서드에서 사용해야 하는 것입니다.

 

WiFi 네트워크에 대한 연결이 완료되면 프로그램 시작 부분에서 선언한 WiFiServer 전역 객체에서 begin 메서드를 호출하여 TCP 서버를 초기화합니다. 이 함수는 인수를 받지 않고 void를 반환합니다.

 

아래에서 이전에 언급한 모든 초기화가 포함된 전체 설정 함수 코드를 확인할 수 있습니다.

 

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 객체의 available 메서드를 호출합니다.

 

이전 튜토리얼에서 언급했듯이, 우리는 여전히 TCP 수준에서 작업하고 있으며, 나중에 Websockets 프로토콜 계층에서 작업할 것입니다.

 

available 메서드 호출은 인수를 받지 않고 WiFiClient 클래스의 객체를 반환합니다. 이 객체를 변수에 저장한 후, 클라이언트가 연결되었는지 여부를 나타내는 Boolean 값을 반환하는 connected 메서드를 호출해야 합니다.

 

클라이언트가 연결되어 있으면 프로토콜의 초기 부분인 Websocket 핸드셰이크 절차를 수행해야 합니다. 이를 위해 프로그램 시작 부분에서 선언한 WebSocketServer 객체의 핸드셰이크 메서드를 간단히 호출합니다.

 

핸드셰이크 메서드는 이전에 얻은 WiFiClient 객체를 입력으로 받고 이를 구현에서 사용하여 클라이언트와 통신합니다. 이 메서드 호출은 핸드셰이크 절차가 올바르게 수행되면 true를 반환하고 그렇지 않으면 false를 반환합니다.

 

따라서 핸드셰이크 메서드가 true를 반환하는 경우에만 클라이언트와 데이터를 교환해야 합니다.

 

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

 

 

이전 조건 블록 내부에서 이제 클라이언트에서 수신한 데이터를 보관할 문자열 버퍼를 선언한 다음 클라이언트가 연결된 동안 루프를 실행합니다. WiFiClient에서 connected 메서드를 다시 호출하여 클라이언트가 여전히 연결되어 있는지 확인하고 반환 값을 while 루프 조건으로 사용할 수 있습니다.

 

그런 다음 루프 내부에서 WebSocketServer 객체의 getData 메서드를 호출하여 클라이언트가 보내는 데이터를 가져옵니다. 이 메서드는 인수를 받지 않고 클라이언트가 보낸 데이터가 포함된 문자열을 반환합니다.

 

그런 다음 데이터 크기가 0보다 큰 경우(클라이언트가 데이터를 보내지 않았을 수 있음) 이전에 정의한 메시지 처리 함수를 호출하여 구문 분석을 처리합니다.

 

테스트 목적으로 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 아두이노 코드입니다.

 

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

 

코드 테스트

 

전체 시스템을 테스트하기 위해 Arduino 코드를 컴파일하여 ESP32에 업로드하는 것으로 시작합니다. 절차가 완료되면 직렬 모니터를 열고 WiFi 네트워크에 연결될 때까지 기다리기만 하면 됩니다.

 

연결이 완료되면 ESP32의 로컬 IP 주소가 콘솔에 인쇄되었을 것입니다. 해당 주소를 복사하여 connect 메서드의 Python 코드에서 사용합니다.

 

그런 다음 Python 코드를 실행합니다. 그림 1과 비슷한 출력이 표시되어야 하는데, 서버에 보낸 JSON 메시지가 다시 에코되었습니다.

 

그림 1 – Python WebSocket 클라이언트로 에코된 JSON 메시지.

 

그런 다음 Arduino 직렬 모니터로 돌아갑니다. 그림 2와 비슷한 결과가 표시되어야 하는데, JSON 메시지에서 구문 분석된 값이 콘솔에 인쇄되는 것을 보여줍니다.

 

그림 2 – JSON 메시지에서 구문 분석된 값. 

 

 

이 포스팅의 참고 기사는 이 링크를 따라가시면 만날 수 있습니다.

 

배움을 멈추지 마세요. 더불어 절대 포기하지 마세요!

 

 

https://youtu.be/15X0WvGaVg8?si=atgsleh2flkZD8M8

반응형

더욱 좋은 정보를 제공하겠습니다.~ ^^