2018년 7월 24일 화요일

ESP32용 Asynchronous HTTP 웹서버

                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          아두이노 코어를 실행하는 ESP32에서 비동기식(asynchronous) HTTP 웹서버를 설정하는 방법에 대해 설명해 보도록 하겠다.
여기서 설명에 사용할 예제는 단순하게 접속하는 클라이언트에게 'hello world'라는 메시지를 리턴하는 웹서버이다.

여기서 필요한 라이브러리는 ESPAsyncWebServer와 AsyncTCP 두개이다.

ESPAsyncWebServer 라이브러리는 https://github.com/me-no-dev/ESPAsyncWebServer 에서 다운받을 수 있다.

이 라이브러리는 비동기식 HTTP (및 웹소켓) 서버를 설정해서 하나 이상의 클라이언트를 동시에 처리할 수 있게 해 준다. 또한 코드에서 볼 수 있는 것 처럼 서버 콜백 함수만 등록을 해 놓으면 메인루프에서 클라이언트 처리 함수를 주기적으로 호출할 필요가 없다.

AsyncTCP 라이브러리는 ESPAsyncWebServer를 위해 필요한 라이브러리라 필요하긴 하지만 코드에서 이 라이브러리를 직접 사용할 일은 없고 그냥 include만 해 주면 된다. https://github.com/me-no-dev/AsyncTCP 에서 다운받을 수 있다.

이 라이브러리는 ESP32용 비동기 TCP 라이브러리로 ESPAsyncWebServer 라이브러리 구현을 위한 기반이다.

코드를 위해서는 몇 가지 라이브러리를 include 해야 한다.

#include <WiFi.h>
#include <FS.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>

그 후 공유기 접속을 위한 정보가 따라온다.

const char *ssid = "Your network";
const char *password = "Your password";

또한 AsyncWebServer 클래스의 변수를 선언해 Asynchronous ESP32 HTTP 서버를 설정 하는데 사용한다.

Constructor의 입력으로 몇번 포트를 사용할 것인지를 넘겨준다. 여기서는 HTTP  디폴트 포트인 80번을 사용한다.

AsyncWebServer server(80);

setup() 함수에서는 먼저 기본적인 작업들...시리얼 포트 초기화, 무선랜 접속 등..을 수행한다. 코드는 다음과 같다.

void setup()
{
  Serial.begin(115200);
  
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi...");
  }
  Serial.println(WiFi.localIP());

이제 HTTP request를 받기 위해 서버가 listen을 할 route를 설정해 주고, 해당 route로 request가 수신될 때 실행될 함수를 만들어 준다.

server 오브젝트의 on 메소드를 호출해 콜백함수를 지정해 준다. on 메소드의 첫번째 파라미터는 패스를 저정하는 문자열이 된다. 여기서는 '/hello' route에 대한 request를 listen하도록 설정할 것이다.

두번째 파라미터는 WebRequestMethod 타입의 enum 값으로 해당 route에 대해 어떤 타입의 HTTP request를 허욜할지 지정한다. 여기서는 HTTP GET request만 받을 것이므로 HTTP_GET 값을 사용한다.

세번째 파라미터는ArRequestHandlerFunction 타입에 의해 signature가 정의된 함수가 온다. ArRequestHandlerFunction 타입 정의는 https://github.com/me-no-dev/ESPAsyncWebServer/blob/63b5303880023f17e1bca517ac593d8a33955e94/src/ESPAsyncWebServer.h 에서 확인할 수 있다.

여기서 지정하는 처리 함수는 파라미터로 AsyncWebServerRequest 타압 오브젝트의 포인터를 넘겨 받고 리턴값은 없다. 각 incoming client는 이 클래스 오브젝트로 wrap 되어 있고 두 오브젝트는 커넥션이 끊어질 때 까지 같이 살아 있다.

이 문법을 간단하게 하기 위해서 이 처리함수를 C++ lambda function으로 선언한다. 그러므로 내부적으로 선언한 이름 없는 함수로 지정할 수 있게 된다. 여러개의 route를 가지는 서버의 경우 이렇게 하는것이 각 route에 대해 각각 이름을 가지는 함수로 선언하는 것 보다 더 깔끔하고 컴팩트 해 진다.

다음과 같은 lambda 문법을 사용할 것이다.

[captures] (params) {body}

여기서는 따로 captures를 사용하지 않을 것이기 때문에 단순히 '[]'를 사용한다. params의 경우 앞에서 언급한 처리 함수의 signature를 따를 필요가 있다. 그러므로 lambda는 AsyncWebServerRequest 타입 오브젝트의 포인터를 파라미터로 받는다.

server.on("/hello", HTTP_GET, [](AsyncWebServerRequest *request) {
  // Lambda body implementation
});

이 예제에서는 클라이언트에게 단순히 'hello world'라는 메시지를 돌려주면 된다. 앞에서 언급한 것 처럼 각 클라이언트는 AsyncWebServerRequest 오브젝트에 연결되어 있고, 이 오브젝트는 응답할 HTTP response를 지정할 수 있게 해 주는 send라는 메소드를 가지고 있다.

이 메소드는 첫번째 파라미터로 HTTP response code를 받는데 여기서는 200을 넘겨준다. 이 HTTP response code는 "OK"를 의미한다.

두번쨔 파라미터는 response의 content-type 값이다. 여기서는 'hello world'라는 문자열을 리턴할 것이기 때문에 "text/plain"을 사용하고 있다.

여기서 우리는 오브젝트 자체가 아니고 오브젝트에 대한 포인터를 다루는 것이기 때문에 AsyncWebServerRequest 오브젝트의 send 메소드를 호출할 때 '->' 를 사용해야 한다는 것을 주의해라.

server.on("/hello", HTTP_GET, [](AsyncWebServerRequest *request) {
  request->send(200, "text/plain", "Hello World");
});

마지막으로 설정을 끝내기 위해 server 오브젝트의 begin 메소드를 호출해 줘야만 한다. begin 메소드를 호출하면 서버를 시작시킨다.

server.begin();

여기서 만든 서버는 비동기식(asynchronous)이기 때문에 메인루프에서 따로 처리함수등을 호출할 필요가 없다. 위에서 선언한 route 처리함수는 클라이언트에서 request가 들어오면 자동으로 호출된다.

전체 코드는 다음과 같다.

#include <WiFi.h>
#include <FS.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>

const char *ssid = "Your network";
const char *password = "Your password";

AsyncWebServer server(80);

void setup()
{
  Serial.begin(115200);
  
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi...");
  }
  Serial.println(WiFi.localIP());
 
  server.on("/hello", HTTP_GET, [](AsyncWebServerRequest *request) {
    request->send(200, "text/plain", "Hello World");
  });
  server.begin();
}

void loop()
{
}



코드 테스트


코드를 컴파일 해서 ESP32에 업로드 해 준다. 시리얼 모니터를 보면 무선랜에 접속되면 ESP32 보드의 IP 주소가 찍힌다.
이제 웹브라우져로 가서 주소창에 다음의 주소를 입력해 주면 된다.

http:[IP address]/hello

여기서 [IP address] 부분을 ESP32 보드의 IP 주소로 바꿔줘야 한다.

정상적으로 동작한다면 웹브라우져에 다음과 같이 출력될 것이다.





2018년 7월 17일 화요일

ESP32에서 웹소켓 서버 실행 (Websocket server on ESP32)

ESP32에 웹소켓(web socket) 서버를 만들는 방법을 설명하겠다. 이번에 만드는 웹소켓 서버는 가장 단순한 echo 서버로 클라이언트가 보낸 데이터를 그대로 클라이언트에 되돌려 준다.
또한 작성한 웹소켓 서버를 테스트 하기 위해 간단한 파이썬 클라이언트를 작성했다.

ESP32에서 웹소켓을 사용하기 위해 Websocket 라이브러리를 사용하였다. 이 라이브러리는 일반적으로 아두이노에서 TCP 서버를 만드는데 사용되는 WiFiServer 라이브러리 상에서 동작한다.
이 라이브러리는 ESP8266용으로 만들어 져 아직 공식적으로 ESP32를 지원하지는 않지만 약간만 수정하면 ESP32에서도 잘 동작한다.

라이브러리는 https://github.com/morrissinger/ESP8266-Websocket 에서 다운받을 수 있다.


우측 상단의 녹색 (1)번 버튼 (Clone or download) 을 누르면 풀다운 창이 열리고 그곳에서 Download ZIP을 눌러주면 된다. 그러면 ESP8266-Websocket-master.zip 이라는 파일이 다운로드 될 것이다. 이 파일의 압축을 풀면 ESP8266-Websocket-master 이라는 이름의 폴더가 생기게 된다. .


이 폴더의 이름을 ESP8266-Websocket (뒷쪽의 "-master' 부분을 제거)로 만들어 준 다음 다시 ZIP 파일로 압축한다

아두이노의 메뉴에서 스케치->라이브러리 포함하기->.ZIP 라이브러리 추가 를 선택 해 준다.



앞축파일을 찾아 선택해서 설치 해 준다.


이제 라이브러리 설치가 완료되었다. 윈도우의 경우 디폴트 위치가 C:\Users\[username]\Documents\Arduino\libraries 가 된다. Mac의 경우/Users/[username]/Docunemts/Arduino/libraries

라이브러리 설치는 끝났지만 그냥 컴파일을 하면 에러가 발생하므로 ESP32에서 사용할 수 있도록 약간의 수정을 해 준다.

라이브러리가 설치된 폴더를 보면 위와 같은 파일이 있는데 그 중 MD5.c, MD5.h 파일을 수정해 줘야 한다.


MD5.c 파일을 오픈 한 화면이다. 파일에서 위의 빨간색 부분 (함수 이름)을 다음과 같이 바꿔주면 된다.


MD5.h 도 유사하게 이름을 바꿔주면 된다.



이제 아두이노 메뉴에서 파일->예제->ESP8266-Websocket->WebsocketClient_Demo 를 열어본다.
예제 파일을 컴파일 해 보면 ESP8266WiFi.h 파일을 찾을 수 없다는 에러가 발생한다. 그러므로 첫번째 줄의 #include <ESP8266WiFi.h> 를 #include <WiFi.h> 로 바꿔줘야 한다.
또한 SSID HERE, PASSWORD HERE 부분에 자신이 사용할 공유기 정보를 넣어줘야 한다.



만일 공유기 ssid가 wifitestap 이고 암호가 ap1234567890 라고 하면 다음과 같이 변경해주면 된다.


샘플코드에서는 analogRead 값을 서버로 보내고 있는데 여기서는 테스트 목적이라 그냥 "Hello" 라는 스트링을 보내도록 수정하였다.


이제 ESP32 보드에 업로드 하고 시리얼 모니터로 결과를 확인할 수 있다.


테스트용 에코서버에 연결되면 반복적으로 "Hello"라는 스트링을 보내고 서버에서 리턴 된 문자를 출력한 것이다.

ESP32용 websocket 라이브러리 설치가 확인되었으면 이제 파이선으로 테스트용 웹소켓 클라이언트를 만들어 보겠다.

여기서는 websocket-client 모듈을 사용한다. 파이선에서 모듈 설치는 pip를 사용해 간단히 해결할 수 있다.

$ pip install websocket-client

모듈을 사용하기 위해 먼저 import 해 줘야 한다.

import websocket
import time

import 한 다음 WebSocket 클래스의 오브젝트를 만들어 준다.

ws = websocket.WebSocket()

이제 만들어 진 오브젝트의 connect() 메소드를 사용해 웹소켓 서버에 쉽게 접속할 수 있다. 메소드를 호출할 때 접속할 서버의 주소를 파라미터로 넘겨주면 되는데 웹소켓을 사용해 접속할 것이기 때문에 주소는 "ws://[server ip]/" 형태로 지정해 줘야 한다. 여기서 server ip는 웹서버가 실행되는 ESP32의 ip 주소를 넣어주면 된다.

테스트 웹소켓 서버로 확인해보고 싶으면 ws://echo.websocket.org/ 를 사용하면 된다.

ws.connect("ws://echo.websocket.org/")

이제 서버에 연결되었기 때문에 send 메소드로 원하는 데이터를 보낼 수 있다.

ws.send("Hello world")

recv 메소드로 서버에서 데이터를 받아 올 수 있다.

result = ws.recv()

서버에 연결을 끊을 때는 close 메소드를 사용하면 된다.

result = ws.close()

간단한 테스트 코드는 아래와 같다.

import websocket
import time

ws = websocket.WebSocket()
ws.connect ("ws://echo.websocket.org/")

i=0
nrOfMsgs = 10

while i<nrOfMsgs:
  ws.send("Msg no. "+str(i))
  result = ws.recv()
  print (result)
  i=i+1
  time.sleep(1)
ws.close()


이제 ESP32에 웹소켓 서버를 만들어 보겠다.
먼저 WebSocketServer.h 를 include 해 줘야 한다.

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

다음으로 WiFiServer 클래스 오브젝트 인스턴스로 TCP 서버를 만들어 줘야 한다. 웹소켓 서버는 이 서버 위에서 동작한다. 오브젝트 인스턴스의 파라미터는 옵션으로 서버 포트를 지정하는데 사용한다. 디폴트 값이 80이라 따로 지정하지 않아도 상관없지만 여기서는 명시적으로 80번 포트를 지정해 줬다.

WiFiServer server(80);

그리고 WebSocketServer 클래스의 오브젝트 인스턴스도 만들어 줘야 한다.

WebSocketServer webSocketServer;

마지막으로 공유기 접속을 위한 정보를 지정해 줘야 한다.

const char* ssid = "wifitestap";
const char *password = "ap1234567890";

setup() 함수에서는 우선 무선랩에 접속 후 server 인스턴스의 begin 메소드를 호출해 초기화 해 준다.

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");
  Serial.println(WiFi.localIP());

  server.begin(80);
  delay(100);
}

loop() 함수에서는 클라이언트와의 접속과 데이터 교환을 처리한다.

먼저 incoming connection이 있는가 확인하기 위해 WiFiServer 오브젝트의 available 메소드를 호출한다. available 메소드는 incoming connection이 있으면 WiFiClient 오브젝트를 리턴한다.

WiFiClient client = server.available();

주의할 점은 아직 웹소켓 클라이언트 레벨이 아니고 TCP 클라이언트 레벨에 있다는 것이다.

TCP 클라이언트가 연결되어 있는지 확인하기 위해 위에서 리턴된 WiFiClient 오브젝트의 connected 메소드를 호출한다. 또한 WebSocketServer 오브젝트에 WiFiClient 오브젝트를 파라미터로 해서 handshake 메소드를 호출해서 프로토콜 핸드쉐이크를 처리하도록 한다.

핸드쉐이크가 성공하면 true를 리턴한다. 이 값이 확인 돼야 클라이언트와 실제 통신을 진행할 수가 있게 된다.


  if (client.connected() && webSocketServer.handshake(client)) {
    // 클라이언트와 통신 코드
  }

여기서는 간단한 에코서버를 만드는 것이기 때문에 클라이언트에서 받은 데이터를 저장할 버퍼가 필요한데 메소드는 String 타입을 사용한다.

String data;

클라이언트가 아무 때나 접속을 끊을 수 있기 때문에 while 루프를 돌면서 접속이 끊어지지 않았는가 확인해야 한다.

또한 루프의 각 iteration 사이에 약간의 딜레이가 필요하다. 그렇지 않으면 첫번째 몇 바이트를 받은 다음 데이서 수신을 멈출 수 있기 때문에 매우 중요하다.

그러므로 루프의 구조는 다음과 같다.

if (client.connected() && webSocketServer.handshake(client)) {
  String data;
  while (client.connected()) {
    // handle communication
    delay(10);  // delay needed for receiving data correctly
  }
}

이제 while 루프 안에서 webSocketServer 오브젝트의 getData 메소드를 호출해 데이터를 수신한다. 이 메소드는 파라미터가 없고 수신한 데이터를 String 타입으로 리턴한다.

    data = webSocketServer.getData();

데이터를 수신하고 나면 그 내용을 시리얼 포트로 출력하고 클라이언트로 되돌려 보낸다.

클라이언트로 데이터를 보낼 땐 webSocketServer 오브젝트의 sendData 메소드를 호출한다.

클라이언트가 아무 데이터도 보내지 않는 경우도 있을 수 있으므로 데이터 버퍼의 길이를 먼저 확인한다. 그러므로 클라이언트에 데이터를 돌려보내기 위해서는 버퍼에 있는 데이터 길이가 0보다 커야만 한다.

    if (data.length() > 0) {
      Serial.println(data);
      webSocketServer.sendData(data);
    }


위에서 설명하며 작성한 전체 코드는 다음과 같다.

#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(1000);
}
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 data correctly
    }
    Serial.println("The client disconnected");
    delay(100);
  }
  delay(100);
}

작성한 코드를 테스트 하려면 먼저 ESP32에 업로드 해서 실행시킨다. 그럼 ESP32는 무선랜에 접속하고 local IP를 시리얼 포트로 출력해 준다.


여기서 ESP32의 IP address는 192.168.0.14 이다.
이제 파이썬 코드의 주소 부분을 ESP32의 IP address로 바꿔준다.




주소를 변경했으면 python으로 작성한 클라이언트를 실행시켜 주면 된다.


파이썬 클라이언트(우측 화면)에서 Msg no.x 라는 문자열을 웹소켓을 통해 ESP32로 보내면 ESP32(좌측화면)가 수신한 데이터를 시리얼 포트에 출력하고 그 내용을 클라이언트로 되돌려 보내 준다. 파이썬 클라이언트는 서버에서 받은 데이터 내용을 화면에 출력한 것이다.


2018년 7월 12일 목요일

ESP32에서 JSON 파싱

ESP32에서 ArduinoJson 라이브러리를 사용해 JSON 메시지를 파싱 해 보겠다.

먼저 ArduinoJson 라이브러리가 설치되어 있어야 한다.

만일 설치되어 있지 않으면 메뉴의 스케치->라이브러리 포함하기->라이브러리 관리...를 선택해 준다.


라이브러리 메니저 창이 열리면 1번 부분에 ArduinoJson 을 입력하면 해당 키워드를 가진 라이브러리 목록이 보인다. 목록중에 ArduinoJson을 찾아 그 부분을 클릭하면 3과 같이 라이브러리 우측 하단에 '설치' 버튼이 나타난다. '설치' 버튼을 눌러 설치해주면 된다. 설치가 완료되면 '닫기' 버튼을 눌러준다.


이제 라이브러리를 가지고 실제 코드를 작성한다.

가장 먼저 ArduinoJson 라이브러리를 include 해 줘야 한다. 또한 loop 함수에서 실제 파싱을 수행할 것이므로 결과를 확인하기 위해 시리얼 포트를 열어준다.

#include <ArduinoJson.h>

void setup()
{
  Serial.begin(115200);
  Serial.println("JSON parsing using ArduinoJson library on ESP32");
}

여기서는 네트웍에서 JSON 데이터를 불러오지 않고 하드코딩 된 JSON 메시지를 파싱 해 볼 것이기 때문에 이를 위한 변수를 선언해 준다. 여기에서 '\' 은 메지시 내에 들어가는 따옴표(")에 대한 escape character이다. JSON의 name에 따옴표(")가 있어야만 하기 때문에 필요하다.

char JSONMsg[] = " {\"SensorType\", \"Temperature\", \"Value\", 10}";

Escape character가 없는 JSON 메시지의 구조는 다음과 같다.

{
  "SensorType" : "Temperature",
  "Value" : 10
}

주의) JSON 파서는 스트링 내용을 수정하기 때문에 내용을 재사용 할 수 없다. 그래서 여기서도 JSONMsg 변수를 전역변수가 아니고 loop 내에서 선언해 loop 함수가 끝나면 free 되고, 다음번에 다시 loop() 함수가 호출될 때 다시 할당되도록 해 놓았다.

또한 파싱한 결과인 JSON object tree를 저장하기 위해 미리 할당된 메모리 풀을 위해 StaticJsonBuffer 클래스 오브젝트를 선언했다.  이 오브젝트는 메모리 풀이기 때문에 크기를 지정해 줘야 한다. 크기는 템플릿 파라미터(코드에서 < > 사이의 값)에 바이트 단위로 지정해 주면 된다. 여기서는 300바이트를 지정했는데 이 정도면 위의 JSON 메시지를 파싱하는데는 충분하다.

StaticJsonBuffer<300> JSONBuffer;    // memory pool for parsed JSON object tree

다음으로는 StaticJsonBuffer 오브젝트의 parseObject 메소드를 호출하면서 파싱하길 원하는 JSON 스트링을 넘겨준다.

JsonObject &parsed = JSONBuffer.parseObject(JSONMsg);

에러 없이 파싱이 끝났는지 확인하기 위해 JSONObject의 success 메소드를 호출할 수 있다.

if (!parsed.success()) {    // check for errors in parsing
  Serial.println("Parsing failed!");
  delay(5000);
  return;
}

이제 JsonObject에서 이름으로 그에 해당하는 값을 가져오기 위해 subscript operator를 사용할 수 있다. 즉 key에 해당하는 값을 가져오기 위해 원하는 파라미터의 key 이름을 [ ]으로 둘러싸면 된다. 이 예제에서 사용하고 있는 key 이름은 "SensorType"과 "Value" 두 가지다.

const char *sensorType = parsed["SensorType"];     // get sensor type value
int value = parsed["Value"]  // get value of the sensor measurement

전체 코드는 다음과 같다.

#include <ArduinoJson.h>
 
void setup() {
 
  Serial.begin(115200);
  Serial.println("JSON parsing using ArduinoJson library on ESP32");
}
 
void loop() {
 Serial.println("Parsing start: ");
 char JSONMsg[] = " {\"SensorType\": \"Temperature\", \"Value\": 10}"; //Original message
 
  StaticJsonBuffer<300> JSONBuffer;                         //Memory pool
  JsonObject& parsed = JSONBuffer.parseObject(JSONMsg); //Parse message
 
  if (!parsed.success()) {   //Check for errors in parsing
    Serial.println("Parsing failed!");
    delay(5000);
    return;
  }
 
  const char * sensorType = parsed["SensorType"]; //Get sensor type value
  int value = parsed["Value"];                    //Get value of sensor measurement
 
  Serial.print("Sensor type: ");
  Serial.println(sensorType);
  Serial.print("Sensor value: ");
  Serial.println(value);
 
  Serial.println();
  delay(5000);
}


위의 코드를 ESP32에 다운로드 해서 실행하고 시리얼모니터를 열면 다음과 같은 결과가 출력 될 것이다.

JSON parsing using ArduinoJson library on ESP32
Parsing start:
Sensor type: Temperature
Sensor value: 10

Parsing start:
Sensor type: Temperature
Sensor value: 10

Parsing start:
Sensor type: Temperature
Sensor value: 10

...