2018년 11월 23일 금요일

ESP32에서 SPIFFS 사용하기

ESP32는 SPIFFS(Serial Peripheral Interface Flash File System)을 가지고 있다. SPIFFS는 SPI 버스에 연결된 플래쉬 칩을 가지고 있는 ESP32같은 마이크로 컨트롤러를 위해 만들어 진 경랑(lightweight) 파일 시스템이다.

SPIFFS를 사용하면 플래쉬 메모리를 컴퓨터에서 일반 파일을 억세스 하는 것 처럼 사용할 수 있게 해 준다. 물론 더 간단하지만 기능은 제한적이다. 파일을 읽고, 쓰고, 삭제하는 것이 가능하다. 현재 시점에서는 디렉토리를 지원하지 않으므로 모든 파일은 flat한 구조에 저장된다.

ESP32 Filesystem Uploader 설치

아두이노 IDE에서 코드를 작성해 ESP32 파일시스템에 파일을 만들어 내용을 기록해 줄 수 있지만, 이 방법은 파일 내용을 전부 아두이노 스케치 코드에 타이핑 해 줘야만 하므로 유용하지 못하다.
다행히도 아두이노 IDE용 플러그인이 있어 컴퓨터의 폴더에서 ESP32 파일시스템으로 파일을 직접 업로드 할 수가 있다. 이를 위해 ESP32 Filesystem Uploader를 설치해 준다.

* 먼저 최신 버젼의 아두이노 IDE가 설치되어 있고 아두이노용 ESP32 개발환경이 설정되어 있어야만 한다.

1. Arduino-esp32fs-plugin 페이지에서 ESP32FS-v01.zip 을 다운받는다.
2. 아두이노 IDE 디렉토리로 가서 Tools 폴더를 연다.
맥의 경우는 ~/Documents/Arduino 폴더 안에 tools 폴더를 만들어 주면 된다.
3. tools 폴더에서 다운받은 zip 파일 압축을 풀어준다.
4. 아두이노 IDE를 재시동한다.
5. 아두이노 IDE의 Tools 메뉴에 "ESP32 Sketch Data Upload" 항목이 보이면 정상적으로 설치가 된 것이다.


Filesystem Uploader를 사용해서 파일 업로드하기

1. 아두이노 스케치를 만들어 저장한다. 여기서는 파일 업로드 데모이므로 빈 스케치를 사용한다.
2. 스케치 폴더를 열어 준다. Sketch 메뉴에서 'Show Sketch Folder'를 선택하면 스케치가 저장되어 있는 폴더가 열린다.


 3. 폴더 안에 data 라는 이름의 폴더를 만들어 준다.


 4. data 폴더 안에 ESP32 파일시스템에 저장하고 싶은 파일들을 넣어주면 된다. 여기서는 예제로 test_example.txt 라는 파일을 만들어 간단한 내용을 넣어 준다.

5. Tools -> ESP32 Sketch Data Upload 를 선택해서 데이터를 업로드 하면 된다.


예제

1. ESP32_SPIFFS_test 라는 이름으로 스케치를 만들고 스케치 폴더 내에 data 폴더를 만들어 준다.

2. 데이터 폴더 안에 'data.txt'라는 이름의 텍스트 파일을 만들고 간단한 내용을 넣어 준다. 여기서는 'Hello World!'를 넣어주었다.

3. 위의 설명대로 SPIFFS에 파일을 업로드 해 준다.

4. 다음의 스케치 코드를 입력해서 보드에 업로드 해 준다.

#include "SPIFFS.h"

void setup() {
  Serial.begin(115200);

  if (!SPIFFS.begin(true)) {
    Serial.println("# ERROR: can not mount SPIFFS");
    while (1) ;
  }
}

void loop() {
  File f = SPIFFS.open("/data.txt");
  if (!f) {  // can not open file
    Serial.println("# ERROR: can not open file");
    delay(5000);
    return;
  }

  while (f.available()) {
    Serial.write(f.read());
  }
  Serial.println("");

  f.close();
  delay(5000);
}


5. 실행 결과는 다음과 같다.




















2018년 11월 22일 목요일

ESP32에서 인터럽트 사용하기

ESP32는 26개의 GPIO핀을 인터럽트 소스로 사용할 수 있다.

* 아두이노 우노/메가에서는 인터럽트 소스로 사용할 수 있는 핀이 몇개 안된다.




위의 그림에서 빨간색 사각형 안에 들어있는 GPIO 핀들이 인터럽트 소스로 사용 가능한 핀이다.

아두이노에서 인터럽트를 사용하려면 attachInterrupt() 함수로 인터럽트를 설정해 줘야 한다.

attachInterrupt (digitalPinToInterrupt(GPIO), function, mode);

첫번째 파라미터는 인터럽트 소스로 사용할 GPIO 핀 번호인데 번호를 그냥 사용하지 말고 digitalPinToInterrupt(GPIO) 로 넣어줘야 한다. 예를 들어 GPIO 13을 인터럽트 소스로 사용하려면 digitalPinToInterrupt(13) 을 넣어주면 된다.

두번째 파라미터는 인터럽트가 발생했을 때 호출될 함수(Interrupt Service Routine)의 이름이다.

세번째 파라미터는 인터럽트 발생 조건이다. 총 5가지 모드중에 하나를 선택하면 된다.

* LOW - 인터럽트 핀 상태가 LOW면 인터럽트가 발생
* HIGH - 인터럽트 핀 상태가 HIGH면 인터럽트가 발생
* CHANGE - 인터럽트 핀 상태가 바뀌면 인터럽트가 발생. 즉 HIGH에서 LOW로 바뀌거나, LOW에서 HIGH로 바뀌면 인터럽트가 발생
* FALLING - 인터럽트 핀 상태가 HIGH에서 LOW로 바뀔 때 인터럽트가 발생
* RISING - 인터럽트 핀 상태가 LOW에서 HIGH로 바뀔 때 인터럽트가 발생

그러므로 인터럽트를 사용하는 경우 코드의 구조는 다음과 같이 된다.

// 인터럽트가 발생하면 이 함수가 호출됨
void IRAM_ATTR intSvc() 
{
  // 스위치가 눌렸을 때 원하는 동작 코드를 이 부분에 넣어줌
  Serial.println("# Button pressed");
}
void setup()
{
  ...
  attachInterrupt(digitalPinToInterrupt(13), intSvc, LOW);  // GPIO13번 핀에 연결된 스위치가 눌리면 인터럽트가 발생해 intSvc 함수를 호출하도록 설정
  ...
}

void loop()
{
}

위의 코드를 잘 보면 인터럽트가 발생했을 때 호출될 함수 선언에 IRAM_ATTR 이라는 것이 추가되어 있는것을 알 수 있을 것이다. IRAM_ATTR을 넣어줘야 인터럽트 처리 함수가 RAM에 들어가게 된다. IRAM_ATTR이 없으면 인터럽트 처리 함수가 플래쉬에 들어가게 되어 실행 속도가 느려진다.


위와 같이 연결 해 놓고 스위치를 누를 때 마다 LED가 토글되는 코드는 다음과 같다.

#define LED 25
#define SW 13

volatile boolean gLedState = LOW;
 
void IRAM_ATTR toggle() 
{
  gLedState = !gLedState;
  digitalWrite(LED, gLedState);
}


void setup()
{
  pinMode(SW, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(SW), toggle, LOW);  // GPIO13번 핀에 연결된 스위치가 눌리면 toggle 함수를 호출하도록 설정
  pinMode(LED, OTUPUT);
}

void loop()
{
}






2018년 11월 20일 화요일

ESP32 PWM 사용하기

ESP32에는 16개의 독립적인 LED PWM 채널을 가지고 있어 각각 다른 속성의 PWM 신호를 만들어 낼 수 있다.

1. 어떤 PWM 채널을 사용할 지 골라야 한다. 0~15 사이의 값을 사용할 수 있다.
2. PWM 주파수를 설정해 줘야 한다. LED의 경우 5000 Hz 정도면 충분하다.
3. PWM duty cycle resolution을 설정해 줘야 한다. 해상도(resolution)은 1비트에서 16비트까지 지정할 수 있다. 여기서는 8 bit 해상도를 사용하는데 그 경우 LED 밝기 값은 0에서 255가 될 수 있다. 만일 16 bit로 설정하면 좀 더 세밀하게 (0~65535) 밝기 값을 조정할 수 있게 된다.

ledcSetup(ch, freq, resolution);   // ch: PWM channel (0~15), freq: PWM 주파수, resolution: PWM  해상도


PWM 주파수는 1초에 몇번 신호를 ON/OFF 할 것인지 결정한다. 위의 그림에서 처럼 PWM 주파수를 4 Hz로 한다면 CH0에 연결되어 있는 LED는 4번 on/off를 반복할 것이다. 8 Hz인 경우 CH1에 연결되어 있는 LED는 8번 on/off를 하게 될 것이다. 아두이노 우노의 경우 PWM 주파수는490 Hz(5,6번 핀은 980 Hz)로 고정되어 있다.

Resolution은 한 주기 내에서 얼마나 세밀하게 시간을 지정할 수 있는가를 결정한다. 8-bit resolution인 경우 2^8 = 256 등분으로 나누지만 10-bit resolution이 되면 2^10 = 1024 등분으로 나뉘어 훨씬 더 정밀하게 제어가 가능해진다. ESP32에서는 1-bit 부터 16-bit까지 resolution을 지정할 수 있다. 아두이노 우노의 경우에는 analogWrite()의 resolution은 8-bit로 고정되어 있어 출력값으로 0~255 사이의 값을 사용해야 한다.

4. 설정한 채널의 출력을 어느 GPIO 핀으로 보낼것인지 지정해야 한다.

ledcAttachPin(gpio, ch);   // gpio : GPIO 핀 번호,  ch : PWM channel

5. 이제 ledcWrite() 함수를 사용해 LED 밝기를 조절할 수 있다.

ledcWrite(ch, duty); // ch : PWM channel, duty : Duty cycle

duty 값은 한 주기 내에서 ON 시간과 OFF 시간의 비율을 결정하는 값이다.


위의 그림을 보면 알 수 있듯이 PWM 주파수가 다르면 같은 duty 값이라도 ON 되어 있는 시간이 달라진다. 다만 전체적으로 ON 시간의 합과 OFF 시간의 합의 비율은 동일하다.




위와 같이 LED를 연결한 경우 LED가 점점 밝아졌다가 다시 점점 어두워지는걸 반복하는 코드는 다음과 같다.

const int ledPin = 16;  // 16 corresponds to GPIO16
// setting PWM properties
const int freq = 5000;
const int ledChannel = 0;
const int resolution = 8;
 

void setup(){
  ledcSetup(ledChannel, freq, resolution);  
// configure LED PWM functionalitites  
  ledcAttachPin(ledPin, ledChannel);  
  // attach the channel to the GPIO to be controlled
}
 void loop(){
  // increase the LED brightness
  for(int dutyCycle = 0; dutyCycle <= 255; dutyCycle++){   
    // changing the LED brightness with PWM
    ledcWrite(ledChannel, dutyCycle);
    delay(15);
  }

  // decrease the LED brightness
  for(int dutyCycle = 255; dutyCycle >= 0; dutyCycle--){
    // changing the LED brightness with PWM
    ledcWrite(ledChannel, dutyCycle);   
    delay(15);
  }
}

PWM 신호의 frequency를 조정할 수 있으므로 16개의 서보모터를 동시에 제어할 수도 있다.


서보모터는 위와 같은 PWM 신호로 제어를 하기 때문에 PWM frequency를 50 Hz로 해 주고 16-bit resolution을 사용하는 경우 duty값 3277이 0도, 4915가 90도, 6554가 180도가 된다.

즉 GPIO16에 연결된 서보모터를 CH0를 사용해서 제어한다고 하면 다음과 같이 할 수 있다.

void setup()
{
  ...
  ledcSetup(0, 50, 16);    // PWM CH 0, Freq. 50 Hz, 16-bit resolution
  ledcAttach(16, 0);         // PWM CH 0을 GPIO 16번으로 출력
  ...
}

// deg는 0~180도 까지
void servoWrite(int ch, int deg)
{
  int duty = deg*18.2 + 3277;

  ledcWrite(ch, duty);
}

void loop()
{
  ...
  servoWrite(0, 90);   // CH 0에 연결된 서보를 90도로
  ...
}







2018년 11월 16일 금요일

ESP32에서 NTP(Network Time Protocol) 서버로부터 시간 가져오기



인터넷에서 정확한 시간을 알기 위해 사용하는 프로토콜로 NTP(Network Time Protocol)이 있다. 곳곳에 운영중인 NTP서버에 접속해 정확한 현재 날짜와 시간을 가져 올 수 있다.



ESP32에서도 NTP를 사용할 수 있게 이미 NTP client 라이브러리가 공개되어 있다.

Download NTP Client Library

위의 링크를 클릭하면 라이브러리를 다운받아 설치해주면 된다.

코드 구조는 다음과 같다.

1. WiFi.h, WiFiUdp.h, NTPClient.h 를 include 해 줘야 한다.

#include <WiFi.h>
#include <WiFiUdp.h>
#include <NTPClient.h>

2. 인터넷 연결을 위해 공유기 설정을 해 준다.

const char* ssid  = "XXXXX";
const char* password = "YYYYY";

위에서 XXXXX 대신 사용할 공유기의 이름(SSID)로, YYYYY 대신 공유기의 암호로 바꿔줘야 한다.

3. 이제 NTP client를 만들어 준다.

WiFiUdp ntpUDP;
NTPClient timeClient(ntpUdp);

4. 공유기에 접속한 후 NTP client를 시작한다.

....
timeClient.begin();

5. 자신의 timezone에 맞게 시간을 조정하기 위해 setTimeOffset() 메소드를 사용할 수 있는데 이때 offset은 초 단위로 지정해 줘야 한다. 즉 1시간은 3600초이므로 한국의 경우 GMT+9 이니까 3600*9 = 32400이 된다.

timeClient.setTimeOffset(32400);

6. NTP 서버에 요청해 시간을 가져온다.

while (!timeClient.update()) {
  timeClient.forceUpdate();
}

7. NTP 서버에게서 읽어온 값을 사람이 볼 수 있는 형태로 변환해준다.

formattedDate = timeClient.getFormattedDate();

getFormattedDate()가 리턴한 값은 다음과 같은 포맷이다.

2018-11-09T07:34:21z

T 앞쪽이 날짜가 되고 T에서 z까지가 시간이므로 각각을 기준으로 원하는 내용을 추출해서 출력해주면 된다.

실행 가능한 전체 코드는 다음과 같다.

#include <WiFi.h>
#include <WiFiUdp.h>

#include <NTPClient.h>

const char* ssid     = "XXXXX";  // XXXXX 를 접속할 공유기 SSID로 변경
const char* password = "YYYYY"   // YYYYY 를 접속할 공유기 암호로 변경

// Define NTP Client to get time
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP);

// Variables to save date and time
String formattedDate;
String dayStamp;
String timeStamp;

void setup() {
  Serial.begin(115200);
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected.");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());

  timeClient.begin();    // NTP 클라이언트 초기화
  // 자신의 timezone에 맞게 초 단위로 time offset을 설정해준다. 예를 들어
  // GMT +1 = 3600
  // GMT +8 = 28800
  // GMT -1 = -3600
  // GMT 0 = 0
  timeClient.setTimeOffset(32400);  // 한국은 GMT+9이므로 9*3600=32400
}
void loop() {
  while(!timeClient.update()) {
    timeClient.forceUpdate();
  }
  // formattedDate 은 다음과 같은 형태임
  // 2018-11-12T16:00:13Z
  formattedDate = timeClient.getFormattedDate();
  Serial.println(formattedDate);

  // 날짜 추출
  int splitT = formattedDate.indexOf("T");
  dayStamp = formattedDate.substring(0, splitT);
  Serial.print("DATE: ");
  Serial.println(dayStamp);
  // 시간 추출
  timeStamp = formattedDate.substring(splitT+1, formattedDate.length()-1);
  Serial.print("HOUR: ");
  Serial.println(timeStamp);
  delay(1000);
}


코드의 실행 결과이다.

Connecting to XXXXX
.......
WiFi connected.
IP address:
172.20.10.13
2018-11-09T08:53:26Z
DATE: 2018-11-09
HOUR: 08:53:26
2018-11-09T08:53:27Z
DATE: 2018-11-09
HOUR: 08:53:27
2018-11-09T08:53:28Z
DATE: 2018-11-09
HOUR: 08:53:28


















2018년 11월 9일 금요일

ESP32에서 터치센서 사용하기 (Using ESP32 Capacitive Touch Sensor)



이전 포스트(ESP32 GPIO 레퍼런스)에서 소개했던 것 처럼 ESP32에는 총 10개의 터치 센서가 들어 있다. 이를 사용하면 아무런 추가 하드웨어 없이 터치 스위치를 만들 수 있다. 10개 터치 센서의 핀 매핑은 다음과 같다.

static const uint8_t T0 = 4;
static const uint8_t T1 = 0;
static const uint8_t T2 = 2;
static const uint8_t T3 = 15;
static const uint8_t T4 = 13;
static const uint8_t T5 = 12;
static const uint8_t T6 = 14;
static const uint8_t T7 = 27;
static const uint8_t T8 = 33;
static const uint8_t T9 = 32;

사용법 역시 매우 간단하다. 푸쉬버튼을 사용하는 경우 먼저 setup()에서 스위치가 연결된 핀을 pinMode()로 설정을 해 주고 digitalRead()로 상태를 읽어야 하는데 터치의 경우 별도의 설정은 필요 없고 touchRead() 함수로 값을 읽으면 된다.

아래 예제 코드는 T0 (4번핀)를 터치 스위치로 사용해서 LED를 토글시키는 것이다.


#define TOUCH_SW T0    // connected to 4
#define LED A13        // connected to 15

int touchVal = 100;

void setup()
{
  Serial.begin(115200);
  Serial.println("ESP32 Touch Example");
  pinMode(LED, OUTPUT);
}

void loop()
{
  static boolean led_state = LOW;
  static boolean prevState = LOW; 
  static boolean touchState;

  touchVal = touchRead(TOUCH_SW);     // read touch switch state
  touchState = touchVal < 50 ? HIGH:LOW;
  if ((LOW == prevState) && (HIGH == touchState))  {    
    Serial.println("Switch touched");
    led_state = !led_state;
    digitalWrite(LED, led_state);      
  }
  prevState = touchState;
  delay(50);
}






2018년 11월 7일 수요일

ESP32 GPIO 레퍼런스

ESP32에는 총 48개의 핀이 있는데 각 핀들은 여러가지 기능을 가지고 있다. 물론 ESP32 개발보드에서 모든 핀을 다 사용할 수 있는것은 아니다. 그래서 여기서는 어떤 핀을 사용해야 하고 어떤 핀을 사용하지 않아야 하는지에 대해 설명하겠다.

ESP32는 다음과 같은 peripheral을 가지고 있다.

* 18 ADC(Analog-to-Digital Converter) channel
* 3 SPI
* 3 UART
* 2 I2C
* 16 PWM
* 2 DAC (Digital-to-Analog Converter)
* 2 I2S
* 10 Capacitive sensing GPIO

ADC와 DAC 기능은 특정 핀에 고정되어 있다. 하지만 UART, I2C, SPI, PWM등의 기능은 어느 핀에 사용할지 결정해서 코드에서 지정해 줘야 한다.

소프트웨어에서 핀의 속성을 정의해 줄 수 있지만, 각 핀들은 디폴트로 지정되어 있는 기능들이 있다. ESP32 DEVKIT V1-DOIT의 경우 디폴트 핀 배열은 다음과 같다.




또한 특정한 기능의 핀들은 프로젝트마다 사용하기에 적합할 수도 그렇지 않을 수도 있다.

아래 그림은 ESP-WROOM-32의 핀 배치이다. 만일 칩만 가지고 자작 보드를 만든다면 아래 그림을 레퍼런스로 삼을 수 있다.



Input Only Pins

GPIO 34부터 39까지는 입력 전용 핀이다. 그리고 이 핀들은 내부적으로 풀업/풀다운 저항이 없다. 출력으로 사용할 수 없으므로 입력으로만 사용해야 한다.

* GPIO 34
* GPIO 35
* GPIO 36
* GPIO 37
* GPIO 38
* GPIO 39

ESP-WROOM-32에 들어 있는 SPI Flash

일부 보드들은 GPIO 6부터 11까지의 핀도 사용할 수 있게 되어 있다. 하지만 이 핀들은 ESP-WROOM-32 칩에 내장된 SPI Flash에 연결되어 있기 때문에 다른 용도로 사용하는걸 권장하지 않는다.

* GPIO 6 (SCK/CLK)
* GPIO 7 (SDO/SD0)
* GPIO 8 (SDI/SD1)
* GPIO 9 (SHD/SD2)
* GPIO 10 (SWP/SD3)
* GPIO 11 (CSC/CMD)

Capacitive Touch GPIO (정전식 터치 입력)

ESP32는 10개의 정전식 터치 입력 센서를 가지고 있다. 이 센서는 사람의 피부 같이 전하를 가진 것들의 전하 변화를 측정할 수 있다. 그러므로 손가락으로 GPIO를 터치할 때 유도되는 변하를 감지할 수 있다. 이 핀들은 전하 패드에 연결해 기계식 버튼을 대치할 수 있다. 정전식 터치 핀은 ESP32를 deep sleep에서 깨어나게 하는데 사용할 수도 있다.

이 내부 터치 센서는 다음의 GPIO에 연결되어 있다.

* T0 (GPIO 4)
* T1 (GPIO 0)
* T2 (GPIO 2)
* T3 (GPIO 15)
* T4 (GPIO 13)
* T5 (GPIO 12)
* T6 (GPIO 14)
* T7 (GPIO 27)
* T8 (GPIO 33)
* T9 (GPIO 32)

ADC (Analog-to-Digital Converter)

 ESP32는 18개의 12-bit ADC 채널을 가지고 있다. (ESP8266의 경우는 1개의 10-bit ADC만 가지고 있음) 다음의 GPIO 는 각각 해당 ADC 채널로 사용될 수 있다.

 * ADC1_CH0 (GPIO 36)
 * ADC1_CH1 (GPIO 37)
 * ADC1_CH2 (GPIO 38)
 * ADC1_CH3 (GPIO 39)
 * ADC1_CH4 (GPIO 32)
 * ADC1_CH5 (GPIO 33)
 * ADC1_CH6 (GPIO 34)
 * ADC1_CH7 (GPIO 35)
 * ADC2_CH0 (GPIO 4)
 * ADC2_CH1 (GPIO 0)
 * ADC2_CH2 (GPIO 2)
 * ADC2_CH3 (GPIO 15)
 * ADC2_CH4 (GPIO 13)
 * ADC2_CH5 (GPIO 12)
 * ADC2_CH6 (GPIO 14)
 * ADC2_CH7 (GPIO 27)
 * ADC2_CH8 (GPIO 25)
 * ADC2_CH9 (GPIO 26)

ADC 입력 채널은 12-bit 해살도를 가지고 있다. 즉 아날로그 값을 읽으면 0~4095 사이의 값을 얻을 수 있다. 여기서 0은 0V, 4095는 3.3V를 의미한다. 또한 코드에서 각 채널의 해설도나 ADC 범위를 조정할 수도 있다.

ESP32 ADC 핀은 리니어 한 속성을 가지고 있지 않으므로 주의해야 한다. 0V와 0.1V, 또는 3.2V와 3.3V를 거의 구별하지 못할 것이다. 그러므로 ADC를 사용할 때 이 점을 염두에 두어야만 한다. 아래 그래프와 같은 속성을 가진다.


DAC (Digital-to-Analog Converter)

ESP32에는 2개의 8-bit DAC 채널이 있어 디지털 신호를 아날로그 전압으로 바꿔 출력해 준다.

* DAC1 (GPIO 25)
* DAC2 (GPIO 26)

RTC GPIO

ESP32는 RTC GPIO를 지원한다. ESP32가 deep sleep 모드에 있을 때 RTC의 low-power subsystem에 연결된 GPIO를 사용할 수 있다. 이 RTC GPIO는 ULP(Ultra Low Power) 코프로세서가 실행중일 때 ESP32를 deep sleep에서 깨어나게 하는데 사용된다. 다음의 GPIO들이 외부 wake up 소스로 사용될 수 있다.

* RTC_GPIO0 (GPIO 36)
* RTC_GPIO3 (GPIO 39)
* RTC_GPIO4 (GPIO 34)
* RTC_GPIO5 (GPIO 35)
* RTC_GPIO6 (GPIO 25)
* RTC_GPIO7 (GPIO 26)
* RTC_GPIO8 (GPIO 33)
* RTC_GPIO9 (GPIO 32)
* RTC_GPIO10 (GPIO 4)
* RTC_GPIO11 (GPIO 0)
* RTC_GPIO12 (GPIO 2)
* RTC_GPIO13 (GPIO 15)
* RTC_GPIO14 (GPIO 13)
* RTC_GPIO15 (GPIO 12)
* RTC_GPIO16 (GPIO 14)
* RTC_GPIO17 (GPIO 27)

PWM (Pulse Width Modulation)

ESP32 LED PWM controller는 16개의 독립적인 채널을 가지고 있어 각각 다른 속성의 PWM 신호를 만들어 내도록 설정할 수 있다. 출력으로 사용할 수 있는 모든 핀은 PWM 으로 사용할 수 있다. (즉 GPIO 34~39는 PWM을 만들 수 없다)

PWM 신호를 만들어주려면 코드에서 다음의 파라미터를 정해줘야 한다.

* Signal Frequency
* Duty Cycle
* PWM Channel
* 신호를 출력할 GPIO핀

I2C

아두이노 IDE에서 ESP32를 사용할 때는 다음의 디폴트 핀을 사용해야만 한다.

* GPIO 21 (SDA)
* GPIO 22 (SCL)

SPI

디폴트로 SPI핀은 다음과 같이 매핑되어 있다.

Interrupt

모든 GPIO 핀은 인터럽트로 설정할 수 있다.

Enable (EN)

Enable (EN)은 3.3V 레귤레이터의 enable 핀이다. 이 핀은 풀업이 되어 있으므로, GND에 연결하면 3.3V 레귤레이터가 비활성화 된다. 즉 이 핀을 푸쉬버튼에 연결해 놓으면 ESP32를 재시동하는데 사용할 수 있다.

GPIO current draw

ESP32 데이터쉬트에 있는 "권장하는 동작 조건(Recommended Operating Condition)"에 따르면 GPIO마다 최대 40mA의 전류를 흘릴 수 있다.























2018년 11월 1일 목요일

ESP32에 static IP 사용하기 (Using static IP address on ESP32)

ESP32를 사용해 무선랜에 접속하는 경우 기본적으로 DHCP를 사용해 IP주소를 받아오게 된다.
하지만 ESP32가 서버로 동작하는 경우 IP주소를 고정시켜 놓아햐 할 필요가 있다. 또한 배터리로 동작시 deep sleep 모드를 활용하게 되는데 이때 deep sleep에서 깨어날 때 마다 공유기에 접속 후 DHCP로 IP를 받아오는 과정을 수행해야 하면 그에 따른 배터리 소모도 무시할 수 없다.

그래서 ESP32 무선랜이 DHCP를 사용하지 않고 수동으로 IP주소를 설정하는 방법을 설명하겠다.

DHCP를 사용하건 아니건 공유기에 접속을 해야만 하므로 접속할 공유기 정보(SSID, password) 는 필요하다.

const char * ssid = "REPLACE_YOUR_SSID";
const char * pwd = "REPLACE_YOUR_PASSWORD";

저기서 빨간색 부분을 자신에게 맞게 바꿔주면 된다. 만일 공유기 이름이 myhome 이고 공유기 암호가 hello12345 라고 한다면 다음과 같이 바뀌게 된다.

const char * ssid = "myhome";
const char * pwd = "hello12345";

다음은 static IP 를 지정해 준다.

IPAddress local_IP(192, 168, 1, 200); // ESP32가 사용할 IP address
IPAddress gateway(192, 168, 1, 1);    // Gateway IP address (공유기 IP주소)

IPAddress subnet(255, 255, 255, 0);   // subnet mask
IPAddress primaryDNS(8, 8, 8, 8);     // primary DNS server IP address
IPAddress secondaryDNS(8, 8, 4, 4);   // secondary DNS server IP address

여기서도 물론 빨간색 부분은 자신의 환경에 맞게 바꿔줘야 한다.

* 주소 사이에 '.'이 아니고 ','를 사용하고 있음을 주의할 것

이제 setup() 에서 WiFi.config() 메소드로 위의 설정값들을 ESP32에 할당해 준다.

if (!WiFi.config(local_IP, gateway, subnet, primaryDNS, secondaryDNS)) {
  Serial.println("STA failed to configure");
}

* 위에서 primaryDNS와 secondaryDNS 는 옵션이라 생략해도 된다.

지금까지 설명한 부분을 정리한 코드이다. 공유기 ssid는 "myhome", 공유기 password는"hello12345", ESP32 IP address는  192.168.1.200, gatewaIP(공유기IP)는192.168.1.1 인 경우의 코드이다. 

#include <WiFi.h>

const char * ssid = "myhome";
const char * pwd = "hello12345";

IPAddress local_IP(192, 168, 1, 200);          // ESP32가 사용할 IP address
IPAddress gateway(192, 168, 1, 1);              // Gateway IP address (공유기 IP주소)

IPAddress subnet(255, 255, 255, 0);          // subnet mask
IPAddress primaryDNS(8, 8, 8, 8);             // primary DNS server IP address
IPAddress secondaryDNS(8, 8, 4, 4);         // secondary DNS server IP address

...

void setup()
{
  Serial.begin(115200);

  if (!WiFi.config(local_IP, gateway, subnet, primaryDNS, secondaryDNS)) {
    Serial.println("STA failed to configure");
  }

  WiFi.begin(ssid, pwd);
  while (WiFi.status() != WL_CONNECTED) P
    delay(500);
    Serial.print(".");
  }
  
  ...
}

---

다른 방법으로는 ESP32의 코드는 따로 건드리지 않고 ESP32의 MAC address를 알아내 공유기의 DHCP가 고정된 IP를 할당하도록 해 주는 방법도 있다. 하지만 이 경우 IP는 일정하지만 DHCP가 매번 동작하기 때문에 deep sleep에서 깨어나는 경우 배터리 소모를 피할 수 없다.

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(좌측화면)가 수신한 데이터를 시리얼 포트에 출력하고 그 내용을 클라이언트로 되돌려 보내 준다. 파이썬 클라이언트는 서버에서 받은 데이터 내용을 화면에 출력한 것이다.