아두이노에 ESP01 와이파이 모듈을 연결시키고 ESP01의 webserver에 접속하여 아두이노의 디지털 핀을 원격 제어하도록 해보자.
스마트폰에서 ESP01의 webserver에 접속하는 방법에는 스마트폰이 연결된 공유기를 통해 내부 네트워크 상의 ESP01의 webserver에 접속하는 방법과 스마트폰을 ESP01의 soft AP에 직접 연결하여 webserver에 접속하는 두 가지 방법 중 하나를 사용할 수 있다. 이는 ESP01이 공유기와 연결하기 위한 station 모드와 스마트폰과 같은 단말기의 연결을 허용하기 위한 soft AP(액세스 포인트) 모드를 지원함으로써 가능하며, 두 모드 중 각각의 모드를 선택하여 활성화하거나 또는 동시에 두 모드를 활성화하여 공유기에 연결한 상태에서 스마트폰과의 연결도 할 수 있다. 하지만 ESP01 모듈이 시리얼 통신을 통해 아두이노와 연결되어 있다는 점과 모듈 자체의 하드웨어적인 한계 또는 와이파이 라이브러리의 문제로 인하여 아두이노가 데이터를 수신할 때에는 큰 불편함이 없으나 아두이노에서 데이터 송신 시 그 시작과 종료에 약 5초 정도의 시간이 소요되어 실제 사용상에는 무리가 있을 수 있다. 실제 데이터 전송에는 약 1 ~ 2초 정도 소요되나 클라이언트를 종료시키는 과정에서 3초 정도의 딜레이가 발생하였다(라이브러리 문제일 가능성이 있다).
아두이노와 Wifi 모듈 ESP01 연결
ESP01의 시리얼 통신 보드 레이트가 9600으로 설정되어 있어야 한다. ESP01의 연결 설정 및 자세한 방법에 관해서는 이전 글 아두이노 - ESP01 wifi 모듈 무선 원격제어 그리고 시리얼 통신 - 6편을 참조하기 바란다.
아두이노에서 WebServer를 이용하기 위해 WiFiEsp 라이브러리를 사용하겠다.
아래 사이트에서 WiFiEsp 라이브러리를 다운로드하자.
https://github.com/bportaluri/WiFiEsp
다운로드 후 압축을 풀면 아래와 같이 WiFiEsp-master 폴더 안에 똑같은 WiFiEsp-master 있는데 안의 폴더명에서 "-master"삭제하여 WiFiEsp로 변경한 뒤 폴더를 복사한다.
내 컴퓨터의 아두이노 저장 폴더 -> 라이브러리 폴더로 이동하여 붙여 넣기를 해준다.
C:\Program Files (x86)\Arduino\libraries
아두이노 IDE가 실행되고 있다면 아두이노 IDE를 반드시 종료하고 다시 실행해 주어야만 라이브러리가 반영된다.
ESP01을 station 모드로 공유기에 연결하고 내부 네트워크 이용 원격 제어하기
우선 ESP01이 station 모드로 공유기에 연결된 상태(내부 네트워크)에서 webserver에 접속하여 디지털 핀을 원격 제어하는 흐름에 대해 살펴보자. 스마트폰에서 제어한다고 가정했을 때 ESP01이 공유기로부터 할당받은 내부 ip 주소를 스마트폰의 웹브라우저에 입력하면 웹브라우저(마스터)는 ESP01의 webserver에 연결 요청을 하고 이 요청을 받은 webserver는 클라이언트를 생성하고 데이터(기본 http 프로토콜)를 시리얼 통신을 통해 아두이노에 전송하게 된다. 아두이노는 클라이언트를 통해 데이터를 수신하고 디지털 핀 제어에 관련된 명령어가 있다면 제어를 실행하고 관련 코드에 따라 수신된 데이터에 대응하는 웹페이지(메시지 또는 디지털 핀의 상태)를 현재 연결이 되어있는 클라이언트를 통해 회신하게 된다.
이 웹페이지(회신 데이터)는 시리얼 통신을 통해 ESP01로 전송되고 다시 와이파이를 통해 스마트폰의 웹브라우저에 디지털 핀의 현재 상태가 반영된 웹페이지를 표시하게 되어 아두이노의 디지털 핀을 제어할 수 있게 된다.
아래 그림처럼 아두이노 IDE의 파일 -> 예제 -> WiFiEsp -> WebServerLed 예제를 실행시킨다.
아래 스케치는 상기 예제를 현재 아두이노와 ESP01 모듈의 연결 상태에 맞게 소프트 시리얼 코드들을 수정해 주고 기본 LED, 디지털 7번 핀을 제어하도록 수정하였으며 html 코드도 PROGMEM을 사용하여 Flash memory에서 직접 읽어 오도록 수정해주었다.
공유기 SSID와 비밀번호를 수정하고 업로드해보자.
#include "WiFiEsp.h"
#include <SoftwareSerial.h>
#define rxPin 3
#define txPin 2
SoftwareSerial esp01(txPin, rxPin); // SoftwareSerial NAME(TX, RX);
const char ssid[] = "SK_WiFiGIGA40F7"; // your network SSID (name)
const char pass[] = "1712037218"; // your network password
int status = WL_IDLE_STATUS; // the Wifi radio's status
#define ledPin 13
#define digiPin 7
WiFiEspServer server(80);
void setup() {
pinMode(ledPin, OUTPUT);
pinMode(digiPin, OUTPUT);
digitalWrite(ledPin, LOW);
digitalWrite(digiPin, LOW);
Serial.begin(9600); // initialize serial for debugging
esp01.begin(9600); //와이파이 시리얼
WiFi.init(&esp01); // initialize ESP module
while ( status != WL_CONNECTED) { // 약 10초동안 wifi 연결 시도
Serial.print(F("Attempting to connect to WPA SSID: "));
Serial.println(ssid);
status = WiFi.begin(ssid, pass); // Connect to WPA/WPA2 network
IPAddress ip = WiFi.localIP();
Serial.print("IP Address: ");
Serial.println(ip);
}
server.begin();
}
const char HTTP_HEAD[] PROGMEM = "<!DOCTYPE html><html lang=\"en\"><head>"
"<meta name=\"viewport\"content=\"width=device-width,initial-scale=1,user-scalable=no\"/>"
"<link rel=\"icon\" href=\"data:,\">";
const char HTTP_STYLE[] PROGMEM = "<style>"
"body{text-align:center;font-family:verdana;}"
"button{border:0;border-radius:0.3rem;background-color:#1fa3ec;color:#fff;line-height:2.4rem;font-size:1.2rem;width:100%} "
"</style>";
const char HTTP_HEAD_END[] PROGMEM = "</head><body><div style=\"text-align:center;display:inline-block;min-width:260px;\">"
"<h3>ESP01 Digital Pin Control</h3>";
const char BUTTON_TYPE[] PROGMEM = "<p><button style=\"width:40%;background-color:#12cb3d;\">ON</button>"
"<button style=\"margin-left:10%;width:40%;background-color:#1fa3ec;\">OFF</button></a></p>";
const char BUTTON_A_ON[] PROGMEM = "<p>Button A</p><a href=\"/A/0\"><button style=\"background-color:#12cb3d;\">ON</button></a></p>";
const char BUTTON_A_OFF[] PROGMEM = "<p>Button A</p><a href=\"/A/1\"><button style=\"background-color:#1fa3ec;\">OFF</button></a></p>";
const char BUTTON_B_ON[] PROGMEM = "<p>Button B</p><a href=\"/B/0\"><button style=\"background-color:#12cb3d;\">ON</button></a></p>";
const char BUTTON_B_OFF[] PROGMEM = "<p>Button B</p><a href=\"/B/1\"><button style=\"background-color:#1fa3ec;\">OFF</button></a></p>";
const char HTTP_END[] PROGMEM = "</div></body></html>";
bool button_a = LOW; // off
bool button_b = LOW; // off
void loop() {
WiFiEspClient client = server.available(); // listen for incoming clients
if (client) { // if you get a client,
while (client.connected()) { // loop while the client's connected
if (client.available()) { // if there's bytes to read from the client,
String income_AP = client.readStringUntil('\n');
if (income_AP.indexOf(F("A/1")) != -1) {
Serial.println(F("button_A on"));
button_a = HIGH;
digitalWrite(ledPin, button_a);
} else if (income_AP.indexOf(F("A/0")) != -1) {
Serial.println(F("button_A off"));
button_a = LOW;
digitalWrite(ledPin, button_a);
} else if (income_AP.indexOf(F("B/1")) != -1) {
Serial.println(F("button_B on"));
button_b = HIGH;
digitalWrite(digiPin, button_b);
} else if (income_AP.indexOf(F("B/0")) != -1) {
Serial.println(F("button_B off"));
button_b = LOW;
digitalWrite(digiPin, button_b);
}
client.flush();
client.println(F("HTTP/1.1 200 OK")); // HTTP 프로토콜 헤더
client.println(F("Content-type:text/html"));
client.println(F("Connection: close"));
client.println();
String page;
page = (const __FlashStringHelper *)HTTP_HEAD;
page += (const __FlashStringHelper *)HTTP_STYLE;
page += (const __FlashStringHelper *)HTTP_HEAD_END;
page += (const __FlashStringHelper *)BUTTON_TYPE;
if (button_a == HIGH) {
page += (const __FlashStringHelper *)BUTTON_A_ON;
} else {
page += (const __FlashStringHelper *)BUTTON_A_OFF;
}
if (button_b == HIGH) {
page += (const __FlashStringHelper *)BUTTON_B_ON;
} else {
page += (const __FlashStringHelper *)BUTTON_B_OFF;
}
page += (const __FlashStringHelper *)HTTP_END;
client.print(page);
client.println();
delay(1);
break;
}
}
client.stop();
Serial.println(F("Client disconnected"));
}
}
스케치가 업로드되고 아두이노가 초기화를 시작하면 ESP01 역시 초기화와 공유기 연결을 진행하고 아래와 같이 공유기로부터 할당받은 ip 주소를 출력한다. 필자는 공유기로부터 "192.168.35.176"이라는 ip주소를 할당받았다. 공유기마다 할당해주는 ip주소는 다르게 된다.
2019.11.22 수정
상기 그림의 메시지 "[WiFiEsp] TIMEOUT: 458"가 출력되지 않도록 코드를 수정해 주었다. 아래 수신 데이터 전문을 살펴보자.
[WiFiEsp] New client 0 GET /A/0 HTTP/1.1 Host: 192.168.35.176 Connection: keep-alive Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Linux; Android 8.0.0; SAMSUNG SM-G930S) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/10.1 Chrome/71.0.3578.99 Mobile Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 Referer: http://192.168.35.176/A/0 Accept-Encoding: gzip, deflate Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7 |
버튼을 눌렀을때 수신되는 데이터이다. 여기서 client.readStringUntil('\n'); 코드를 사용하면 "GET /A/0 HTTP/1.1" 데이터가 수신된다. 그 이후의 나머지 데이터는 버퍼에 남아있게 되는데 이게 문제가 되어 "[WiFiEsp] TIMEOUT: 458" 와 같은 메시지가 출력되었었다. 클라이언트 종료 직전에 client.flush(); 코드를 써 주어 남아있는 데이터를 삭제 하고자 했었지만 client.flush(); 코드를 사용하여 남아있는 데이터를 삭제해 주는데 시간이 소요되어 그 같은 메시지가 표시 되었던것 같다. 코드의 위치를 디지털 핀 작동 후 버튼의 상태를 표시하는 html 코드 전송 전으로 이동해 주어 메시지가 표시 되지 않도록 해주었으며 이로인해 핀을 제어한 다음에 남아 있는 데이터를 삭제해주고, 버튼 상태 html 코드를 웹브라우저에 송신한 다음 클라이언트가 정상적으로 종료되게 된다. 딜레이도 약간 감소한것 같다.
이제 내부 네트워크에 연결된 스마트폰의 웹브라우저나 PC의 웹브라우저 주소창에 상기의 ip주소를 입력해주면 연결 상태가 시리얼 모니터에 표시되며 이때 아두이노는 정해진 코드에 따라 html 코드를 클라이언트를 통해 웹브라우저(마스터)에게 전송하고 웹브라우저는 아래 그림처럼 아두이노로부터 수신한 html을 표시하게 된다.
작동 흐름을 살펴보면 웹브라우저가 주소 "192.168.35.176"로 연결을 시도하게 되면 ESP01의 webserver는 요청을 수신하고 client를 생성하고 아두이노에 root값("192.168.35.176" 이후 추가된 주소가 없다)인 "GET / HTTP/1.1"이라는 스트링 메시지를 시리얼 통신을 통해 전송하게 된다. 아두이노 클라이언트 수신 코드에 따라 "GET /" 이후에 아무런 메시지가 없으므로 초기 html 코드를 ESP01 클라이언트를 통해 웹브라우저에 전송해 준다. 웹브라우저는 수신한 html 코드에 따라 페이지를 출력한다.
Button_A를 클릭하면 웹브라우저는 "192.168.35.176/A/1"주소로 연결을 시도하고 이 연결에 대한 주소 값이 아두이노에 전송되어 아두이노에서는 "GET /A/1 HTTP/1.1"이라는 스트링 메시지를 수신하게 된다. 이 메시지 중 "A/1" 스트링에 의해 아두이노 기본 LED를 켜는 코드가 작동되어 LED가 켜지게 되고 LED의 상태가 반영된 html 코드를 다시 웹브라우저에 송신하여 웹브라우저에 현재 버튼 상태 값이 반영된 웹페이지를 출력하게 한다.
핵심 코드 분석
웹브라우저에 전송하는 html 코드를 PROGMEM 지시어를 사용하여 Flash Memory에서 직접 읽도록 하였다. RPOGMEM 지시어에 관한 사항은 이전 글 아두이노 - 메모리(SRAM) 공간 확보를 위한 PROGMEM과 F() 매크로의 사용을 참조하기 바란다.
아래는 웹페이지의 스타일(폰트 크기, 버튼 색상 지정)을 지정해 주는 html 코드이다.
const char HTTP_STYLE[] PROGMEM = "<style>"
"body{text-align:center;font-family:verdana;}"
"button{border:0;border-radius:0.3rem;background-color:#1fa3ec;color:#fff;line-height:2.4rem;font-size:1.2rem;width:100%} "
"</style>";
아래는 버튼의 상태 값을 표시해주는 html 코드이다.
const char BUTTON_A_ON[] PROGMEM = "<p>Button A</p><a href=\"/A/0\"><button style=\"background-color:#12cb3d;\">ON</button></a></p>";
const char BUTTON_A_OFF[] PROGMEM = "<p>Button A</p><a href=\"/A/1\"><button style=\"background-color:#1fa3ec;\">OFF</button></a></p>";
const char BUTTON_B_ON[] PROGMEM = "<p>Button B</p><a href=\"/B/0\"><button style=\"background-color:#12cb3d;\">ON</button></a></p>";
const char BUTTON_B_OFF[] PROGMEM = "<p>Button B</p><a href=\"/B/1\"><button style=\"background-color:#1fa3ec;\">OFF</button></a></p>";
loop() 함수 내의 클라이언트 작동 코드를 살펴보자.
WiFiEspClient client = server.available(); |
loop() 함수를 scan 할 때마다 WiFiEspClient 객체 client를 생성하고 webserver에 웹 브라우저로부터 연결 요청이 있는지를 확인한다.
if (client) { 실행코드 } |
연결 요청이 있어 client 값이 ture가 되면 코드를 실행한다.
while (client.connected()) { 실행코드 } |
클라이언트가 웹브라우저(마스터)와 연결된 상태인 동안에는 while() 함수를 빠져나가지 않고 코드를 계속해서 실행한다.
String income_AP = client.readStringUntil('\n'); |
종료 문자를 만날 때까지 client 버퍼에 있는 데이터를 수신하고 스트링 변수 income_AP에 저장한다.
break; |
데이터 수신이 완료되면 while() 루프를 빠져나간다.
client.flush(); |
클라이언트를 통해 웹브라우저로 부터 데이터를 수신할 때 읽지 않고 버퍼에 남아있는 데이터를 삭제한다.
client.stop(); |
클라이언트를 종료한다.
아래는 loop() 함수 내의 클라이언트 작동 코드만 정리한 것이다.
WiFiEspClient client = server.available(); // listen for incoming clients if (client) { // if you get a client, while (client.connected()) { // loop while the client's connected if (client.available()) { // if there's bytes to read from the client, String income_AP = client.readStringUntil('\n'); break; client.flush(); } } client.stop(); Serial.println(F("Client disconnected")); } |
수신한 데이터를 저장한 스트링 변수 income_AP 내에 버튼 지정 코드 "A/0", "A/1", "B/0", "B/1"의 문자열이 있는지를 검색하고 문자열이 있다면 해당 코드를 실행한다.
if (income_AP.indexOf(F("A/1")) != -1) { 실행코드 } else if (income_AP.indexOf(F("A/0")) != -1) { 실행코드 } . . |
실행 코드에는 LED를 켜고 끄는 상태 변경 변수 button_a 또는 button_b가 있고 이 상태 변수에 의해 버튼의 상태가 반영된 html 코드를 지정하고 전송하게 된다.
client.println(F("HTTP/1.1 200 OK")); // HTTP 프로토콜 헤더 client.println(F("Content-type:text/html")); client.println(F("Connection: close")); client.println(); |
HTTP 프로토콜 헤더이다. "HTTP/1.1 200 OK" 이 코드에 의해 웹브라우저는 html 형식 코드라는 것을 인지하고 데이터를 수신한다. "Connection: close"는 데이터 수신 후 연결을 종료한다는 명령어이다. 또한 스트링 html 코드에 F() 매크로를 사용하여 Flash Memory에서 바로 읽어서 스트링을 전송할 수 있도록 하였다.
String page; page = (const __FlashStringHelper *)HTTP_HEAD; page += (const __FlashStringHelper *)HTTP_STYLE; . . client.print(page); |
스트링 변수 page를 선언하고 PROGMEM 지시어에 의해 저장된 html 코드를 읽고 스트링을 모두 더한다음 클라이언트를 통해 데이터를 전송하는 코드이다.
if (button_a == HIGH) { page += (const __FlashStringHelper *)BUTTON_A_ON; } else { page += (const __FlashStringHelper *)BUTTON_A_OFF; } |
변수 button_a의 상태에 따라 버튼의 표시를 하기 위한 html 코드를 스트링 변수 page에 더해주는 코드이다.
상기의 연결 설명을 하는 그림에서 스마트폰 항목에 "웹브라우저 / 원격제어 앱"이라는 항목이 있었다.
스마트폰에서 웹브라우저와 원격제어 앱에서 제어를 할 수 있도록 코드를 작성해 보았다. 이전 글아두이노 - ESP01 wifi 모듈 무선 원격제어 그리고 시리얼 통신 - 6편에서는 ESP01의 soft AP에 접속하여 제어를 했었었다. 당연한 얘기지만 내부 네트워크로 연결이 되어 있다면 원격제어 앱에서도 내부 내트워크 ip주소로 연결하여 제어할 수 있다. 상기의 코드에 이전 글에서 사용했던 코드를 접목하여 안드로이드 원격제어 앱에서도 같은 제어를 할 수 있게 해 보자.
다만, 이전 글에서는 WiFiEsp.h 라이브러리를 사용하지 않고 직접 ESP01을 제어했으므로 PWM의 순차적이고 연속적인 데이터를 받을 수 있도록 수신 id 컨트롤을 할 수 있었지만 WiFiEsp.h 라이브러리를 사용하는 연결에서는 id 컨트롤을 라이브러리가 관리함으로 인해 연속적으로 들어오는 데이터를 순서대로 받을 수가 없었다. 따라서 PWM 제어 코드는 삭제하고 디지털 버튼 두 개만 사용하도록 코드를 수정해 주었다.
사실 이글의 핵심은 아두이노에서 html 코드를 웹브라우저로 전송하는 것이다. 이전 글의 코드는 데이터의 수신에 중점을 두어 제어의 응답성을 극대화시킨 반면 송신(아두이노에서 -> 앱)은 전혀 염두에 두지 않은 코드이다. 물론 앱에서는 버튼의 표시등이 앱에 의해 기본적으로 표현되므로 굳이 아두이노로부터 페이지의 표현 등을 위한 html 코드를 수신할 필요는 없다. 따라서 앱에서 제어하는 경우에는 아두이노에서 버튼의 표현에 관한 html 코드를 앱으로 전송할 필요가 없게 된다.
아래 코드를 업로드하고 웹브라우저에 공유기에서 할당받은 ip주소(예. 192.168.35.176)를 입력하고 ESP01과 연결하여 제어를 해보고 안드로이드 앱에서도 제어를 해보자. 안드로이드 앱의 경우 이전 글을 참조하여 와이파이 연결 주소를 192.168.4.1이 아닌 할당받은 ip주소를 입력하면 된다. 한 가지 유의할 점은 웹브라우저와 안드로이드 앱이 서로 정보를 공유할 수는 없다는 것이다. 웹브라우저와 안드로이드 앱이 동시에 ESP01에 연결 요청을 한다고 할지라도 생성되는 클라이언트는 서로 다르고 독립적이다. 웹브라우저의 연결 요청에 의해 생성된 클라이언트를 통해서는 웹브라우저에게만 데이터를 송신할 수 있고 안드로이드 앱의 요청에 의해 생성된 클라이언트는 안드로이드 전용으로만 사용될 수 있다. 즉. 웹브라우저에서 버튼을 눌렀을 때 버튼의 상태를 웹브라우저에 의해 생성된 클라이언트를 통해서는 안드로이드 앱에 송신할 수 없고 반대의 경우도 마찬가지이다.
두 연결에 의한 작동상태를 확인해 보기만 하면 된다.
#include "WiFiEsp.h"
#include <SoftwareSerial.h>
#define rxPin 3
#define txPin 2
SoftwareSerial esp01(txPin, rxPin); // SoftwareSerial NAME(TX, RX);
const char ssid[] = "SK_WiFiGIGA40F7"; // your network SSID (name)
const char pass[] = "1712037218"; // your network password
int status = WL_IDLE_STATUS; // the Wifi radio's status
#define ledPin 13
#define digiPin 7
WiFiEspServer server(80);
uint8_t pin_val = 0; // 디지털 버튼 제어용 변수
String text = "";
void setup() {
pinMode(ledPin, OUTPUT);
pinMode(digiPin, OUTPUT);
digitalWrite(ledPin, LOW);
digitalWrite(digiPin, LOW);
Serial.begin(9600); // initialize serial for debugging
esp01.begin(9600); //와이파이 시리얼
WiFi.init(&esp01); // initialize ESP module
while ( status != WL_CONNECTED) { // 약 10초동안 wifi 연결 시도
Serial.print(F("Attempting to connect to WPA SSID: "));
Serial.println(ssid);
status = WiFi.begin(ssid, pass); // Connect to WPA/WPA2 network
IPAddress ip = WiFi.localIP();
Serial.print("IP Address: ");
Serial.println(ip);
}
server.begin();
}
const char HTTP_HEAD[] PROGMEM = "<!DOCTYPE html><html lang=\"en\"><head>"
"<meta name=\"viewport\"content=\"width=device-width,initial-scale=1,user-scalable=no\"/>"
"<link rel=\"icon\" href=\"data:,\">";
const char HTTP_STYLE[] PROGMEM = "<style>"
"body{text-align:center;font-family:verdana;}"
"button{border:0;border-radius:0.3rem;background-color:#1fa3ec;color:#fff;line-height:2.4rem;font-size:1.2rem;width:100%} "
"</style>";
const char HTTP_HEAD_END[] PROGMEM = "</head><body><div style=\"text-align:center;display:inline-block;min-width:260px;\">"
"<h3>ESP01 Digital Pin Control</h3>";
const char BUTTON_TYPE[] PROGMEM = "<p><button style=\"width:40%;background-color:#12cb3d;\">ON</button>"
"<button style=\"margin-left:10%;width:40%;background-color:#1fa3ec;\">OFF</button></a></p>";
const char BUTTON_A_ON[] PROGMEM = "<p>Button A</p><a href=\"/A/0\"><button style=\"background-color:#12cb3d;\">ON</button></a></p>";
const char BUTTON_A_OFF[] PROGMEM = "<p>Button A</p><a href=\"/A/1\"><button style=\"background-color:#1fa3ec;\">OFF</button></a></p>";
const char BUTTON_B_ON[] PROGMEM = "<p>Button B</p><a href=\"/B/0\"><button style=\"background-color:#12cb3d;\">ON</button></a></p>";
const char BUTTON_B_OFF[] PROGMEM = "<p>Button B</p><a href=\"/B/1\"><button style=\"background-color:#1fa3ec;\">OFF</button></a></p>";
const char HTTP_END[] PROGMEM = "</div></body></html>\r\n";
bool button_a = LOW; // off
bool button_b = LOW; // off
void loop() {
WiFiEspClient client = server.available(); // listen for incoming clients
if (client) { // if you get a client,
while (client.connected()) { // loop while the client's connected
if (client.available()) { // if there's bytes to read from the client,
String income_wifi = client.readStringUntil('\n');
bool browser = false;
if (income_wifi.indexOf("%%F0") != -1) {
String wifi_temp = income_wifi.substring(income_wifi.indexOf("%%F0")+4, income_wifi.indexOf("%%F1"));
pin_val = wifi_temp.toInt();
pin_control();
} else if (income_wifi.indexOf(F("A/1")) != -1) {
Serial.println(F("button_A on"));
button_a = HIGH; browser = true;
digitalWrite(ledPin, button_a);
} else if (income_wifi.indexOf(F("A/0")) != -1) {
Serial.println(F("button_A off"));
button_a = LOW; browser = true;
digitalWrite(ledPin, button_a);
} else if (income_wifi.indexOf(F("B/1")) != -1) {
Serial.println(F("button_B on"));
button_b = HIGH; browser = true;
digitalWrite(digiPin, button_b);
} else if (income_wifi.indexOf(F("B/0")) != -1) {
Serial.println(F("button_B off"));
button_b = LOW; browser = true;
digitalWrite(digiPin, button_b);
} else {
String wifi_temp = income_wifi.substring(income_wifi.indexOf("GET /")+5, income_wifi.indexOf("HTTP/1.1"));
if (wifi_temp != " ") {
if (wifi_temp.indexOf("%20") != -1) {
String space = "%20";
String space_convert = " ";
wifi_temp.replace(space, space_convert);
}
text = wifi_temp;
Serial.println(text);
} else {
browser = true;
}
}
client.flush();
if (browser == true) {
client.println(F("HTTP/1.1 200 OK")); // HTTP 프로토콜 헤더
client.println(F("Content-type:text/html"));
client.println(F("Connection: close"));
client.println();
String page;
page = (const __FlashStringHelper *)HTTP_HEAD;
page += (const __FlashStringHelper *)HTTP_STYLE;
page += (const __FlashStringHelper *)HTTP_HEAD_END;
page += (const __FlashStringHelper *)BUTTON_TYPE;
if (button_a == HIGH) {
page += (const __FlashStringHelper *)BUTTON_A_ON;
} else {
page += (const __FlashStringHelper *)BUTTON_A_OFF;
}
if (button_b == HIGH) {
page += (const __FlashStringHelper *)BUTTON_B_ON;
} else {
page += (const __FlashStringHelper *)BUTTON_B_OFF;
}
page += (const __FlashStringHelper *)HTTP_END;
client.print(page);
client.println();
delay(1);
}
break;
}
}
client.stop();
Serial.println(F("Client disconnected"));
}
}
void pin_control() {
if (pin_val != 0) {
switch (pin_val) {
case 11: digitalWrite(ledPin, HIGH); // button 1 : on
Serial.println("App Button_A ON");
break;
case 10: digitalWrite(ledPin, LOW); // button 1 : off
Serial.println("App Button_A OFF");
break;
case 21: Serial.println("button 2 : on");
Serial.println("App Button_B ON");
break;
case 20: Serial.println("button 2 : off");
Serial.println("App Button_B OFF");
break;
}
pin_val = 0;
}
}
station 모드를 통한 원격제어의 장점은 사용자가 ESP01에 접속을 할 때 직접 연결을 하는 게 아니라 공유기를 거쳐서 한다는 점이다. 스마트폰이 공유기와 연결되어 있는 상태에서는 별도로 와이파이 연결 프로그램을 실행시키지 않고도 아두이노를 켜고 ESP01이 공유기와 연결되면 해당 ip주소만 입력하여 제어를 할 수 있게 된다. soft AP를 이용할 경우에는 매번 와이파이 프로그램을 통해 기존 와이파이 연결을 끊고 다시 soft AP에 연결을 해야 하며 이 작업이 은근히 많을 시간을 소요할 뿐만 아니라 스트레스를 유발하기도 한다.
하지만 station 모드에서 연결하고 제어하는 방법에는 큰 문제점이 있는데 웹브라우저에서 연결할 ip주소를 사용자가 임의대로 지정할 수 없다는 것이다(공유기의 설정을 할 수 없다고 가정했을 때). ESP01이 공유기에 SSID와 비밀번호를 통해 연결할 때 공유기의 DHCP 서버가 ESP01이 사용할 ip주소를 할당해주게 되므로 아두이노가 실행되고 와이파이가 연결되기 전에는 ip주소를 확인할 수가 없다. 연결된 뒤에도 시리얼 모니터에 표시된 메시지를 통해서 확인해야 하므로 ip주소를 확인하기 위해서는 아두이노가 컴퓨터와 연결되고 시리얼 모니터가 켜진 상태여야만 한다. 이는 아두이노를 독립적인 전원을 통해 단독 실행할 경우 ESP01이 공유기로부터 할당받은 ip주소를 확인할 수 없게 되어 웹브라우저에서 연결조차 하지 못하는 상황이 발생할 수 있다. 보통 한번 받은 ip 주소가 다음번에도 같을 가능성이 높긴 하지만 그렇더라도 공유기가 바뀐 환경 즉 다른 장소에서 아두이노를 켜고 제어를 할 때에는 ip주소를 확인할 수 없게 된다.
ESP01의 soft AP에 연결하여 원격 제어하기
이번에는 스마트폰에서 ESP01의 soft AP 연결을 통해 webserver에 연결하고 제어를 해보자. 앞선 코드에서 와이파이 연결부분만 수정을 해주면 된다.
soft AP 연결 설정 코드
const char ssid_AP[] = "esp01_AP"; // your network SSID (name) const char pass_AP[] = "1234test"; // your network password WiFi.beginAP(ssid_AP, 10, pass_AP, ENC_TYPE_WPA2_PSK); // 10:채널, IPAddress ap = WiFi.localIP(); Serial.print(F("AP Address: ")); Serial.println(ap); |
soft AP의 기본 ip 주소는 192.168.4.1이다.
만약 상기 주소가 아닌 특정 주소로 변경하고 싶다면 아래의 코드를 적용해주면 된다.
IPAddress localIp(192, 168, 111, 111); // softAP 접속 IP 설정 WiFi.configAP(localIp); |
#include "WiFiEsp.h"
#include <SoftwareSerial.h>
#define rxPin 3
#define txPin 2
SoftwareSerial esp01(txPin, rxPin); // SoftwareSerial NAME(TX, RX);
const char ssid_AP[] = "esp01_AP"; // your network SSID (name)
const char pass_AP[] = "1234test"; // your network password
#define ledPin 13
#define digiPin 7
WiFiEspServer server(80);
void setup() {
pinMode(ledPin, OUTPUT);
pinMode(digiPin, OUTPUT);
digitalWrite(ledPin, LOW);
digitalWrite(digiPin, LOW);
Serial.begin(9600); // initialize serial for debugging
esp01.begin(9600); //와이파이 시리얼
WiFi.init(&esp01); // initialize ESP module
// IPAddress localIp(192, 168, 111, 111); // softAP 접속 IP 설정
// WiFi.configAP(localIp);
WiFi.beginAP(ssid_AP, 10, pass_AP, ENC_TYPE_WPA2_PSK); // 10:채널,
IPAddress ap = WiFi.localIP();
Serial.print(F("AP Address: "));
Serial.println(ap);
server.begin();
}
const char HTTP_HEAD[] PROGMEM = "<!DOCTYPE html><html lang=\"en\"><head>"
"<meta name=\"viewport\"content=\"width=device-width,initial-scale=1,user-scalable=no\"/>"
"<link rel=\"icon\" href=\"data:,\">";
const char HTTP_STYLE[] PROGMEM = "<style>"
"body{text-align:center;font-family:verdana;}"
"button{border:0;border-radius:0.3rem;background-color:#1fa3ec;color:#fff;line-height:2.4rem;font-size:1.2rem;width:100%} "
"</style>";
const char HTTP_HEAD_END[] PROGMEM = "</head><body><div style=\"text-align:center;display:inline-block;min-width:260px;\">"
"<h3>ESP01 Digital Pin Control</h3>";
const char BUTTON_TYPE[] PROGMEM = "<p><button style=\"width:40%;background-color:#12cb3d;\">ON</button>"
"<button style=\"margin-left:10%;width:40%;background-color:#1fa3ec;\">OFF</button></a></p>";
const char BUTTON_A_ON[] PROGMEM = "<p>Button A</p><a href=\"/A/0\"><button style=\"background-color:#12cb3d;\">ON</button></a></p>";
const char BUTTON_A_OFF[] PROGMEM = "<p>Button A</p><a href=\"/A/1\"><button style=\"background-color:#1fa3ec;\">OFF</button></a></p>";
const char BUTTON_B_ON[] PROGMEM = "<p>Button B</p><a href=\"/B/0\"><button style=\"background-color:#12cb3d;\">ON</button></a></p>";
const char BUTTON_B_OFF[] PROGMEM = "<p>Button B</p><a href=\"/B/1\"><button style=\"background-color:#1fa3ec;\">OFF</button></a></p>";
const char HTTP_END[] PROGMEM = "</div></body></html>";
bool button_a = LOW; // off
bool button_b = LOW; // off
void loop() {
WiFiEspClient client = server.available(); // listen for incoming clients
if (client) { // if you get a client,
while (client.connected()) { // loop while the client's connected
if (client.available()) { // if there's bytes to read from the client,
String income_AP = client.readStringUntil('\n');
if (income_AP.indexOf(F("A/1")) != -1) {
Serial.println(F("button_A on"));
button_a = HIGH;
digitalWrite(ledPin, button_a);
} else if (income_AP.indexOf(F("A/0")) != -1) {
Serial.println(F("button_A off"));
button_a = LOW;
digitalWrite(ledPin, button_a);
} else if (income_AP.indexOf(F("B/1")) != -1) {
Serial.println(F("button_B on"));
button_b = HIGH;
digitalWrite(digiPin, button_b);
} else if (income_AP.indexOf(F("B/0")) != -1) {
Serial.println(F("button_B off"));
button_b = LOW;
digitalWrite(digiPin, button_b);
}
client.flush();
client.println(F("HTTP/1.1 200 OK"));
client.println(F("Content-type:text/html"));
client.println(F("Connection: close"));
client.println();
String page;
page = (const __FlashStringHelper *)HTTP_HEAD;
page += (const __FlashStringHelper *)HTTP_STYLE;
page += (const __FlashStringHelper *)HTTP_HEAD_END;
page += (const __FlashStringHelper *)BUTTON_TYPE;
if (button_a == HIGH) {
page += (const __FlashStringHelper *)BUTTON_A_ON;
} else {
page += (const __FlashStringHelper *)BUTTON_A_OFF;
}
if (button_b == HIGH) {
page += (const __FlashStringHelper *)BUTTON_B_ON;
} else {
page += (const __FlashStringHelper *)BUTTON_B_OFF;
}
page += (const __FlashStringHelper *)HTTP_END;
client.print(page);
client.println();
delay(1);
break;
}
}
client.stop();
Serial.println(F("Client disconnected"));
}
}
상기 코드를 아두이노에 업로드해주고 아래 그림처럼 스마트폰의 와이파이 연결 프로그램에서 "esp01_AP"에 연결을 한다.
웹브라우저에서 192.168.4.1을 입력하고 연결하여 원격제어를 해보면 된다.
soft AP 연결을 통해 ip주소를 확인하고 내부 네트워크를 통해 원격 제어하기
이번에는 station모드와 soft AP 모드를 동시에 사용할 수 있도록 ESP01에 설정을 하고 station 모드에서는 원격제어를 하고 soft AP 연결을 통해서는 공유기에서 할당받은 ip주소를 확인하도록 하여 시리얼 모니터 없이도 ip주소를 확인하고 station 모드를 통하여 제어를 할 수 있도록 코드를 작성해 보자.
- soft AP 연결
- station 연결
WiFiEsp.h 라이브러리에 정의된 soft AP 시작 옵션은 아래와 같다.
int WiFiEspClass::beginAP(const char* ssid, uint8_t channel, const char* pwd, uint8_t enc, bool apOnly) if(apOnly) espMode = 2; // softAP 모드 else espMode = 3; // softAP와 station 동시 사용 |
따라서 softAP와 station을 동시에 사용하기 위해서는 soft AP 시작 함수에서 모드 설정을 해주어야 한다.
WiFi.beginAP(ssid_AP, 10, pass_AP, ENC_TYPE_WPA2_PSK, false); // 10:채널, false: 모드 설정 |
ip 주소는 자료형 IPAddress 변수 ip에 32비트 int값으로 저장된다.
WiFiEsp.h 라이브러리는 ip 주소를 아래 함수로 입력받는다.
IPAddress(uint8_t first_octet, uint8_t second_octet, uint8_t third_octet, uint8_t fourth_octet); // 입력
저장은 uint32_t address에 저장한다.
32비트 ip 주소값을 스트링 값으로 변환하기 위해 아래와 같은 코드를 추가해 주었다.
String stIp = "000.000.000.000"; String toStringIp(IPAddress ip) { String res = ""; for (int i = 0; i < 3; i++) { res += String((ip >> (8 * i)) & 0xFF) + "."; } res += String(((ip >> 8 * 3)) & 0xFF); return res; } |
이전 코드에 웹브라우저에서 "192.168.4.1/ip"를 입력하면 공유기에서 할당받은 ip주소를 전송해주는 코드를 추가해 주었다.
if (income_AP.indexOf(F("ip")) != -1) { if (status == WL_CONNECTED) { page += F("<p>IP Address: "); page += stIp; page += F("</p>"); } else page += F("<p>Connection failed"); }else { 실행코드 } |
아래 스케치를 업로드하고 스마트폰에서 spft AP 연결한 뒤 웹브라우저에서 "192.168.4.1/ip"를 입력해서 ip주소를 확인하고 다시 스마트폰을 공유기에 연결한 다음 웹브라우저에 확인한 ip 주소를 입력하여 제어를 해보자.
#include "WiFiEsp.h"
#include <SoftwareSerial.h>
#define rxPin 3
#define txPin 2
SoftwareSerial esp01(txPin, rxPin); // SoftwareSerial NAME(TX, RX);
const char ssid[] = "SK_WiFiGIGA40F7"; // your network SSID (name)
const char pass[] = "1712037218"; // your network password
int status = WL_IDLE_STATUS; // the Wifi radio's status
const char ssid_AP[] = "esp01_AP"; // your network SSID (name)
const char pass_AP[] = "1234test"; // your network password
#define ledPin 13
#define digiPin 7
WiFiEspServer server(80);
String stIp = "000.000.000.000";
String toStringIp(IPAddress ip) {
String res = "";
for (int i = 0; i < 3; i++) {
res += String((ip >> (8 * i)) & 0xFF) + ".";
}
res += String(((ip >> 8 * 3)) & 0xFF);
return res;
}
void setup() {
pinMode(ledPin, OUTPUT);
pinMode(digiPin, OUTPUT);
digitalWrite(ledPin, LOW);
digitalWrite(digiPin, LOW);
Serial.begin(9600); // initialize serial for debugging
esp01.begin(9600); //와이파이 시리얼
WiFi.init(&esp01); // initialize ESP module
while ( status != WL_CONNECTED) { // 약 10초동안 wifi 연결 시도
Serial.print(F("Attempting to connect to WPA SSID: "));
Serial.println(ssid);
status = WiFi.begin(ssid, pass); // Connect to WPA/WPA2 network
IPAddress ip = WiFi.localIP();
Serial.print("IP Address: ");
stIp = toStringIp(ip);
Serial.println(stIp);
}
// IPAddress localIp(192, 168, 111, 111); // softAP 접속 IP 설정
// WiFi.configAP(localIp);
WiFi.beginAP(ssid_AP, 10, pass_AP, ENC_TYPE_WPA2_PSK, false); // 10:채널, false: softAP와 station 동시 사용모드
IPAddress ap = WiFi.localIP();
Serial.print("AP Address: ");
Serial.println(ap);
server.begin();
}
const char HTTP_HEAD[] PROGMEM = "<!DOCTYPE html><html lang=\"en\"><head>"
"<meta name=\"viewport\"content=\"width=device-width,initial-scale=1,user-scalable=no\"/>"
"<link rel=\"icon\" href=\"data:,\">";
const char HTTP_STYLE[] PROGMEM = "<style>"
"body{text-align:center;font-family:verdana;}"
"button{border:0;border-radius:0.3rem;background-color:#1fa3ec;color:#fff;line-height:2.4rem;font-size:1.2rem;width:100%} "
"</style>";
const char HTTP_HEAD_END[] PROGMEM = "</head><body><div style=\"text-align:center;display:inline-block;min-width:260px;\">"
"<h3>ESP01 Digital Pin Control</h3>";
const char BUTTON_TYPE[] PROGMEM = "<p><button style=\"width:40%;background-color:#12cb3d;\">ON</button>"
"<button style=\"margin-left:10%;width:40%;background-color:#1fa3ec;\">OFF</button></a></p>";
const char BUTTON_A_ON[] PROGMEM = "<p>Button A</p><a href=\"/A/0\"><button style=\"background-color:#12cb3d;\">ON</button></a></p>";
const char BUTTON_A_OFF[] PROGMEM = "<p>Button A</p><a href=\"/A/1\"><button style=\"background-color:#1fa3ec;\">OFF</button></a></p>";
const char BUTTON_B_ON[] PROGMEM = "<p>Button B</p><a href=\"/B/0\"><button style=\"background-color:#12cb3d;\">ON</button></a></p>";
const char BUTTON_B_OFF[] PROGMEM = "<p>Button B</p><a href=\"/B/1\"><button style=\"background-color:#1fa3ec;\">OFF</button></a></p>";
const char HTTP_END[] PROGMEM = "</div></body></html>";
bool button_a = LOW; // off
bool button_b = LOW; // off
void loop() {
WiFiEspClient client = server.available(); // listen for incoming clients
if (client) { // if you get a client,
while (client.connected()) { // loop while the client's connected
if (client.available()) { // if there's bytes to read from the client,
String income_AP = client.readStringUntil('\n');
if (income_AP.indexOf(F("A/1")) != -1) {
Serial.println(F("button_A on"));
button_a = HIGH;
digitalWrite(ledPin, button_a);
} else if (income_AP.indexOf(F("A/0")) != -1) {
Serial.println(F("button_A off"));
button_a = LOW;
digitalWrite(ledPin, button_a);
} else if (income_AP.indexOf(F("B/1")) != -1) {
Serial.println(F("button_B on"));
button_b = HIGH;
digitalWrite(digiPin, button_b);
} else if (income_AP.indexOf(F("B/0")) != -1) {
Serial.println(F("button_B off"));
button_b = LOW;
digitalWrite(digiPin, button_b);
}
client.flush();
client.println(F("HTTP/1.1 200 OK"));
client.println(F("Content-type:text/html"));
client.println(F("Connection: close"));
client.println();
String page;
page = (const __FlashStringHelper *)HTTP_HEAD;
page += (const __FlashStringHelper *)HTTP_STYLE;
page += (const __FlashStringHelper *)HTTP_HEAD_END;
if (income_AP.indexOf(F("ip")) != -1) {
if (status == WL_CONNECTED) {
page += F("<p>IP Address: ");
page += stIp;
page += F("</p>");
} else page += F("<p>Connection failed</p>");
}else {
page += (const __FlashStringHelper *)BUTTON_TYPE;
if (button_a == HIGH) page += (const __FlashStringHelper *)BUTTON_A_ON;
else page += (const __FlashStringHelper *)BUTTON_A_OFF;
if (button_b == HIGH) page += (const __FlashStringHelper *)BUTTON_B_ON;
else page += (const __FlashStringHelper *)BUTTON_B_OFF;
}
page += (const __FlashStringHelper *)HTTP_END;
client.print(page);
client.println();
delay(1);
break;
}
}
client.stop();
Serial.println(F("Client disconnected"));
}
}
웹페이지 응답 코드가 있는 아두이노 와이파이 제어에서 사용할 수 있는 연결 도우미 앱입니다.
https://play.google.com/store/apps/details?id=com.tistory.postpop.MCUWiFi
특징:
1. 스마트폰에서 아두이노 웹서버에 연결할 때 에러 메세지 출력 및 연결 가이드 제시
2. 아두이노 웹서버의 html 페이지 연결시 스마트폰의 웹브라우저 대신 앱 내부 웹 뷰어를 통해 연결 및 표시
3. 앱 실행전 스마트폰의 와이파이 실행 유무에 따라 앱 종료시 와이파이 설정 페이지 이동
자세한 설명은 아래 포스트를 참조하세요.
[Arduino] - 아두이노 와이파이 연결 도우미 앱
관련 글
[arduino] - 아두이노 - 와이파이, ESP01 wifi 모듈 무선 원격제어 그리고 시리얼 통신 - 6편
[arduino] - 아두이노 - 메모리(SRAM) 공간 확보를 위한 PROGMEM과 F() 매크로의 사용
[arduino] - 아두이노 - ESP01 모듈, 기상청 RSS / 오픈웨더맵 API 날씨 정보 받기
[arduino] - 아두이노 - 문자열의 이해와 표현 방법
[arduino] - 아두이노 - 와이파이 매니저, ESP01 soft AP를 통해 공유기 연결용 아이디와 비밀번호 설정하기
'Arduino' 카테고리의 다른 글
아두이노 - 와이파이, ESP01 wifi 모듈 무선 원격제어 그리고 시리얼 통신 - 6편 (9) | 2022.06.08 |
---|---|
arduino - Simple Melody 이용 피에조 부저 멜로디 코딩하기, Esp01, EEPROM (0) | 2020.09.19 |
ESP8266 / ESP32 - SPIFFS 파일시스템 라이브러리 예제 및 사용방법 (4) | 2020.03.22 |
아두이노 - ESP01 와이파이 매니저, soft AP이용 공유기 연결용 아이디와 비밀번호 설정하기, wifimanager (0) | 2019.11.24 |
아두이노 - 메모리(SRAM) 공간 확보를 위한 PROGMEM과 F() 매크로의 사용 (0) | 2019.11.17 |
아두이노 - 문자열의 이해와 표현 방법 (2) | 2019.11.14 |
아두이노 - EEPROM 사용하기 (0) | 2019.11.12 |
ESP8266 / ESP32 - 아두이노 IDE 코어 git 최신버전 설치하기 (0) | 2019.11.08 |