아두이노 나노 ESP32 - 웹 플로터 | Arduino Nano ESP32 - Web Plotter

이 튜토리얼은 Arduino IDE에서 찾을 수 있는 시리얼 플로터와 유사한 웹 기반 플로터를 구축하는 방법을 안내합니다. 이 웹 기반 플로터를 사용하면 스마트폰이나 PC의 웹 브라우저를 통해 Arduino Nano ESP32에서 실시간 데이터를 모니터링할 수 있습니다. 플로팅된 데이터는 그래프 형식으로 제시되며, 일반적으로 Arduino IDE의 시리얼 플로터에서 관찰하는 것과 같은 모습을 보여줍니다.

Arduino Nano ESP32 web plotter

준비물

1×Arduino Nano ESP32 Amazon
1×USB Cable Type-C 쿠팡 | Amazon
1×(Recommended) Screw Terminal Expansion Board for Arduino Nano 쿠팡 | Amazon
공개: 이 섹션에서 제공된 링크 중 일부는 제휴 링크입니다. 이 링크를 통해 구매한 경우 추가 비용없이 수수료를 받을 수 있습니다. 지원해 주셔서 감사합니다.

웹 플로터 작동 방식

아두이노 나노 ESP32 코드는 웹 서버와 웹소켓 서버를 모두 생성합니다.

웹 브라우저를 통해 아두이노 나노 ESP32 보드에서 호스팅하는 웹페이지에 사용자가 접속하면, 아두이노 나노 ESP32의 웹 서버는 웹 콘텐츠(HTML, CSS, JavaScript)를 브라우저로 보냅니다.

웹 브라우저에서 실행되는 자바스크립트 코드는 시리얼 플로터와 유사한 그래프를 생성합니다.

웹페이지에서 연결 버튼을 클릭하면, 자바스크립트 코드는 아두이노 나노 ESP32 보드에서 실행되고 있는 웹소켓 서버에 웹소켓 연결을 시작합니다.

아두이노 나노 ESP32는 시리얼 플로터에서 사용하는 것과 유사한 형식으로 웹 브라우저에 데이터를 웹소켓 연결을 통해 전송합니다(자세한 내용은 다음 부분에서 제공됩니다).

웹 브라우저의 자바스크립트 코드는 데이터를 받아 그래프에 표시합니다.

아두이노 나노 ESP32가 웹 플롯터로 전송하는 데이터 포맷

여러 변수를 플롯하려면 변수들을 “\t” 또는 " " 문자로 서로 분리해야 합니다. 마지막 값은 “\r\n” 문자로 종결되어야 합니다.

자세히:

  • 첫 번째 변수
plotter.broadcastTXT(data_1);

중간 변수들

plotter.broadcastTXT("\t"); // 두 값 사이에 탭 문자('\t') 또는 공백(' ')이 출력됩니다. plotter.broadcastTXT(data_2); plotter.broadcastTXT("\t"); // 두 값 사이에 탭 문자('\t') 또는 공백(' ')이 출력됩니다. plotter.broadcastTXT(data_3);

마지막 변수

plotter.broadcastTXT("\t"); // 두 값 사이에 탭 문자('\t') 또는 공백(' ')이 인쇄됩니다. plotter.broadcastTXT(data_4); plotter.broadcastTXT("\r\n"); // 마지막 값은 캐리지 리턴('\r')과 개행('\n') 문자로 종료됩니다.

자세한 내용은 Arduino Nano ESP32 - Serial Plotter 튜토리얼을 참조해 주세요.

아두이노 나노 ESP32 코드 - 웹 플로터

웹페이지의 내용(HTML, CSS, JavaScript)은 index.h 파일에 별도로 저장됩니다. 따라서, 아두이노 IDE에서는 두 개의 코드 파일을 가지게 됩니다:

  • 아두이노 나노 ESP32 코드가 들어 있는 .ino 파일로, 웹 서버와 웹소켓 서버를 모두 생성합니다.
  • 웹페이지의 내용을 담고 있는 .h 파일입니다.

사용 방법

  • 아두이노 나노 ESP32에 처음이라면, 아두이노 IDE에서 아두이노 나노 ESP32 환경 설정 방법에 관한 튜토리얼을 참조하세요. BASE_URL/tutorials/arduino-nano-esp32/arduino-nano-esp32-software-installation.
  • 아두이노 나노 ESP32 보드를 PC에 USB 케이블을 이용하여 연결하세요.
  • PC에서 아두이노 IDE를 엽니다.
  • 올바른 아두이노 나노 ESP32 보드(예: Arduino Nano ESP32와 COM 포트를 선택하세요.
  • 아두이노 IDE의 왼쪽 내비게이션 바에 있는 Library Manager 아이콘을 클릭하여 라이브러리 관리자를 엽니다.
  • "ESPAsyncWebServer"를 검색한 후, lacamera가 만든 ESPAsyncWebServer를 찾으세요.
  • Install 버튼을 클릭하여 ESPAsyncWebServer 라이브러리를 설치하세요.
Arduino Nano ESP32 ESPAsyncWebServer library

의존성을 설치하라는 요청을 받게 됩니다. Install All 버튼을 클릭하세요.

Arduino Nano ESP32 ESPAsyncWebServer dependencies library
  • “WebSockets”를 검색한 다음, Markus Sattler이 만든 WebSockets를 찾으세요.
  • WebSockets 라이브러리를 설치하려면 Install 버튼을 클릭하세요.
Arduino Nano ESP32 WebSockets library
  • 아두이노 IDE에서 새 스케치를 생성하고, 이름을 지정하세요. 예를 들어, newbiely.kr.ino
  • 아래의 코드를 복사하고 아두이노 IDE로 열어주세요.
/* * 이 Arduino Nano ESP32 코드는 newbiely.kr 에서 개발되었습니다 * 이 Arduino Nano ESP32 코드는 어떠한 제한 없이 공개 사용을 위해 제공됩니다. * 상세한 지침 및 연결도에 대해서는 다음을 방문하세요: * https://newbiely.kr/tutorials/arduino-nano-esp32/arduino-nano-esp32-web-plotter */ #include <WiFi.h> #include <ESPAsyncWebServer.h> #include <WebSocketsServer.h> #include "index.h" // HTML, JavaScript 및 CSS가 포함되어 있습니다. const char* ssid = "YOUR_WIFI_SSID"; // 자신의 네트워크 자격 증명으로 교체하십시오. const char* password = "YOUR_WIFI_PASSWORD"; // 자신의 네트워크 자격 증명으로 교체하십시오. AsyncWebServer server(80); WebSocketsServer plotter = WebSocketsServer(81); // 81번 포트에서 WebSocket 서버 int last_update = 0; void setup() { Serial.begin(9600); delay(1000); // Wi-Fi에 연결 WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(1000); Serial.println("Connecting to WiFi..."); } Serial.println("Connected to WiFi"); // WebSocket 서버 초기화 plotter.begin(); // WebSocket 연결을 생성하는 자바스크립트가 포함된 기본 HTML 페이지 제공 server.on("/", HTTP_GET, [](AsyncWebServerRequest* request) { Serial.println("Web Server: received a web page request"); String html = HTML_CONTENT; // index.h 파일에서 HTML 내용을 사용합니다. request->send(200, "text/html", html); }); server.begin(); Serial.print("Arduino Nano ESP32 Web Server's IP address: "); Serial.println(WiFi.localIP()); } void loop() { plotter.loop(); if (millis() - last_update > 500) { last_update = millis(); String data_1 = String(random(0, 100)); String data_2 = String(random(0, 100)); String data_3 = String(random(0, 100)); String data_4 = String(random(0, 100)); // 시리얼 플로터로 Serial.print(data_1); Serial.print("\t"); // 두 값 사이에 탭 문자('\t') 또는 공백(' ')이 인쇄됩니다. Serial.print(data_2); Serial.print("\t"); // 두 값 사이에 탭 문자('\t') 또는 공백(' ')이 인쇄됩니다. Serial.print(data_3); Serial.print("\t"); // 두 값 사이에 탭 문자('\t') 또는 공백(' ')이 인쇄됩니다. Serial.println(data_4); // 마지막 값은 캐리지 리턴('\r')과 줄바꿈('\n') 문자로 종결됩니다. // 웹 플로터로 plotter.broadcastTXT(data_1); plotter.broadcastTXT("\t"); // 두 값 사이에 탭 문자('\t') 또는 공백(' ')이 인쇄됩니다. plotter.broadcastTXT(data_2); plotter.broadcastTXT("\t"); // 두 값 사이에 탭 문자('\t') 또는 공백(' ')이 인쇄됩니다. plotter.broadcastTXT(data_3); plotter.broadcastTXT("\t"); // 두 값 사이에 탭 문자('\t') 또는 공백(' ')이 인쇄됩니다. plotter.broadcastTXT(data_4); plotter.broadcastTXT("\r\n"); // 마지막 값은 캐리지 리턴('\r')과 줄바꿈('\n') 문자로 종결됩니다. } }
  • 코드에서 WiFi 정보(SSID 및 비밀번호)를 자신의 네트워크 자격증명과 일치하도록 수정하세요.
  • 아두이노 IDE에서 index.h 파일을 생성하려면:
    • 직렬 모니터 아이콘 바로 아래의 버튼을 클릭하고 새 탭을 선택하거나, Ctrl+Shift+N 키를 사용하세요.
    Arduino IDE 2 adds file

    파일 이름을 index.h로 지정하고 OK 버튼을 클릭하세요.

    Arduino IDE 2 adds file index.h

    아래 코드를 복사하여 index.h에 붙여넣으세요.

    /* * 이 Arduino Nano ESP32 코드는 newbiely.kr 에서 개발되었습니다 * 이 Arduino Nano ESP32 코드는 어떠한 제한 없이 공개 사용을 위해 제공됩니다. * 상세한 지침 및 연결도에 대해서는 다음을 방문하세요: * https://newbiely.kr/tutorials/arduino-nano-esp32/arduino-nano-esp32-web-plotter */ const char *HTML_CONTENT = R"=====( <!DOCTYPE html> <html> <head> <title>Arduino Nano ESP32 - Web Plotter</title> <meta name="viewport" content="width=device-width, initial-scale=0.7"> <style> body {text-align: center; height: 750px; } h1 {font-weight: bold; font-size: 20pt; padding-bottom: 5px; color: navy; } h2 {font-weight: bold; font-size: 15pt; padding-bottom: 5px; } button {font-weight: bold; font-size: 15pt; } #footer {width: 100%; margin: 0px; padding: 0px 0px 10px 0px; bottom: 0px; } .sub-footer {margin: 0 auto; position: relative; width:400px; } .sub-footer a {position: absolute; font-size: 10pt; top: 3px; } </style> <script> var COLOR_BACKGROUND = "#FFFFFF"; var COLOR_TEXT = "#000000"; var COLOR_BOUND = "#000000"; var COLOR_GRIDLINE = "#F0F0F0"; var COLOR_LINE = ["#0000FF", "#FF0000", "#009900", "#FF9900", "#CC00CC", "#666666", "#00CCFF", "#000000"]; var LEGEND_WIDTH = 10; var X_TITLE_HEIGHT = 40; var Y_TITLE_WIDTH = 40; var X_VALUE_HEIGHT = 40; var Y_VALUE_WIDTH = 50; var PLOTTER_PADDING_TOP = 30; var PLOTTER_PADDING_RIGHT = 30; var X_GRIDLINE_NUM = 5; var Y_GRIDLINE_NUM = 4; var WSP_WIDTH = 400; var WSP_HEIGHT = 200; var MAX_SAMPLE = 50; // in sample var X_MIN = 0; var X_MAX = MAX_SAMPLE; var Y_MIN = -5; var Y_MAX = 5; var X_TITLE = "X"; var Y_TITLE = "Y"; var plotter_width; var plotter_height; var plotter_pivot_x; var plotter_pivot_y; var sample_count = 0; var buffer = ""; var data = []; var webSocket; var canvas; var ctx; function init(){ canvas = document.getElementById("graph"); canvas.style.backgroundColor = COLOR_BACKGROUND; ctx = canvas.getContext("2d"); canvas_resize(); setInterval(update_plotter, 1000 / 60); } function connect_to_esp32(){ if(webSocket == null){ webSocket = new WebSocket("ws://" + window.location.host + ":81"); document.getElementById("ws_state").innerHTML = "CONNECTING"; webSocket.onopen = ws_onopen; webSocket.onclose = ws_onclose; webSocket.onmessage = ws_onmessage; webSocket.binaryType = "arraybuffer"; } else webSocket.close(); } function ws_onopen(){ document.getElementById("ws_state").innerHTML = "<span style='color: blue'>CONNECTED</span>"; document.getElementById("btn_connect").innerHTML = "Disconnect"; } function ws_onclose(){ document.getElementById("ws_state").innerHTML = "<span style='color: gray'>CLOSED</span>"; document.getElementById("btn_connect").innerHTML = "Connect"; webSocket.onopen = null; webSocket.onclose = null; webSocket.onmessage = null; webSocket = null; } function ws_onmessage(e_msg){ e_msg = e_msg || window.event; // MessageEvent console.log(e_msg.data); buffer += e_msg.data; buffer = buffer.replace(/\r\n/g, "\n"); buffer = buffer.replace(/\r/g, "\n"); while(buffer.indexOf("\n") == 0) buffer = buffer.substr(1); if(buffer.indexOf("\n") <= 0) return; var pos = buffer.lastIndexOf("\n"); var str = buffer.substr(0, pos); var new_sample_arr = str.split("\n"); buffer = buffer.substr(pos + 1); for(var si = 0; si < new_sample_arr.length; si++) { var str = new_sample_arr[si]; var arr = []; if(str.indexOf("\t") > 0) arr = str.split("\t"); else arr = str.split(" "); for(var i = 0; i < arr.length; i++){ var value = parseFloat(arr[i]); if(isNaN(value)) continue; if(i >= data.length) { var new_line = [value]; data.push(new_line); // new line } else data[i].push(value); } sample_count++; } for(var line = 0; line < data.length; line++){ while(data[line].length > MAX_SAMPLE) data[line].splice(0, 1); } auto_scale(); } function map(x, in_min, in_max, out_min, out_max){ return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; } function get_random_color(){ var letters = '0123456789ABCDEF'; var _color = '#'; for (var i = 0; i < 6; i++) _color += letters[Math.floor(Math.random() * 16)]; return _color; } function update_plotter(){ if(sample_count <= MAX_SAMPLE) X_MAX = sample_count; else X_MAX = 50; ctx.clearRect(0, 0, WSP_WIDTH, WSP_HEIGHT); ctx.save(); ctx.translate(plotter_pivot_x, plotter_pivot_y); ctx.font = "bold 20px Arial"; ctx.textBaseline = "middle"; ctx.textAlign = "center"; ctx.fillStyle = COLOR_TEXT; // draw X axis title if(X_TITLE != "") ctx.fillText(X_TITLE, plotter_width / 2, X_VALUE_HEIGHT + X_TITLE_HEIGHT / 2); // draw Y axis title if(Y_TITLE != ""){ ctx.rotate(-90 * Math.PI / 180); ctx.fillText(Y_TITLE, plotter_height / 2, -Y_VALUE_WIDTH - Y_TITLE_WIDTH / 2); ctx.rotate(90 * Math.PI / 180); } ctx.font = "16px Arial"; ctx.textAlign = "right"; ctx.strokeStyle = COLOR_BOUND; for(var i = 0; i <= Y_GRIDLINE_NUM; i++){ var y_gridline_px = -map(i, 0, Y_GRIDLINE_NUM, 0, plotter_height); y_gridline_px = Math.round(y_gridline_px) + 0.5; ctx.beginPath(); ctx.moveTo(0, y_gridline_px); ctx.lineTo(plotter_width, y_gridline_px); ctx.stroke(); ctx.strokeStyle = COLOR_BOUND; ctx.beginPath(); ctx.moveTo(-7 , y_gridline_px); ctx.lineTo(4, y_gridline_px); ctx.stroke(); var y_gridline_value = map(i, 0, Y_GRIDLINE_NUM, Y_MIN, Y_MAX); y_gridline_value = y_gridline_value.toFixed(1); ctx.fillText(y_gridline_value + "", -15, y_gridline_px); ctx.strokeStyle = COLOR_GRIDLINE; } ctx.strokeStyle = COLOR_BOUND; ctx.textAlign = "center"; ctx.beginPath(); ctx.moveTo(0.5, y_gridline_px - 7); ctx.lineTo(0.5, y_gridline_px + 4); ctx.stroke(); for(var i = 0; i <= X_GRIDLINE_NUM; i++){ var x_gridline_px = map(i, 0, X_GRIDLINE_NUM, 0, plotter_width); x_gridline_px = Math.round(x_gridline_px) + 0.5; ctx.beginPath(); ctx.moveTo(x_gridline_px, 0); ctx.lineTo(x_gridline_px, -plotter_height); ctx.stroke(); ctx.strokeStyle = COLOR_BOUND; ctx.beginPath(); ctx.moveTo(x_gridline_px, 7); ctx.lineTo(x_gridline_px, -4); ctx.stroke(); var x_gridline_value; if(sample_count <= MAX_SAMPLE) x_gridline_value = map(i, 0, X_GRIDLINE_NUM, X_MIN, X_MAX); else x_gridline_value = map(i, 0, X_GRIDLINE_NUM, sample_count - MAX_SAMPLE, sample_count);; ctx.fillText(x_gridline_value.toString(), x_gridline_px, X_VALUE_HEIGHT / 2 + 5); ctx.strokeStyle = COLOR_GRIDLINE; } var line_num = data.length; for(var line = 0; line < line_num; line++){ // draw graph var sample_num = data[line].length; if(sample_num >= 2){ var y_value = data[line][0]; var x_px = 0; var y_px = -map(y_value, Y_MIN, Y_MAX, 0, plotter_height); if(line == COLOR_LINE.length) COLOR_LINE.push(get_random_color()); ctx.strokeStyle = COLOR_LINE[line]; ctx.beginPath(); ctx.moveTo(x_px, y_px); for(var i = 0; i < sample_num; i++){ y_value = data[line][i]; x_px = map(i, X_MIN, X_MAX -1, 0, plotter_width); y_px = -map(y_value, Y_MIN, Y_MAX, 0, plotter_height); ctx.lineTo(x_px, y_px); } ctx.stroke(); } // draw legend var x = plotter_width - (line_num - line) * LEGEND_WIDTH - (line_num - line - 1) * LEGEND_WIDTH / 2; var y = -plotter_height - PLOTTER_PADDING_TOP / 2 - LEGEND_WIDTH / 2; ctx.fillStyle = COLOR_LINE[line]; ctx.beginPath(); ctx.rect(x, y, LEGEND_WIDTH, LEGEND_WIDTH); ctx.fill(); } ctx.restore(); } function canvas_resize(){ canvas.width = 0; // to avoid wrong screen size canvas.height = 0; document.getElementById('footer').style.position = "fixed"; var width = window.innerWidth - 20; var height = window.innerHeight - 20; WSP_WIDTH = width; WSP_HEIGHT = height - document.getElementById('header').offsetHeight - document.getElementById('footer').offsetHeight; canvas.width = WSP_WIDTH; canvas.height = WSP_HEIGHT; ctx.font = "16px Arial"; var y_min_text_size = ctx.measureText(Y_MIN.toFixed(1) + "").width; var y_max_text_size = ctx.measureText(Y_MAX.toFixed(1) + "").width; Y_VALUE_WIDTH = Math.round(Math.max(y_min_text_size, y_max_text_size)) + 15; plotter_width = WSP_WIDTH - Y_VALUE_WIDTH - PLOTTER_PADDING_RIGHT; plotter_height = WSP_HEIGHT - X_VALUE_HEIGHT - PLOTTER_PADDING_TOP; plotter_pivot_x = Y_VALUE_WIDTH; plotter_pivot_y = WSP_HEIGHT - X_VALUE_HEIGHT; if(X_TITLE != "") { plotter_height -= X_TITLE_HEIGHT; plotter_pivot_y -= X_TITLE_HEIGHT; } if(Y_TITLE != "") { plotter_width -= Y_TITLE_WIDTH; plotter_pivot_x += Y_TITLE_WIDTH; } ctx.lineWidth = 1; } function auto_scale(){ if(data.length >= 1){ var max_arr = []; var min_arr = []; for(var i = 0; i < data.length; i++){ if(data[i].length >= 1){ var max = Math.max.apply(null, data[i]); var min = Math.min.apply(null, data[i]); max_arr.push(max); min_arr.push(min); } } var max = Math.max.apply(null, max_arr); var min = Math.min.apply(null, min_arr); var MIN_DELTA = 10.0; if((max - min) < MIN_DELTA){ var mid = (max + min) / 2; max = mid + MIN_DELTA / 2; min = mid - MIN_DELTA / 2; } var range = max - min; var exp; if (range == 0.0) exp = 0; else exp = Math.floor(Math.log10(range / 4)); var scale = Math.pow(10, exp); var raw_step = (range / 4) / scale; var step; potential_steps =[1.0, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0, 6.0, 8.0, 10.0]; for (var i = 0; i < potential_steps.length; i++) { if (potential_steps[i] < raw_step) continue; step = potential_steps[i] * scale; Y_MIN = step * Math.floor(min / step); Y_MAX = Y_MIN + step * (4); if (Y_MAX >= max) break; } var count = 5 - Math.floor((Y_MAX - max) / step); Y_MAX = Y_MIN + step * (count - 1); ctx.font = "16px Arial"; var y_min_text_size = ctx.measureText(Y_MIN.toFixed(1) + "").width; var y_max_text_size = ctx.measureText(Y_MAX.toFixed(1) + "").width; Y_VALUE_WIDTH = Math.round(Math.max(y_min_text_size, y_max_text_size)) + 15; plotter_width = WSP_WIDTH - Y_VALUE_WIDTH - PLOTTER_PADDING_RIGHT; plotter_pivot_x = Y_VALUE_WIDTH; } } window.onload = init; </script> </head> <body onresize="canvas_resize()"> <h1 id="header">Arduino Nano ESP32 - Web Plotter</h1> <canvas id="graph"></canvas> <br> <div id="footer"> <div class="sub-footer"> <h2>WebSocket <span id="ws_state"><span style="color: gray">CLOSED</span></span></h2> </div> <button id="btn_connect" type="button" onclick="connect_to_esp32();">Connect</button> </div> </body> </html> )=====";
    • 이제 두 개 파일에 코드가 있습니다: newbiely.kr.inoindex.h
    • Arduino Nano ESP32에 코드를 업로드하기 위해 Arduino IDE에서 Upload 버튼을 클릭하세요.
    • 시리얼 모니터를 열어주세요
    • 시리얼 모니터에서 결과를 확인하세요.
    COM6
    Send
    Connecting to WiFi... Connected to WiFi Arduino Nano ESP32 Web Server's IP address IP address: 192.168.0.6
    Autoscroll Show timestamp
    Clear output
    9600 baud  
    Newline  
    • 표시된 IP 주소를 기록하고, 스마트폰이나 PC의 웹 브라우저 주소창에 이 주소를 입력하세요.
    • 다음과 같은 웹페이지가 표시됩니다:
    Arduino Nano ESP32 plotter web browser
    • WebSocket을 통해 웹페이지를 Arduino Nano ESP32에 연결하려면 연결(CONNECT) 버튼을 클릭하세요.
    • 아래 이미지와 같이 플로터가 데이터를 플롯하는 것을 볼 수 있습니다.
    Arduino Nano ESP32 web graph

    아두이노 IDE에서 시리얼 플로터를 열어 웹 브라우저의 웹 플로터와 비교할 수 있습니다.

    ※ NOTE THAT:

    • index.h 파일의 HTML 내용을 수정하고 newbiely.kr.ino 파일에는 어떠한 변경도 하지 않으면, Arduino IDE를 사용하여 코드를 Arduino Nano ESP32에 컴파일하고 업로드할 때, IDE는 HTML 내용을 업데이트하지 않을 것입니다.
    • 이 상황에서 Arduino IDE가 HTML 내용을 업데이트하도록 강제하려면, newbiely.kr.ino 파일에서 변경을 해야 합니다. 예를 들어, 빈 줄을 추가하거나 주석을 포함할 수 있습니다.

    코드 줄별 설명

    위의 Arduino Nano ESP32 코드에는 줄마다 설명이 포함되어 있습니다. 코드의 주석을 읽어주세요!

※ OUR MESSAGES

  • Please feel free to share the link of this tutorial. However, Please do not use our content on any other websites. We invested a lot of effort and time to create the content, please respect our work!