ESP8266 보드에 IP로 접속하면 체크박스가 달린 페이지가 뜨고, 체크를 바꾸는 순간 보드의 LED가 켜지거나 꺼지는 구조입니다. 같은 보드를 PC 쪽 프로그램에서 시리얼로도 제어할 수 있게 4바이트짜리 간단한 명령 형식을 하나 더 넣었습니다. 나중에 VB.NET으로 만든 관리 프로그램에서 시리얼 포트로 붙이거나, 공유기 안에서 휴대폰 브라우저로 기기를 켜고 끄는 용도로 확장하려고 만든 뼈대입니다.

준비한 것

보드는 알리익스프레스에서 산 "ESP8266 ESP-12E NodeMcu" 한 개입니다. USB만 꽂으면 바로 업로드가 되고, GPIO2 에 파란색 내장 LED가 붙어 있어서 따로 배선할 필요가 없습니다.

광고

ESP8266 ESP-12E NodeMcu 보드 제품 사진

개발 환경은 아두이노 IDE를 설치해서 썼고, ESP8266 코어는 3.1.2 버전을 썼습니다. 보드 매니저에서 esp8266을 검색해 설치하면 되고, 필요한 라이브러리는 라이브러리 매니저로 추가했습니다. 결과 확인은 IDE에 들어 있는 시리얼 모니터로 했습니다.

아두이노 IDE에서 ESP8266 스케치를 작성하는 화면

아두이노 스케치 전체

검색해 가며 이것저것 붙여서 만든 코드입니다. handleRoot() 는 접속하면 페이지 HTML을 통째로 내려주고, handleSubmit() 은 /api 로 들어온 POST 값을 보고 LED를 제어합니다. loop() 안에서는 웹 요청 처리와 시리얼 명령 처리를 번갈아 돌립니다.

/* ip로 접속 버튼 핸들링 */
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>

const char* ssid = "와이파이명";
const char* password = "와이파이비밀번호";
const int ledPin =  2;
boolean bLED = false;

// Create an instance of the server
// specify the port to listen on as an argument
ESP8266WebServer server(800);    // 일반적인 웹포트는 80입니다.

void handleRoot() {
  String html = "<!DOCTYPE html><html lang='ko'><head><meta http-equiv='content-type' content='text/html; charset=UTF-8'><title>ESP8266 :: WebServer</title><script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js'></script><link rel='stylesheet' type='text/css' href='../../../Arduino/main.css'></head><body><script type='text/javascript' src='http://code.jquery.com/jquery-3.5.1.min.js'></script><div class='container'><div class='inner'><div class='content'><form id='join_form' class='form btn_fixed' action='/api' method='POST'><div class='form_content'><div class='form_section'><div class='check_terms'><div class='check_wrap'><input type='checkbox' id='chk_all' name='chk_all' class='blind' checked='checked'><label for='chk_all'><span class='text check_all'>전체 켜기</span></label></div></div><ul class='terms_list'><li class='terms_item'><div class='check_terms'><div class='check_wrap'><input type='checkbox' id='d_1' class='blind' checked='checked'><label for='d_1'><em class='option point'>Device 1</em><div class='text_wrap'><span class='text'>Device 1</span></div></label></div></div></li><li class='terms_item'><div class='check_terms'><div class='check_wrap'><input id='d_2' name='d_2' type='checkbox' checked='checked'><label for='d_2'><em class='option point'>Device 2</em><div class='text_wrap'><span class='text'>Device 2</span></div></label></div></div></li><li class='terms_item'><div class='check_terms'><div class='check_wrap'><input type='checkbox' id='d_3' name='d_3' checked='checked'><label for='d_3'><em class='option'>Device 3</em><span class='text'>Device 3</span></label></div></div></li><li class='terms_item'><div class='check_terms'><div class='check_wrap'><input type='checkbox' id='d_4' name='d_4' checked='checked'><label for='d_4'><em class='option'>Device 4</em><div class='text_wrap'><span class='text'>Device 4</span></div></label></div></div></li><li class='terms_item'><div class='check_terms'><div class='check_wrap'><input type='checkbox' id='d_5' name='d_5' checked='checked'><label for='d_5'><em class='option'>Device 5</em><div class='text_wrap'><span class='text'>Device 5</span></div></label></div></div></li></ul></div></div><div class='btn_submit_wrap'><button type='button' id='submit' class='btn_submit'>적용</button></div></form></div><p id='message'></p></div></div><script src='../../../Arduino/main.js'></script></body></html>";

  server.send(200, "text/html", html);
}

void handleSubmit() {
  String message = "Hello from ESP8266!";
  message += "\nPOST variables:\n";

  if (server.hasArg("d_1") && server.hasArg("d_2")) {
    String d_1 = server.arg("d_1");
    String d_2 = server.arg("d_2");
    if (d_1 == "on") {
      bLED = true;
      digitalWrite(ledPin, LOW);  // LED 켜기
      Serial.println("LED is ON");
    } else if (d_1 == "off") {
      bLED = false;
      digitalWrite(ledPin, HIGH); // LED 끄기
      Serial.println("LED is OFF");
   }
    message += "d_1: " + d_1 + ", d_2: " + d_2 + "\n";
  } else {
    message += "No POST data received\n";
  }

  server.send(200, "text/plain", message);
}

void setup() {
  pinMode(ledPin, OUTPUT);
  Serial.begin(115200);
  delay(100);

  // WiFi 연결
  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.print("IP address: ");
  Serial.println(WiFi.localIP());

  // 핸들러 등록
  server.on("/", HTTP_GET, handleRoot);    // 루트 페이지 처리
  server.on("/api", HTTP_POST, handleSubmit); // POST 처리

  // 웹 서버 시작
  server.begin();
  Serial.println("HTTP server started");
}

void loop() {
  // 클라이언트 요청 처리
  server.handleClient();

  // 시리얼 데이터 수신 처리
  if (Serial.available() >= 4) {
    // 시리얼에서 4바이트 읽기
    byte data[4];
    for (int i = 0; i < 4; i++) {
      data[i] = Serial.read();
    }

    // 데이터 확인 및 처리
    if (data[0] == 241 && data[1] == 123 && data[3] == 242) {
      if (data[2] == 1) {
        bLED = true;
        digitalWrite(ledPin, LOW);  // LED 켜기
        Serial.println("Received: 241,123,1,242 - LED is ON");
      } else if (data[2] == 0) {
        bLED = false;
        digitalWrite(ledPin, HIGH); // LED 끄기
        Serial.println("Received: 241,123,0,242 - LED is OFF");
      }
    }
  }
}

처음 돌릴 때 헷갈리는 게 두 가지 있습니다. 하나는 LED 방향입니다. NodeMCU 내장 LED는 GPIO2 가 LOW일 때 켜지는 구조라서, 코드에서도 켤 때 LOW, 끌 때 HIGH 를 줍니다. 외부 LED를 GND 쪽으로 물리면 반대로 바꿔야 합니다. 다른 하나는 포트 번호입니다. 서버를 800 번으로 열었기 때문에 브라우저에서는 http://시리얼에 찍힌IP:800/ 처럼 포트를 붙여야 들어가집니다. 80으로 바꾸면 포트를 생략할 수 있습니다.

시리얼 명령은 241, 123, 값, 242 네 바이트로 정했습니다. 앞 두 바이트와 마지막 바이트를 시작·끝 표시로 써서 엉뚱한 바이트가 들어왔을 때 반응하지 않게 한 것이고, 세 번째 바이트가 1이면 켜고 0이면 끕니다. 시리얼 모니터는 문자를 보내기 때문에 이 바이트를 직접 넣기 어렵고, PC 쪽에서 SerialPort.Write(byte(), 0, 4) 같은 식으로 바이너리를 보내는 프로그램을 쓰는 게 편합니다. 다만 4바이트 단위로 끊어 읽기만 하므로, 중간에 바이트가 하나 빠지면 그 뒤로는 계속 어긋날 수 있다는 점은 알고 쓰셔야 합니다.

페이지 HTML과 외부 파일 분리

ESP8266 메모리에 CSS와 스크립트까지 다 넣기 싫어서, 보드는 뼈대 HTML만 내려주고 main.css 와 main.js 는 제 웹서버(../../../Arduino/)에서 불러오게 했습니다. 아래는 스케치 안의 문자열을 보기 좋게 풀어 쓴 것입니다.

<!DOCTYPE html>
<html lang="ko">

<head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
    <title>ESP8266 :: WebServer</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <link rel="stylesheet" type="text/css" href="../../../Arduino/main.css">
</head>

<body>
    <script type="text/javascript" src="http://code.jquery.com/jquery-3.5.1.min.js"></script>
    <div class="container">
        <div class="inner">
            <div class="content">
                <form id="join_form" class="form btn_fixed" action="/api" method="POST">
                    <div class="form_content">
                        <div class="form_section">
                            <div class="check_terms">
                                <div class="check_wrap"><input type="checkbox" id="chk_all" name="chk_all" class="blind" checked="checked"><label for="chk_all"><span class="text check_all">전체 켜기</span></label></div>
                            </div>
                            <ul class="terms_list">
                                <li class="terms_item">
                                    <div class="check_terms">
                                        <div class="check_wrap"><input type="checkbox" id="d_1" class="blind" checked="checked"><label for="d_1"><em class="option point">Device 1</em>
                                                <div class="text_wrap"><span class="text">Device 1</span></div>
                                            </label></div>
                                    </div>
                                </li>
                                <li class="terms_item">
                                    <div class="check_terms">
                                        <div class="check_wrap"><input id="d_2" name="d_2" type="checkbox" checked="checked"><label for="d_2"><em class="option point">Device 2</em>
                                                <div class="text_wrap"><span class="text">Device 2</span></div>
                                            </label></div>
                                    </div>
                                </li>
                                <li class="terms_item">
                                    <div class="check_terms">
                                        <div class="check_wrap"><input type="checkbox" id="d_3" name="d_3" checked="checked"><label for="d_3"><em class="option">Device 3</em><span class="text">Device 3</span></label></div>
                                    </div>
                                </li>
                                <li class="terms_item">
                                    <div class="check_terms">
                                        <div class="check_wrap"><input type="checkbox" id="d_4" name="d_4" checked="checked"><label for="d_4"><em class="option">Device 4</em>
                                                <div class="text_wrap"><span class="text">Device 4</span></div>
                                            </label></div>
                                    </div>
                                </li>
                                <li class="terms_item">
                                    <div class="check_terms">
                                        <div class="check_wrap"><input type="checkbox" id="d_5" name="d_5" checked="checked"><label for="d_5"><em class="option">Device 5</em>
                                                <div class="text_wrap"><span class="text">Device 5</span></div>
                                            </label></div>
                                    </div>
                                </li>
                            </ul>
                        </div>
                    </div>
                    <div class="btn_submit_wrap"><button type="button" id="submit" class="btn_submit">적용</button></div>
                </form>
            </div>
            <p id="message"></p>
        </div>
    </div>
    <script src="../../../Arduino/main.js"></script>
</body>

</html>

jQuery를 head와 body에서 두 번 부르고 있는데, 뒤에 로드된 3.5.1이 최종적으로 쓰이고 동작에는 문제가 없습니다. 정리할 때 하나만 남기면 됩니다. 또 d_1 체크박스에는 name 이 없어서 폼을 그대로 submit 하면 값이 빠지지만, 실제 전송은 아래 스크립트가 id 기준으로 직접 만들어 보내기 때문에 상관없습니다.

main.js는 체크박스를 클릭하거나 적용 버튼을 누를 때마다 전체 체크박스 상태를 id=on/off 형태로 묶어서 /api 로 POST 합니다. 응답으로 받은 텍스트는 #message 영역에 그대로 찍힙니다.

$('#chk_all').click(function(event) {
    if (this.checked) {
        $(':checkbox').each(function() {
            this.checked = true;
        });
    } else {
        $(':checkbox').each(function() {
            this.checked = false;
        });
    }
});

$(document).ready(function() {
    $('#submit').click(function() { // 적용 버튼을 눌렀을 때
        sendCheckboxData();
    });

    $('input[type=checkbox]').click(function(event) {
        sendCheckboxData();
    });

    // 체크박스 데이터 전송 함수
    function sendCheckboxData() {
        // 모든 체크박스의 id와 on/off 상태를 배열로 만든다
        let checkboxes = $('input[type=checkbox]').map(function() {
            let id = $(this).attr('id');
            let value = $(this).prop('checked') ? 'on' : 'off';
            return id + '=' + value;
        }).get();

        // 배열을 a=on&b=off 형태의 문자열로 합친다
        let sendData = checkboxes.join('&');

        $.ajax({
            type: 'post', // POST 방식
            url: '/api', // ESP8266 쪽 처리 주소
            data: sendData, // 위에서 만든 문자열 전송
            dataType: 'html', // 응답을 문자열로 받음
            success: function(data) { // 성공하면 응답 내용을 data 로 받는다
                $('#message').html(data); // id="message" 영역에 응답 표시
            }
        });
    }

});

CSS(main.css)는 체크박스 모양을 꾸미는 내용뿐이라 여기서는 생략합니다. 주의할 점은 이 방식이 접속하는 휴대폰이나 PC가 인터넷에 연결돼 있어야 한다는 겁니다. 외부 CSS·JS를 못 받으면 체크박스는 보이지만 클릭해도 아무 요청이 가지 않습니다. 인터넷이 없는 현장에서 쓸 거라면 스크립트를 스케치 문자열 안에 넣거나 SPIFFS/LittleFS에 올려 두는 쪽으로 바꿔야 합니다.

실제로 돌려 보니

보드 IP로 접속해서 Device 1 체크를 켰다 껐다 하면 LED가 바로 따라오고, 시리얼 모니터에는 LED is ON / LED is OFF 가 찍힙니다. 지금은 d_1 값만 LED에 연결했고 나머지 Device 2~5는 응답 메시지에 d_2 만 같이 찍어 보는 정도입니다. 릴레이 모듈을 GPIO 여러 개에 물리면 같은 구조로 다섯 개를 각각 제어하도록 늘릴 수 있습니다.

브라우저에서 ESP8266 페이지에 접속해 체크박스로 제어하는 화면

시리얼 모니터에 LED 상태 메시지가 출력된 화면

동작 영상: 영상 1 · 영상 2

한 가지 더, handleSubmit() 은 d_1 과 d_2 가 둘 다 있어야 처리하도록 되어 있습니다. 다른 클라이언트(예를 들어 PHP나 VB.NET의 HTTP 요청)에서 /api 를 호출할 때 d_1=on 만 보내면 "No POST data received" 가 돌아오니, 테스트할 때는 두 값을 함께 보내야 합니다.