ESP32 - 웹 플로터 | ESP32 - Web Plotter

이 튜토리얼에서는 아두이노 IDE의 시리얼 플로터와 유사하게 보이는 웹 플로터를 만드는 방법을 배워보겠습니다. 이 설정을 사용하면 스마트폰이나 PC의 웹 브라우저를 통해 ESP32에서 실시간 데이터를 쉽게 확인할 수 있습니다. 아두이노 IDE의 시리얼 플로터에서 보듯이 데이터를 그래프로 표시할 것입니다.

ESP32 web plotter

준비물

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

웹 플로터 작동 방식

  • ESP32 코드는 웹 서버와 웹소켓 서버를 생성합니다.
  • 사용자가 웹 브라우저를 통해 ESP32 보드에 있는 웹페이지에 접속할 때, ESP32 보드의 웹 서버는 웹 콘텐츠(HTML, CSS, JavaScript)를 웹 브라우저로 다시 보냅니다.
  • JavaScript 코드는 시리얼 플로터와 유사하게 웹 브라우저에 그래프를 그립니다.
  • 사용자가 웹에서 연결 버튼을 클릭하면, JavaScript 코드는 ESP32 보드의 웹소켓 서버에 대한 웹소켓 연결을 생성합니다.
  • ESP32는 웹소켓 연결을 통해 시리얼 플로터와 유사한 형식(다음 부분 참조)으로 웹 브라우저로 데이터를 전송합니다.
  • 웹 브라우저의 JavaScript 코드는 데이터를 받아 그래프에 데이터를 표시합니다.

ESP32가 웹 플로터로 보내는 데이터 형식

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

자세히:

첫 번째 변수

webSocket.broadcastTXT(variable_1);

중간 변수들

webSocket.broadcastTXT(" "); // 두 값 사이에 탭 '\t' 또는 공백 ' ' 문자가 출력됩니다. webSocket.broadcastTXT(variable_2); webSocket.broadcastTXT(" "); // 두 값 사이에 탭 '\t' 또는 공백 ' ' 문자가 출력됩니다. webSocket.broadcastTXT(variable_3);

마지막 변수

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

자세한 내용은 ESP32 - Serial Plotter 튜토리얼을 참조하십시오.

ESP32 코드 - 웹 플로터

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

  • ESP32 코드인 .ino 파일로, 웹 서버와 WebSocket 서버를 생성합니다.
  • 웹페이지의 내용이 담긴 .h 파일입니다.

사용 방법

  • ESP32를 처음 사용하는 경우, Arduino IDE에서 ESP32 환경 설정 방법을 확인하세요.
  • ESP32 보드를 PC에 마이크로 USB 케이블을 통해 연결하세요.
  • PC에서 Arduino IDE를 엽니다.
  • 올바른 ESP32 보드(예: ESP32 Dev Module)와 COM 포트를 선택하세요.
  • Arduino IDE의 왼쪽 탐색 바에서 Library Manager 아이콘을 클릭하여 라이브러리 관리자를 엽니다.
  • “ESPAsyncWebServer”를 검색한 다음, lacamera가 만든 ESPAsyncWebServer를 찾습니다.
  • ESPAsyncWebServer 라이브러리를 설치하기 위해 Install 버튼을 클릭하세요.
ESP32 ESPAsyncWebServer library

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

ESP32 ESPAsyncWebServer dependencies library
  • "WebSockets"를 검색한 다음, Markus Sattler이 만든 WebSockets를 찾으세요.
  • WebSockets 라이브러리를 설치하려면 Install 버튼을 클릭하세요.
ESP32 WebSockets library
  • Arduino IDE에서 새 스케치를 만들고, 이름을 지정하세요. 예를 들어, newbiely.kr.ino
  • 아래 코드를 복사하여 Arduino IDE로 열기
/* * 이 ESP32 코드는 newbiely.kr 에서 개발되었습니다 * 이 ESP32 코드는 어떠한 제한 없이 공개 사용을 위해 제공됩니다. * 상세한 지침 및 연결도에 대해서는 다음을 방문하세요: * https://newbiely.kr/tutorials/esp32/esp32-web-plotter */ #include <WiFi.h> #include <ESPAsyncWebServer.h> #include <WebSocketsServer.h> #include "index.h" const char* ssid = "YOUR_WIFI_SSID"; // 변경하세요 const char* password = "YOUR_WIFI_PASSWORD"; // 변경하세요 AsyncWebServer server(80); WebSocketsServer webSocket = 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 서버 초기화 webSocket.begin(); //webSocket.onEvent(webSocketEvent); // WebSocket 연결을 생성하기 위한 기본 HTML 페이지와 JavaScript 제공 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("ESP32 Web Server's IP address: "); Serial.println(WiFi.localIP()); } void loop() { webSocket.loop(); if (millis() - last_update > 500) { last_update = millis(); String variable_1 = String(random(0, 100)); String variable_2 = String(random(0, 100)); String variable_3 = String(random(0, 100)); String variable_4 = String(random(0, 100)); // 시리얼 플로터에 Serial.print(variable_1); Serial.print(" "); // 두 값 사이에 탭 '\t' 또는 공백 ' ' 문자가 인쇄됩니다. Serial.print(variable_2); Serial.print(" "); // 두 값 사이에 탭 '\t' 또는 공백 ' ' 문자가 인쇄됩니다. Serial.print(variable_3); Serial.print(" "); // 두 값 사이에 탭 '\t' 또는 공백 ' ' 문자가 인쇄됩니다. Serial.println(variable_4); // 마지막 값은 캐리지 리턴과 줄바꿈 문자로 종료됩니다. // 웹 플로터에 webSocket.broadcastTXT(variable_1); webSocket.broadcastTXT(" "); // 두 값 사이에 탭 '\t' 또는 공백 ' ' 문자가 인쇄됩니다. webSocket.broadcastTXT(variable_2); webSocket.broadcastTXT(" "); // 두 값 사이에 탭 '\t' 또는 공백 ' ' 문자가 인쇄됩니다. webSocket.broadcastTXT(variable_3); webSocket.broadcastTXT(" "); // 두 값 사이에 탭 '\t' 또는 공백 ' ' 문자가 인쇄됩니다. webSocket.broadcastTXT(variable_4); webSocket.broadcastTXT("\r\n"); // 마지막 값은 캐리지 리턴과 줄바꿈 문자로 종료됩니다. } }
  • 코드에서 WiFi 정보(SSID와 비밀번호)를 자신의 네트워크 자격증명과 일치하게 수정하세요.
  • Arduino IDE에서 index.h 파일을 다음과 같이 생성하세요:
    • 직렬 모니터 아이콘 바로 아래의 버튼을 클릭하고 새 탭을 선택하거나, Ctrl+Shift+N 키를 사용하세요.
    Arduino IDE 2 adds file

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

    Arduino IDE 2 adds file index.h

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

    /* * 이 ESP32 코드는 newbiely.kr 에서 개발되었습니다 * 이 ESP32 코드는 어떠한 제한 없이 공개 사용을 위해 제공됩니다. * 상세한 지침 및 연결도에 대해서는 다음을 방문하세요: * https://newbiely.kr/tutorials/esp32/esp32-web-plotter */ const char *HTML_CONTENT = R"=====( <!DOCTYPE html> <html> <head> <title>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 = ["#33FFFF", "#FF00FF", "#FF0000", "#FF8C00", "#00FF00"]; //var COLOR_LINE = ["#0000FF", "#FF0000", "#00FF00", "#FF8C00", "#00FF00"]; //var COLOR_LINE = ["#33FFFF", "#FF0000", "#00FF00", "#FF8C00", "#00FF00"]; var COLOR_LINE = ["#0000FF", "#FF0000", "#009900", "#FF9900", "#CC00CC", "#666666", "#00CCFF", "#000000"]; var LEGEND_WIDTH = 10; var X_AXIS_TITLE_HEIGHT = 40; var Y_AXIS_TITLE_WIDTH = 40; var X_AXIS_VALUE_HEIGHT = 40; var Y_AXIS_VALUE_WIDTH = 50; var PLOT_AREA_PADDING_TOP = 30; var PLOT_AREA_PADDING_RIGHT = 30; var X_GRIDLINE_NUM = 5; var Y_GRIDLINE_NUM = 4; var WSP_SIZE_TYPE = 1; /* 0: Fixed size, 1: full screen */ var WSP_WIDTH = 400; var WSP_HEIGHT = 200; var MAX_SAMPLE = 50; // in sample var X_AXIS_MIN = 0; var X_AXIS_MAX = MAX_SAMPLE; var Y_AXIS_AUTO_SCALE = 1; /* 0: y axis fixed range, 1: y axis auto scale */ var Y_AXIS_MIN = -5; var Y_AXIS_MAX = 5; var X_AXIS_TITLE = "X"; var Y_AXIS_TITLE = "Y"; var plot_area_width; var plot_area_height; var plot_area_pivot_x; var plot_area_pivot_y; var sample_count = 0; var buffer = ""; var data = []; var ws; var canvas; var ctx; function init() { canvas = document.getElementById("graph"); canvas.style.backgroundColor = COLOR_BACKGROUND; ctx = canvas.getContext("2d"); canvas_resize(); setInterval(update_view, 1000 / 60); } function connect_onclick() { if(ws == null) { ws = new WebSocket("ws://" + window.location.host + ":81"); document.getElementById("ws_state").innerHTML = "CONNECTING"; ws.onopen = ws_onopen; ws.onclose = ws_onclose; ws.onmessage = ws_onmessage; ws.binaryType = "arraybuffer"; } else ws.close(); } function ws_onopen() { document.getElementById("ws_state").innerHTML = "<span style='color: blue'>CONNECTED</span>"; document.getElementById("bt_connect").innerHTML = "Disconnect"; } function ws_onclose() { document.getElementById("ws_state").innerHTML = "<span style='color: gray'>CLOSED</span>"; document.getElementById("bt_connect").innerHTML = "Connect"; ws.onopen = null; ws.onclose = null; ws.onmessage = null; ws = 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); } if(Y_AXIS_AUTO_SCALE) 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_view() { if(sample_count <= MAX_SAMPLE) X_AXIS_MAX = sample_count; else X_AXIS_MAX = 50; ctx.clearRect(0, 0, WSP_WIDTH, WSP_HEIGHT); ctx.save(); ctx.translate(plot_area_pivot_x, plot_area_pivot_y); ctx.font = "bold 20px Arial"; ctx.textBaseline = "middle"; ctx.textAlign = "center"; ctx.fillStyle = COLOR_TEXT; // draw X axis title if(X_AXIS_TITLE != "") { ctx.fillText(X_AXIS_TITLE, plot_area_width / 2, X_AXIS_VALUE_HEIGHT + X_AXIS_TITLE_HEIGHT / 2); } // draw Y axis title if(Y_AXIS_TITLE != "") { ctx.rotate(-90 * Math.PI / 180); ctx.fillText(Y_AXIS_TITLE, plot_area_height / 2, -Y_AXIS_VALUE_WIDTH - Y_AXIS_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, plot_area_height); y_gridline_px = Math.round(y_gridline_px) + 0.5; ctx.beginPath(); ctx.moveTo(0, y_gridline_px); ctx.lineTo(plot_area_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_AXIS_MIN, Y_AXIS_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, plot_area_width); x_gridline_px = Math.round(x_gridline_px) + 0.5; ctx.beginPath(); ctx.moveTo(x_gridline_px, 0); ctx.lineTo(x_gridline_px, -plot_area_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_AXIS_MIN, X_AXIS_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_AXIS_VALUE_HEIGHT / 2 + 5); ctx.strokeStyle = COLOR_GRIDLINE; } //ctx.lineWidth = 2; 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_AXIS_MIN, Y_AXIS_MAX, 0, plot_area_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_AXIS_MIN, X_AXIS_MAX -1, 0, plot_area_width); y_px = -map(y_value, Y_AXIS_MIN, Y_AXIS_MAX, 0, plot_area_height); ctx.lineTo(x_px, y_px); } ctx.stroke(); } // draw legend var x = plot_area_width - (line_num - line) * LEGEND_WIDTH - (line_num - line - 1) * LEGEND_WIDTH / 2; var y = -plot_area_height - PLOT_AREA_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; if(WSP_SIZE_TYPE) { // full screen 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_AXIS_MIN.toFixed(1) + "").width; var y_max_text_size = ctx.measureText(Y_AXIS_MAX.toFixed(1) + "").width; Y_AXIS_VALUE_WIDTH = Math.round(Math.max(y_min_text_size, y_max_text_size)) + 15; plot_area_width = WSP_WIDTH - Y_AXIS_VALUE_WIDTH - PLOT_AREA_PADDING_RIGHT; plot_area_height = WSP_HEIGHT - X_AXIS_VALUE_HEIGHT - PLOT_AREA_PADDING_TOP; plot_area_pivot_x = Y_AXIS_VALUE_WIDTH; plot_area_pivot_y = WSP_HEIGHT - X_AXIS_VALUE_HEIGHT; if(X_AXIS_TITLE != "") { plot_area_height -= X_AXIS_TITLE_HEIGHT; plot_area_pivot_y -= X_AXIS_TITLE_HEIGHT; } if(Y_AXIS_TITLE != "") { plot_area_width -= Y_AXIS_TITLE_WIDTH; plot_area_pivot_x += Y_AXIS_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_AXIS_MIN = step * Math.floor(min / step); Y_AXIS_MAX = Y_AXIS_MIN + step * (4); if (Y_AXIS_MAX >= max) break; } var count = 5 - Math.floor((Y_AXIS_MAX - max) / step); Y_AXIS_MAX = Y_AXIS_MIN + step * (count - 1); ctx.font = "16px Arial"; var y_min_text_size = ctx.measureText(Y_AXIS_MIN.toFixed(1) + "").width; var y_max_text_size = ctx.measureText(Y_AXIS_MAX.toFixed(1) + "").width; Y_AXIS_VALUE_WIDTH = Math.round(Math.max(y_min_text_size, y_max_text_size)) + 15; plot_area_width = WSP_WIDTH - Y_AXIS_VALUE_WIDTH - PLOT_AREA_PADDING_RIGHT; plot_area_pivot_x = Y_AXIS_VALUE_WIDTH; } } window.onload = init; </script> </head> <body onresize="canvas_resize()"> <h1 id="header">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="bt_connect" type="button" onclick="connect_onclick();">Connect</button> </div> </body> </html> )=====";
    • 이제 코드가 두 파일에 있습니다: newbiely.kr.inoindex.h
    • Arduino IDE에서 Upload 버튼을 클릭하여 ESP32에 코드를 업로드하세요.
    • 시리얼 모니터를 엽니다
    • 시리얼 모니터에서 결과를 확인하세요.
    COM6
    Send
    Connecting to WiFi... Connected to WiFi ESP32 Web Server's IP address IP address: 192.168.0.2
    Autoscroll Show timestamp
    Clear output
    9600 baud  
    Newline  

    화면에 표시된 IP 주소를 확인하고, 이 주소를 스마트폰이나 PC의 웹 브라우저 주소창에 입력하세요.

    아래와 같은 웹페이지가 표시될 것입니다:

    ESP32 plotter web browser
    • 웹 페이지를 ESP32에 WebSocket을 통해 연결하려면 CONNECT 버튼을 클릭하십시오.
    • 아래 이미지처럼 플로터가 데이터를 플로팅하는 것을 볼 수 있습니다.
    ESP32 web graph

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

    ※ NOTE THAT:

    • index.h에서 HTML 내용을 수정하고 newbiely.kr.ino 파일은 건드리지 않으면, 코드를 ESP32에 컴파일하고 업로드할 때 Arduino IDE가 HTML 내용을 업데이트하지 않습니다.
    • 이 경우 Arduino IDE가 HTML 내용을 업데이트하게 하려면, newbiely.kr.ino 파일을 변경하세요 (예: 빈 줄 추가, 주석 추가 등).

    코드 줄별 설명

    위의 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!