ESP32_WEB_LED_Multiple

박찬영·2022년 7월 3일
0

ESP32_Study

목록 보기
5/5

LED 여러개를 웹으로 제어하기

앞서 올렸던 게시물은 1개의 LED를 제어했다.
이번 게시물은 3개의 LED를 제어할 것이다.

참고
https://randomnerdtutorials.com/esp32-web-server-websocket-sliders/


작동 원리

  1. 원하는 슬라이더를 움직인다.
  2. 움직인 슬라이더의 값을 WebSocket프로토콜을 통해 슬라이더 번호와 값을 서버로 보낸다.
  3. ESP(서버)에서 번호와 값을 수신한 후 PWM 듀티 사이클을 조정한다.
  4. 변경된 슬라이더 값을 다른 모든 클라이언트에게 보낸다. (다른 클라이언트가 업데이트된다)

출처: https://randomnerdtutorials.com/esp32-web-server-websocket-sliders/


코드

#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include "SPIFFS.h"
#include <Arduino_JSON.h>

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

AsyncWebServer server(80);

AsyncWebSocket ws("/ws");
const int ledPin1 = 16;
const int ledPin2 = 13;
const int ledPin3 = 12;

String message = "";
String sliderValue1 = "0";
String sliderValue2 = "0";
String sliderValue3 = "0";

int dutyCycle1;
int dutyCycle2;
int dutyCycle3;

const int freq = 5000;
const int ledChannel1 = 0;
const int ledChannel2 = 1;
const int ledChannel3 = 2;

const int resolution = 8;

JSONVar sliderValues;

String getSliderValues(){
  sliderValues["sliderValue1"] = String(sliderValue1);
  sliderValues["sliderValue2"] = String(sliderValue2);
  sliderValues["sliderValue3"] = String(sliderValue3);

  String jsonString = JSON.stringify(sliderValues);
  return jsonString;
}

void initFS() {
  if (!SPIFFS.begin()) {
    Serial.println("An error has occurred while mounting SPIFFS");
  }
  else{
   Serial.println("SPIFFS mounted successfully");
  }
}

void initWiFi() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.print("Connecting to WiFi ..");
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print('.');
    delay(1000);
  }
  Serial.println(WiFi.localIP());
}

void notifyClients(String sliderValues) {
  ws.textAll(sliderValues);
}

void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) {
  AwsFrameInfo *info = (AwsFrameInfo*)arg;
  if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
    data[len] = 0;
    message = (char*)data;
    if (message.indexOf("1s") >= 0) {
      sliderValue1 = message.substring(2);
      dutyCycle1 = map(sliderValue1.toInt(), 0, 100, 0, 255);
      Serial.println(dutyCycle1);
      Serial.print(getSliderValues());
      notifyClients(getSliderValues());
    }
    if (message.indexOf("2s") >= 0) {
      sliderValue2 = message.substring(2);
      dutyCycle2 = map(sliderValue2.toInt(), 0, 100, 0, 255);
      Serial.println(dutyCycle2);
      Serial.print(getSliderValues());
      notifyClients(getSliderValues());
    }    
    if (message.indexOf("3s") >= 0) {
      sliderValue3 = message.substring(2);
      dutyCycle3 = map(sliderValue3.toInt(), 0, 100, 0, 255);
      Serial.println(dutyCycle3);
      Serial.print(getSliderValues());
      notifyClients(getSliderValues());
    }
    if (strcmp((char*)data, "getValues") == 0) {
      notifyClients(getSliderValues());
    }
  }
}
void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) {
  switch (type) {
    case WS_EVT_CONNECT:
      Serial.printf("WebSocket client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str());
      break;
    case WS_EVT_DISCONNECT:
      Serial.printf("WebSocket client #%u disconnected\n", client->id());
      break;
    case WS_EVT_DATA:
      handleWebSocketMessage(arg, data, len);
      break;
    case WS_EVT_PONG:
    case WS_EVT_ERROR:
      break;
  }
}

void initWebSocket() {
  ws.onEvent(onEvent);
  server.addHandler(&ws);
}


void setup() {
  Serial.begin(115200);
  pinMode(ledPin1, OUTPUT);
  pinMode(ledPin2, OUTPUT);
  pinMode(ledPin3, OUTPUT);
  initFS();
  initWiFi();

  ledcSetup(ledChannel1, freq, resolution);
  ledcSetup(ledChannel2, freq, resolution);
  ledcSetup(ledChannel3, freq, resolution);

  ledcAttachPin(ledPin1, ledChannel1);
  ledcAttachPin(ledPin2, ledChannel2);
  ledcAttachPin(ledPin3, ledChannel3);


  initWebSocket();
  
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(SPIFFS, "/index.html", "text/html");
  });
  
  server.serveStatic("/", SPIFFS, "/");

  server.begin();

}

void loop() {
  ledcWrite(ledChannel1, dutyCycle1);
  ledcWrite(ledChannel2, dutyCycle2);
  ledcWrite(ledChannel3, dutyCycle3);

  ws.cleanupClients();
}

html code

보여지는 웹페이지를 만드는 역활.

<!DOCTYPE html>
<html>
<head>
    <title>ESP IOT DASHBOARD</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="icon" type="image/png" href="favicon.png">
    <link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
    <div class="topnav">
        <h1>Multiple Sliders</h1>
    </div>
    <div class="content">
        <div class="card-grid">
           <!-- 첫번째 슬라이더에 대한 내용. -->
            <div class="card">
                <p class="card-title">Fader 1</p>
                <p class="switch">
                   <!-- id = JavaScript를 사용하여 슬라이더 값을 조작할 수 있다. -->
                    <input type="range" onchange="updateSliderPWM(this)" id="slider1" min="0" max="100" step="1" value ="0" class="slider">
                </p>
               <!-- id = 현재 슬라이더 값을 삽입할 수 있다. -->
                <p class="state">Brightness: <span id="sliderValue1"></span> &percnt;</p>
            </div>
          <!-- 다른 슬라이더를 만들기 위해 복사하고, 나머지 고유한 id을 만들어준다. -->
            <div class="card">
                <p class="card-title"> Fader 2</p>
                <p class="switch">
                    <input type="range" onchange="updateSliderPWM(this)" id="slider2" min="0" max="100" step="1" value ="0" class="slider">
                </p>
                <p class="state">Brightness: <span id="sliderValue2"></span> &percnt;</p>
            </div>
            <div class="card">
                <p class="card-title"> Fader 3</p>
                <p class="switch">
                    <input type="range" onchange="updateSliderPWM(this)" id="slider3" min="0" max="100" step="1" value ="0" class="slider">
                </p>
                <p class="state">Brightness: <span id="sliderValue3"></span> &percnt;</p>
            </div>
        </div>
    </div>
    <script src="script.js"></script>
</body>
</html>

css code

보여지는 웹페이지의 스타일을 만들어주는 역활.

html {
    font-family: Arial, Helvetica, sans-serif;
    display: inline-block;
    text-align: center;
  }
  h1 {
    font-size: 1.8rem;
    color: white;
  }
  p {
    font-size: 1.4rem;
  }
  .topnav {
    overflow: hidden;
    background-color: #0A1128;
  }
  body {
    margin: 0;
  }
  .content {
    padding: 30px;
  }
  .card-grid {
    max-width: 700px;
    margin: 0 auto;
    display: grid;
    grid-gap: 2rem;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  }
  .card {
    background-color: white;
    box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5);
  }
  .card-title {
    font-size: 1.2rem;
    font-weight: bold;
    color: #034078
  }
  .state {
    font-size: 1.2rem;
    color:#1282A2;
  }
  //슬라이더 자체의 스타일 지정
  .slider {
    -webkit-appearance: none;
    margin: 0 auto;
    width: 100%;
    height: 15px;
    border-radius: 10px;
    background: #FFD65C;
    outline: none;
  }
  .slider::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 30px;
    height: 30px;
    border-radius: 50%;
    background: #034078;
    cursor: pointer;
  }
  .slider::-moz-range-thumb {
    width: 30px;
    height: 30px;
    border-radius: 50% ;
    background: #034078;
    cursor: pointer;
  }
  .switch {
    padding-left: 5%;
    padding-right: 5%;
  }

JS code

슬라이더가 움직일 때 발생하는 일을 처리하고
WebSocket프로토콜을 통해 수신된 메세지를 보내고 받고 해석하는 역활.

// gateway는 WebSocoket인터페이스의 진입점이다.
//웹서버의 ip주소를 가져온다.
var gateway = `ws://${window.location.hostname}/ws`;
//전역변수 생성
var websocket;
window.addEventListener('load', onload);

// initWebSocket()을 호출하여 서버와의 WebSocket 연결을 초기화
function onload(event) {
    initWebSocket();
}

function getValues(){
    websocket.send("getValues");
}

//initWebSocket() 함수는 앞에서 정의한 게이트웨이에서 WebSocket 연결을 초기화. 
//또한 WebSocket 연결이 열리거나 닫힐 때 또는 메시지가 수신될 때 여러 가지 콜백 기능을 할당.
function initWebSocket() {
    console.log('Trying to open a WebSocket connection…');
    websocket = new WebSocket(gateway);
    websocket.onopen = onOpen;
    websocket.onclose = onClose;
    websocket.onmessage = onMessage;
}

//웹 소켓 연결이 열리면 getValues 함수를 호출
function onOpen(event) {
    console.log('Connection opened');
  	//getValues() 함수는 메시지를 getValues 서버로 전송하여 모든 슬라이더의 현재 값을 가져온다. 
    //그런 다음 서버 측에서 해당 메시지를 수신할 때 발생하는 작업(ESP32)을 처리해야 한다ㅇ.
    getValues();
}

function onClose(event) {
    console.log('Connection closed');
    setTimeout(initWebSocket, 2000);
}

//updateSliderPWM() 기능은 슬라이더를 이동할 때 실행
function updateSliderPWM(element) {
    var sliderNumber = element.id.charAt(element.id.length-1);
    var sliderValue = document.getElementById(element.id).value;
    document.getElementById("sliderValue"+sliderNumber).innerHTML = sliderValue;
    console.log(sliderValue);
    //websocket.send()는 슬라이더에서 값을 가져오고 해당 단락을 올바른 값으로 업데이트한다.
    //이 기능은 또한 ESP32가 LED 밝기를 업데이트하도록 서버에 메시지를 보냄.
    websocket.send(sliderNumber+"s"+sliderValue.toString());
}

//onMessage() 함수의 웹 소켓 프로토콜을 통해 수신된 메시지를 처리
function onMessage(event) {
    console.log(event.data);
    var myObj = JSON.parse(event.data);
    var keys = Object.keys(myObj);

    for (var i = 0; i < keys.length; i++){
        var key = keys[i];
        document.getElementById(key).innerHTML = myObj[key];
        document.getElementById("slider"+ (i+1).toString()).value = myObj[key];
    }
}

코드 설명

#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include "SPIFFS.h"
#include <Arduino_JSON.h>

웹 서버를 구축하는 라이브러리들과 SPIFFS를 이용하기에 라이브러리 추가,
JSON를 처리하기 위해 라이브러리 추가해준다.



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

ESP가 로컬 네트워크에 연결할 수 있도록 변수에 네트워크를 입력해준다.



AsyncWebServer server(80);
AsyncWebSocket ws("/ws");

포트 80에 비동기 WebServer 개체를 생성한다.
(동기와 비동기의 차이:
https://velog.io/@slobber/%EB%8F%99%EA%B8%B0%EC%99%80-%EB%B9%84%EB%8F%99%EA%B8%B0%EC%9D%98-%EC%B0%A8%EC%9D%B4 )

WebSocket 서버를 처리할 새로운 글로벌 변수 ws를 정의하여 /ws 경로에 연결.
(ESPAsyncWebServer 라이브러리에는 다른 수신 서비스를 시작하거나 다른 포트를 사용하지 않고도 연결할 수 있는 다양한 WebSocket 위치를 정의할 수 있는 WebSocket 플러그인이 포함되어 있다.)



String sliderValue1 = "0";
String sliderValue2 = "0";
String sliderValue3 = "0";

sliderValue는 변수 슬라이더 값을 유지, 시작 시에는 0으로 설정.
LED를 3개 사용하기 때문에 3개를 만들어준다.



String getSliderValues(){
  sliderValues["sliderValue1"] = String(sliderValue1);
  sliderValues["sliderValue2"] = String(sliderValue2);
  sliderValues["sliderValue3"] = String(sliderValue3);

  String jsonString = JSON.stringify(sliderValues);
  return jsonString;
}

getSliderValues() 함수는 현재 슬라이더 값으로 JSON 문자열을 생성.
JSON.stringif() 함수는 자바스크립트의 값을 JSON 문자열로 변환해준다.
변환해준 값을 jsonString변수에 넣어준다.



  void notifyClients(String sliderValues) {
   ws.textAll(sliderValues);
 }

notifyClients() 기능은 모든 클라이언트에 현재 슬라이더 값을 알린다.
이 함수를 호출하면 슬라이더에 새 위치를 설정할 때마다 모든 클라이언트의 변경 사항을 알릴 수 있다.


void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) {
  AwsFrameInfo *info = (AwsFrameInfo*)arg;
  if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
    data[len] = 0;
    message = (char*)data;
    if (message.indexOf("1s") >= 0) {
      sliderValue1 = message.substring(2);
      dutyCycle1 = map(sliderValue1.toInt(), 0, 100, 0, 255);
      Serial.println(dutyCycle1);
      Serial.print(getSliderValues());
      notifyClients(getSliderValues());
    }
    if (message.indexOf("2s") >= 0) {
      sliderValue2 = message.substring(2);
      dutyCycle2 = map(sliderValue2.toInt(), 0, 100, 0, 255);
      Serial.println(dutyCycle2);
      Serial.print(getSliderValues());
      notifyClients(getSliderValues());
    }    
    if (message.indexOf("3s") >= 0) {
      sliderValue3 = message.substring(2);
      dutyCycle3 = map(sliderValue3.toInt(), 0, 100, 0, 255);
      Serial.println(dutyCycle3);
      Serial.print(getSliderValues());
      notifyClients(getSliderValues());
    }

handleWebSocketMessage() 함수는 WebSocket 프로토콜을 통해 클라이언트로부터 새 데이터를 수신할 때마다 실행되는 콜백 함수이다.

JavaScript 파일에서 서버가 getValues 메시지 또는 슬라이더 번호와 슬라이더 값이 포함된 메시지를 수신한다.
슬라이더 1일 경우를 해석하면
다른 메시지를 받으면 메시지에 해당하는 슬라이더를 확인하고 해당 듀티 사이클 값을 업데이트하고, 마지막으로 모든 클라이언트에게 변경 사항이 발생했음을 알린다.
나머지 함수는 슬라이더 2,3도 같은 방식이다.



if (strcmp((char*)data, "getValues") == 0) {
      notifyClients(getSliderValues());
    }
  }
}

메세지를 받을 때 getValues메세지가 표시되면 현재 슬라이더 값을 보낸다.
이렇게 하면 모든 클라이언트에 변경 사항이 통지되고 그에 따라 인터페이스가 업데이트된다.



void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) {
  switch (type) {
    case WS_EVT_CONNECT:
      Serial.printf("WebSocket client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str());
      break;
    case WS_EVT_DISCONNECT:
      Serial.printf("WebSocket client #%u disconnected\n", client->id());
      break;
    case WS_EVT_DATA:
      handleWebSocketMessage(arg, data, len);
      break;
    case WS_EVT_PONG:
    case WS_EVT_ERROR:
      break;
  }
}

WebSocket 프로토콜의 다양한 비동기 단계를 처리하도록 이벤트 수신기를 구성한 것이다.

type 인수는 발생하는 이벤트를 나타냅니다. 다음 값을 사용할 수 있습니다.


클라이언트가 로그인한 경우 WS_EVT_CONNECT입니다.
클라이언트가 로그아웃한 경우 WS_EVT_DISCONNECT입니다.
WS_EVT_DATA: 클라이언트로부터 데이터 패킷을 수신할 때 사용됩니다.
ping 요청에 대한 WS_EVT_PNG입니다.
클라이언트에서 오류를 수신한 경우 WS_EVT_ERROR입니다.



void initWebSocket() {
  ws.onEvent(onEvent);
  server.addHandler(&ws);
}

initWebSocket() 함수는 WebSocket 프로토콜을 초기화한다.

AsyncWebServer 개체의 addHandler 메서드를 호출하여 HTTP 웹 서버에 websocket 개체를 등록 한다.
입력으로 이 메서드는 AsyncWebSocket 개체의 주소를 받는다.



server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(SPIFFS, "/index.html", "text/html");
  });
  
  server.serveStatic("/", SPIFFS, "/"); //페이지를 브라우저로 보냄.

웹 소켓 끝점을 구성했으므로 이전 섹션에서 개발한 HTML/JavaScript 파일을 서비스하기 위한 경로를 추가해야 한다.
이 경로는 "/html"로 지정되며 HTTP GET 요청만 수신함.

핸들링 기능 구현에서는 단순히 파일 시스템에서 파일을 서비스할 것이다.
이를 위해, ESP32 SPIFS 파일 시스템과 상호 작용하는 데 사용되는 SPIFS 변수를 첫 번째 입력으로 전달하는 Async WebServerRequest 개체 포인터의 전송 방법을 사용한다.

두 번째 입력으로 파일의 경로를 전달하므로 비동기 웹 서버 프레임워크가 파일을 검색하여 클라이언트에 반환할 수 있습니다. 파일은 루트 디렉터리에 있으며 이름은 index.html이다.
따라서 전체 경로는 "/index.html" 임.

마무리하기 위해, 우리는 송신 메서드의 마지막 매개 변수로 "text/html"이 될 내용 유형을 가진 문자열을 전달한다.



추가자료








profile
안녕하세여

0개의 댓글