본문 바로가기

아두이노우노 R4

Arduino를 사용하여 ESP32 보드에 FreeRTOS 구현하기 4

반응형

 

튜토리얼 3과를 통해 ESP 32 기반 브레드보드 회로를 구축하고 FreeRTOS와 Arduino IDE를 사용하여 기능을 성공적으로 프로그래밍했습니다. 작업, 우선순위, 인터럽트, 뮤텍스, 큐, 타이머를 사용하여 실행 흐름을 제어하고 시스템이 특정 시점에 가장 중요한 작업을 처리하도록 하는 실시간 운영 체제 프로그래밍의 기본 사항을 배웠습니다. 최종적으로 6개의 작업을 동시에 실행하고 콘솔 키보드와 간단한 푸시 버튼 입력에 응답하는 시스템을 만들었습니다. 4과에서는 구성에 웹 서버를 추가하고 웹소켓 연결을 사용하여 시스템과의 양방향 통신 링크를 제공합니다.

 

 

ESP 32 보드에는 Wi-Fi 트랜시버가 포함되어 있으며, Arduino IDE에는 웹 서버 및 웹소켓 연결을 생성하고 관리하는 데 사용할 수 있는 라이브러리가 포함되어 있습니다. 이 튜토리얼에서는 이러한 기능을 FreeRTOS와 함께 사용하여 기존 시스템을 확장하는 방법을 보여드리겠습니다. 이 튜토리얼을 웹 서버와 웹소켓에 대한 일반적인 내용으로 만들지는 않겠습니다. 너무 많은 페이지가 필요할 것입니다. 대신 여기에서 찾을 수 있는 "ESP32 웹소켓 서버: 제어 출력(Arduino IDE)" 문서를 참조하십시오 . 이 문서에서는 여기에서 사용된 방법론의 많은 부분을 훨씬 더 자세히 다루고 있습니다. 실제로 첨부된 문서의 코드는 수정 없이 ESP 32 브레드보드에서 실행할 수 있습니다. 저는 FreeRTOS 기반 시스템의 고유한 특성에 중점을 둘 것입니다.

 

웹 서버 및 웹 소켓 개요

 

대부분의 최신 웹 브라우저에서 실행되는 시스템의 대화형 프런트엔드를 구축하기 위해 표준 HTML 코드를 처리하는 웹 서버와 표준 HTML, CSS, Javascript 서비스를 사용하는 클라이언트 인터페이스를 사용할 것입니다. 상위 수준에서, 클라이언트가 URL을 입력하여 웹 서버에 연결할 때:

 

1. 웹서버(즉, ESP 32 장치)는 웹 페이지 정의 정보를 클라이언트로 다시 전송합니다.

2. 클라이언트는 로컬에서 웹페이지를 로드한 후 시작 스크립트를 실행합니다. 시작 스크립트는 버튼을 누르거나 서버에서 새 메시지를 수신할 때 발생하는 동작을 정의합니다. 이 경우, 시작 스크립트는 웹 소켓 연결을 열기 위한 요청을 서버로 다시 전송합니다.

3. 서버는 웹 소켓 연결을 열고 LED 표시등 상태와 관련된 주기적인 업데이트 정보를 클라이언트에 전송합니다. 또한 클라이언트의 요청(빠르거나 느림)을 수신하여 응답합니다.

4. 이 양방향 통신은 연결이 유지되는 한 유지됩니다. 연결이 끊어지면 웹 소켓이 닫힙니다.

 

ESP 32의 웹 서버는 여러 연결을 동시에 유지하고, 연결된 모든 클라이언트에 업데이트를 전송하고, 모든 클라이언트의 요청에 응답할 수 있습니다. 3장에서 구축한 명령 큐를 사용하여 여러 클라이언트의 요청을 관리하겠습니다.

 

수업 4 목표

 

4과에서는 지금까지 개발한 시스템을 확장하여 두 가지 FreeRTOS 작업을 추가합니다. 하나는 웹 클라이언트의 요청과 연결을 관리하는 WebServer 작업이고, 다른 하나는 연결된 모든 클라이언트에 LED 표시등 상태를 주기적으로(0.25초마다) 전송하는 WebUpdate 작업입니다. 코드로 들어가기 전에 몇 가지 더 다루고 싶은 주제가 있습니다.

 

웹사이트 정의 파일

 

이전에 웹사이트를 개발한 적이 있다면, 사이트의 모양과 느낌을 정의할 때 html, css, js 파일을 사용했을 가능성이 높습니다.

 

  • HTML(Hypertext Markup Language)은 표준 브라우저와 서버 간의 인터페이스를 제어하는 데 사용됩니다. 인터넷의 제어 언어로 볼 수 있습니다.
  • CSS(Cascading Style Sheets)는 사이트의 모양과 느낌을 정의하는 데 사용되며, 색상, 글꼴, 크기, 이벤트(툴팁, 마우스 오버 등)에 대한 응답 등을 정의합니다. 모양과 느낌을 실제 인터페이스 정의와 분리하면 개발자가 여러 웹 페이지 간의 일관성을 유지하고 CSS 정의를 변경하여 여러 페이지의 모양과 느낌을 동시에 변경할 수 있습니다.
  • JS(JavaScript) 파일은 계산 및 조건 논리를 수행하고, 페이지 요소의 값이나 특성을 업데이트하거나 웹 소켓 연결을 통해 웹 서버와 상호 작용하는 데 사용할 수 있는 Java 스크립트를 포함하는 데 사용됩니다.

 

일반적으로 이 세 가지 유형의 정보는 Project.html, Project.css, Project.js 등 세 개 이상의 파일로 나뉩니다. 하지만 간단한 인터페이스 프로젝트에서는 세 가지 방식으로 다른 접근 방식을 취하겠습니다.

 

1. 모든 것을 하나의 "파일"에 포함시키겠습니다.마커를 사용하여 CSS 명령을 캡슐화하고

 

마커를 사용하여 Java 스크립트 명령을 캡슐화하여 CSS 및 JS 정보를 HTML 파일에 포함할 수 있습니다.

 

2. ESP 장치에는 어떤 유형의 디스크도 없으므로, 정보를 문자 배열에 저장하고 이 배열에서 클라이언트에 제공합니다. 이는 텍스트 정보를 한 줄씩 문자 배열에 로드하는 Arduino/C++ rawliteral 명령을 사용하여 구현됩니다. ESP 32 장치에 SD 카드 리더가 있는 경우, 파일 데이터를 로드한 후 SPIFFS라는 특수 드라이버를 설치하여 데이터에 액세스할 수 있습니다. 자세한 내용은 여기를 참조하세요 . 많은 공간을 차지하는 복잡한 웹 인터페이스가 있는 경우 이 방법이 더 나은 해결책입니다.

 

3. html/css/js 콘텐츠가 많은 코드 줄(정확히 149줄)을 차지하기 때문에, 시스템이 코드를 컴파일할 때 로드되는 헤더 파일로 옮기기로 했습니다. 이렇게 하면 시스템을 업데이트할 때 매번 스크롤할 필요가 없습니다.

 

힘들게 얻은 교훈이 하나 있습니다. rawliteral 스트림에 % 기호가 포함되어 있으면 제대로 기록되지 않습니다. 제어 문자로 간주되기 때문입니다. %% 기호를 두 개 삽입해야 문자 스트림에 하나가 포함됩니다. 이 사실을 알아내는 데 오랜 시간이 걸렸습니다.

 

소수를 활용한 재미있는 놀이

 

앞서 언급했듯이 웹 서버는 웹 클라이언트에 주기적인 업데이트를 전송하여 세 개의 조명 각각의 현재 켜짐/꺼짐 상태를 전달합니다. 이 정보를 전달하는 방법은 여러 가지가 있지만, 저는 소수는 자기 자신으로만 나누어 떨어진다는 사실을 이용하여 상태 번호(정수)를 생성하여 정보를 전달하기로 했습니다. 서버 측에서 newStatus 값은 다음과 같이 계산됩니다. 

 

int newStatus = 1;
  if(redLED ==1) {newStatus *= redLED*2;}
  if(greenLED == 1) {newStatus *= greenLED*3;}
  if (blueLED == 1) {newStatus *= blueLED*5;}

 

여기서 2, 3, 5는 처음 세 개의 소수를 나타냅니다. 정수 값은 1(LED가 켜지지 않음)에서 30(모든 LED가 켜짐) 사이의 값이 됩니다. 클라이언트 측에서는 아래와 같은 코드(자바 스크립트로 작성)를 사용했습니다. 

 

 if (LEDInt%%2 == 0) {document.getElementById('redLED').style.backgroundColor='red';}
    else {document.getElementById('redLED').style.backgroundColor='gray';}
    if (LEDInt%%3 == 0) {document.getElementById('greenLED').style.backgroundColor='lightgreen';}
    else {document.getElementById('greenLED').style.backgroundColor='gray';}
    if (LEDInt%%5 == 0) {document.getElementById('blueLED').style.backgroundColor='blue';}
    else {document.getElementById('blueLED').style.backgroundColor='gray';}

 

클라이언트에 전달된 상태 값(LEDInt)을 2, 3, 또는 5로 나누고 나머지가 0인지 확인합니다. 0이면 해당 LED가 켜져 있다는 의미이므로 디스플레이의 LED 색상을 적절하게 변경합니다. 애플리케이션에 업데이트할 추가 이진 상태 필드가 있는 경우 7, 11, 13, 17, 19, 23, 29 등과 같이 연속된 소수를 곱하고 나누면 됩니다. 또한 %% 기호를 두 번 사용한 점에 유의하세요. 웹 클라이언트에 실제로 전송되는 명령은 if (LEDInt%2 == 0)… 입니다. 여기서 %는 JavaScript의 나머지 연산자입니다.

 

4과 코딩

 

레슨 4 코딩을 시작하기 전에 레슨 3 코드 결과에 몇 가지 변경을 가해보겠습니다.

 

  1. taskBlinkSeq 코드(레슨 3) 대신 taskBlink 코드(레슨 2)를 사용하도록 깜박이는 LED를 되돌리면 디스플레이에 여러 개의 LED가 동시에 켜집니다.
  2. 수업 2에서처럼 taskTally 코드를 수정하여 켜진 LED의 개수를 표시합니다.
  3. taskSpeed ​​코드는 더 이상 사용되지 않으므로 제거하십시오. 이 코드는 3과에서 명령 실행 코드로 대체되었습니다.
  4. 하이워터 마크 타이머의 출력 빈도를 10초가 아닌 60초마다 출력되도록 변경하세요. 이렇게 하면 시리얼 콘솔 화면이 깔끔해집니다.

 

물론 // 줄 바꿈 표시를 사용하여 사용하지 않는 코드를 주석 처리할 수도 있지만, 대신 코드를 더 쉽게 읽고 이해할 수 있도록 사용하지 않는 코드를 제거하겠습니다. Arduino IDE를 사용하여 Lesson4Start라는 새 스케치에서 아래 코드를 엽니다. 

 

// 1 - define freeRTOS settings
#if CONFIG_FREERTOS_UNICORE
#define ARDUINO_RUNNING_CORE 0
#else
#define ARDUINO_RUNNING_CORE 1
#endif
#define SYSTEM_RUNNING_CORE 0

// 2 - define LCD display settings
#define OLED_ADDR 0x3C
#define OLED_RESET 4
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_SSD1306.h>

// 3 - set up task handles for the FreeRTOS tasks
TaskHandle_t taskGreen;
TaskHandle_t taskRed;
TaskHandle_t taskBlue;
TaskHandle_t taskSpeed;
TaskHandle_t taskTally;
TaskHandle_t taskHighwater;
TaskHandle_t taskCmdParse;
TaskHandle_t taskCmdExec;
TaskHandle_t taskCmd = NULL; // task handle used to pause, restart, or stop running tasks

// 4 - define global variable for LED status
int greenLED = 0;  // LED status indicator 0-off, 1-on
int redLED = 0;
int blueLED = 0;
int *greenPtr = &greenLED;  //pointers to pass to tasks to update led status
int *redPtr = &redLED;
int *bluePtr = &blueLED;
float speedMult = 1.0; // define speed multipier value

// 5 - define structure for data to pass to each task
struct BlinkData {
  int pin;
  int delay;
  float *speedMult;
  int *ptr;
};

// 6 - load up data for each LED task
BlinkData blinkGreen = {15, 2500, &speedMult, &greenLED };
BlinkData blinkRed = {4, 3300, &speedMult, &redLED };
BlinkData blinkBlue = {16, 1800, &speedMult, &blueLED };


// 8 - define the function for highwater mark processing
UBaseType_t uxTaskGetStackHighWaterMark(TaskHandle_t xTask);
TimerHandle_t highwaterTimer; // define data types for the highwater mark timer
BaseType_t hwTimerStarted;


// New section for Lesson 3
//define semaphore object handle
SemaphoreHandle_t syncFlag;

// define mutex object handle
SemaphoreHandle_t printMutex;

// define the queue handle for the queCmd queue
QueueHandle_t queCmd;

// define structure for data in the cmdQueue
const byte numChars = 24;
struct cmdData {
  char  Cmd[numChars];
  char  Obj[numChars];
};

// define  global variables for keyboard entry
char receivedChars[numChars];
char tempChars[numChars];        // temporary array for use when parsing
cmdData valueToSend;

// define global variables to hold the parsed input data
char cmdFromPC[numChars] = {0};
char objFromPC[numChars] = {0};
const char* cmdList = "kill...pause...resume...speed"; // list of accepted values for command
const char* objListLED = "red...green...blue";   // list of accepted values 
const char* objListSpeed = "faster...slower";
boolean newData = false;

// 9 - define interupt processing for the push button
int pinButton = 23; //GPIO pin used for pushbutton
void IRAM_ATTR buttonPush() 
{
  BaseType_t xStatus;
  cmdData sendCmd = {"speed","faster"};
  xStatus = xQueueSendToBack( queCmd, &sendCmd, 0 );
  if( xStatus != pdPASS ) {Serial.println("queue send failed!");}
}

// the setup function runs once when you press reset or power the board
void setup() 
{

  // initialize serial communication at 115200 bits per second:
  Serial.begin(115200);

  // start printer mutex access
  
  printMutex = xSemaphoreCreateMutex();
  if (printMutex != NULL){Serial.println("printMutex created");}

  // initialize queue for commands
  queCmd = xQueueCreate( 5, sizeof( cmdData ) );
  if( queCmd != NULL ) {Serial.println("command queue created");}

  // setup button for interupt processing
  pinMode(pinButton, INPUT_PULLUP);
  attachInterrupt(pinButton, buttonPush, FALLING); 

  // set up a timer to control the highwater mark display
  highwaterTimer = xTimerCreate("Highwater Timer",pdMS_TO_TICKS(60000),pdTRUE,0,hwCallback);
  hwTimerStarted = xTimerStart( highwaterTimer, 0 );

  // Now set up tasks to run independently.
  xTaskCreatePinnedToCore
    (
    TaskBlink, // name of function to invoke
    "TaskBlinkGreen" , // label for the task
    4096 , // This stack size can be checked & adjusted by reading the Stack Highwater   
    (void *)&blinkGreen, // pointer to global data area
    3 , // Priority, 
    &taskGreen, //pointer to task handle
    SYSTEM_RUNNING_CORE // run on the arduino process core
    );

  xTaskCreatePinnedToCore
    (
    TaskBlink, 
    "TaskBlinkRed",
    4096,
    (void *)&blinkRed, 
    3,
    &taskRed, 
    SYSTEM_RUNNING_CORE
    );

  xTaskCreatePinnedToCore
    (
    TaskBlink, 
    "TaskBlinkBlue",  
    4096,  
    (void *)&blinkBlue,
    3, 
    &taskBlue, 
    SYSTEM_RUNNING_CORE
    );

  xTaskCreatePinnedToCore
  (
    TaskTally,
    "TaskTally", 
    4096,  
    NULL, 
    3, 
    &taskTally, 
    ARDUINO_RUNNING_CORE
    );
xTaskCreatePinnedToCore
  (
    TaskHighWater,
    "High Water Mark Display", 
    4096,
    NULL,
    2,
    &taskHighwater, 
    ARDUINO_RUNNING_CORE
    );
xTaskCreatePinnedToCore(
    TaskCmdParse, 
    "Command Parser",  // A name just for humans
    4096,  // This stack size can be checked & adjusted by reading the Stack Highwater
    NULL,
    3,  
    &taskCmdParse,
    ARDUINO_RUNNING_CORE);  //run this in core separate from LED tasks if there are two cores
xTaskCreatePinnedToCore(
    TaskCmdExec, 
    "Execute Commands From Queue",  // A name just for humans
    4096,  // This stack size can be checked & adjusted by reading the Stack Highwate
    NULL, 
    3,  // Priority
    &taskCmdExec,
    ARDUINO_RUNNING_CORE);  //run this in core separate from LED tasks if there are two cores
} //end of setup

// Now the task scheduler, which takes over control of scheduling individual tasks, is automatically started.
 
void loop() {
  // Hooked to Idle task, it will run whenever CPU is idle (i.e. other tasks blocked)
  // DO NOTHING HERE...
 
  /*--------------------------------------------------*/
  /*---------------------- Tasks ---------------------*/
  /*--------------------------------------------------*/
}
void TaskBlink(void *xStruct)  // This is a task template
{
  BlinkData *data = (BlinkData *)xStruct; //passed data cast as BlinkData structure
  // unpack data from the BlinkData structure passed by reference
  int pin = data->pin;
  int delay = data->delay;
  float *speedMult = data->speedMult;
  int *statePtr = data->ptr;

  // set pinMode on output pin
  pinMode(pin, OUTPUT);


  while(true)  // A Task shall never return or exit.
  {
    int delayInt = (delay * (*speedMult)); // get nearest int to new delay time
    digitalWrite(pin, HIGH);           // turn the LED on (HIGH is the voltage level)
    *statePtr = 1;
    vTaskDelay(pdMS_TO_TICKS(delayInt));  // unblock delay for on cycle time
    digitalWrite(pin, LOW);            // turn the LED off by making the voltage LOW
    *statePtr = 0; 
    //UpdateLED();                    // set the LED state to 0
    vTaskDelay(pdMS_TO_TICKS(delayInt)); // unblock delay for off cycle                
  }
}

void TaskTally(void *pvParameters)  // This is a task template
{
  (void)pvParameters;

  TickType_t xLastWaitTime;  //updated after first call by vTaskDelayUntil

  // initialize xLastWaitTime
  xLastWaitTime = xTaskGetTickCount();

  // initialize the lcd display (once)
  Adafruit_SSD1306 Display(OLED_RESET);
  Display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR);
  Display.clearDisplay();
  Display.display();

  while (true)  // A Task shall never return or exit.
  {
    // calculate numer of lamps lit from global variables
    int numLit = redLED + greenLED + blueLED;
    //display data on LCD display
    Display.clearDisplay();
    Display.setTextSize(1);
    Display.setTextColor(WHITE);
    Display.setCursor(40, 0);
    Display.println("LEDs Lit");
    Display.setTextSize(2);
    Display.setTextColor(WHITE);
    Display.setCursor(60, 15);
    Display.println(String(numLit));
    Display.display();
    // short unblocked delay between readings (1/10 second)
    xTaskDelayUntil(&xLastWaitTime, pdMS_TO_TICKS(100));
  }
}
void TaskHighWater(void *pvParameters)
{
  while (true) //run forever
  {
    //vTaskDelay(pdMS_TO_TICKS(5000));   // wait 5 seconds so system is fully running
    // display highwater marks for all  tasks
    Serial.println("***************************");
    Serial.print("High Water Mark for Green LED : ");
    Serial.println(uxTaskGetStackHighWaterMark(taskGreen));
    Serial.print("High Water Mark for Red LED : ");
    Serial.println(uxTaskGetStackHighWaterMark(taskRed));
    Serial.print("High Water Mark for Blue LED : ");
    Serial.println(uxTaskGetStackHighWaterMark(taskBlue));
    Serial.print("High Water Mark for Tally : ");
    Serial.println(uxTaskGetStackHighWaterMark(taskTally));
    Serial.print("High Water Mark for Highwater Task: ");
    Serial.println(uxTaskGetStackHighWaterMark(taskHighwater));
    Serial.print("High Water Mark for Command Parse: ");
    Serial.println(uxTaskGetStackHighWaterMark(taskCmdParse));
    Serial.print("High Water Mark for Command Execute: ");
    Serial.println(uxTaskGetStackHighWaterMark(taskCmdExec));
    Serial.flush(); // make sure last data is written
    
    vTaskSuspend(NULL); // run this task only once
  }
}

void hwCallback( TimerHandle_t xTimer )
{
vTaskResume(taskHighwater); // run the highwater program once
}

void TaskCmdParse(void *pvParameters)  // This is a task template
{
  // This uses non-blocking input routines to read and check user input
  // If values are acceptable the command is sent to the command processor
  // The input format is "command object" where command and object must conform
  // to values in lists.  If either is not valid the user is notified. 

  // define sub functions used here
  (void)pvParameters;
  void parseData(void);
  void checkParsedData(void);
  void recvWithEndMarker(void);

  while (true)  // A Task shall never return or exit. 
  {
    BaseType_t xStatus;  // *** 1 *** typedef statement
    
    recvWithEndMarker();
    if (newData == true) {
      strcpy(tempChars, receivedChars);
      strcat (tempChars, " 1 2 3"); //add spaces in case user didn't enter any
        // this temporary copy is necessary to protect the original data
        //   because strtok() used in parseData() replaces the commas with \0
      parseData();
      checkParsedData();
      newData = false;

      // load values into the struct used to send message in queue and send
      strcpy(valueToSend.Cmd, cmdFromPC);
      strcpy(valueToSend.Obj, objFromPC);
      // *** 2 *** add the input values to the back of the queue
      xStatus = xQueueSendToBack( queCmd, &valueToSend, 0 );
        if( xStatus != pdPASS ) {Serial.println("queue send failed!");}
    } 
  }
}

  void parseData()       // split the data into its parts
    {
      
      char * strtokIndx; // this is used by strtok() as an index

      strtokIndx = strtok(tempChars," ");      // get the first part - the string
      strcpy(cmdFromPC, strtokIndx); // copy it to cmdFromPC

      strtokIndx = strtok(NULL, " "); // this continues where the previous call left off
      strcpy(objFromPC,strtokIndx);  // copy it to objFromPC
    }

  void checkParsedData()
    {
      // *** 3 *** take the printer mutex
      xSemaphoreTake(printMutex,portMAX_DELAY);
      char * cmd;
      char * obj;
      int validRequest = 0;
    cmd = strstr(cmdList,cmdFromPC);
      if (cmd){
        validRequest += 1;}
      else{
        Serial.println("rejected - unrecognized command");
        validRequest  -= 1;
      }
      if (strstr("speed",cmdFromPC))
        {
        obj = strstr(objListSpeed,objFromPC);
        if (obj)
          {
          validRequest  += 1;
          }
        else
          {
          Serial.println("rejected - unrecognized request");
          validRequest -= 1;
          }
        }
      else 
        {
        obj = strstr(objListLED,objFromPC);
        if (obj)
          {
          validRequest  += 1;
          }
        else
          {
          Serial.println("rejected - unrecognized request");
          validRequest -= 1;
          }
        }
      if (validRequest == 2){
        Serial.println("command accepted");
        Serial.println(String(cmdFromPC) + "  " + String(objFromPC));
      }
    Serial.flush();
    vTaskDelay(pdMS_TO_TICKS(3000));  // *** 4 *** flush the print data and wait three seconds for the user to read the response
    xSemaphoreGive(printMutex);
    }

  void recvWithEndMarker() 
    {
      static byte ndx = 0;
      char endMarker = '\n';
      char rc;
      
      while (Serial.available() > 0 && newData == false) {
        rc = Serial.read();

        if (rc != endMarker) {
          receivedChars[ndx] = rc;
          ndx++;
          if (ndx >= numChars) {
              ndx = numChars - 1;
          }
        }
        else {
          receivedChars[ndx] = '\0'; // terminate the string
          ndx = 0;
          newData = true;
        }
      }
    }
    
void TaskCmdExec(void *pvParameters) 
{
(void)pvParameters;
cmdData receivedValue;
BaseType_t xStatus;
const TickType_t xTicksToWait = pdMS_TO_TICKS( 100 );
char nameObj[8];

  while (true) 
  {
    xStatus = xQueueReceive( queCmd, &receivedValue, xTicksToWait );
    if( xStatus == pdPASS )
      {
      xSemaphoreTake(printMutex,portMAX_DELAY);  // get the printer mutex for use of the printer
      // check which led color to modify and set approriate task
      if (strstr("red",receivedValue.Obj))
        {
        strcpy(nameObj,"red");
        taskCmd = taskRed;
        }
      else if (strstr("green",receivedValue.Obj))
        {strcpy(nameObj,"green");
        taskCmd = taskGreen;}
      else if (strstr("blue",receivedValue.Obj)) 
        {strcpy(nameObj,"blue");
        taskCmd = taskBlue;}

      // check which command is sent and respond accordingly
      if (strstr("pause",receivedValue.Cmd)) 
        {
        vTaskSuspend(taskCmd);
        Serial.print(nameObj);
        Serial.println(" paused");
        }
      else if (strstr("resume",receivedValue.Cmd))
        {
          vTaskResume(taskCmd);
          Serial.print(nameObj);
          Serial.println(" resumed");
        }  
      else if (strstr("kill",receivedValue.Cmd))
              {
                vTaskDelete(taskCmd);
                Serial.print(nameObj);
                Serial.println(" killed");
        }
      else if (strstr("speed" , receivedValue.Cmd) ) 
        {
          if (strstr ( "slower" , receivedValue.Obj))
            {speedMult *= 1.5;
            Serial.println("speed is slower");}
          else if (strstr("faster" , receivedValue.Obj)) 
            {speedMult *= 0.67;
            Serial.println("speed is faster");} 
        }
      Serial.flush(); 
      vTaskDelay(pdMS_TO_TICKS(5000)); 
      xSemaphoreGive(printMutex);   
    } 
    else
      {
      /* Do nothing here - Wait for a new entry to be added into the command queue */
      }
    }
}

 

 

이제 앞서 설명한 대로 웹사이트 정의 코드를 추가하는 것으로 시작해 보겠습니다. 첫 번째 단계는 Lesson4Start의 스케치 폴더에 FreeRTOSWebServer.h라는 새 파일을 만들고 아래 코드를 붙여넣는 것입니다. 저는 Windows의 메모장 앱을 사용했지만, 어떤 텍스트 편집기든 사용할 수 있습니다. 

 

const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
  <title>ESP Web Server</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" href="data:,">
  <style>
  html {
    font-family: Arial, Helvetica, sans-serif;
    text-align: center;
  }
  h1 {
    font-size: 1.8rem;
    color: white;
  }
  h2{
    font-size: 1.5rem;
    font-weight: bold;
    color: #143642;
  }
  .topnav {
    overflow: hidden;
    background-color: #143642;
  }
  body {
    margin: 0;
  }
  .content {
    padding: 30px;
    max-width: 600px;
    margin: 0 auto;
  }
  .card {
    background-color: #F8F7F9;;
    box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5);
    padding-top:10px;
    padding-bottom:20px;
  }
  .button {
    padding: 15px 30px;
    font-size: 24px;
    text-align: center;
    outline: none;
    color: #fff;
    background-color: #0f8b8d;
    border: none;
    border-radius: 5px;
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    -khtml-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
    -webkit-tap-highlight-color: rgba(0,0,0,0);
   }
   /*.button:hover {background-color: #0f8b8d}*/
   .button:active {
     background-color: #0f8b8d;
     box-shadow: 2 2px #CDCDCD;
     transform: translateY(2px);
   }
   .state {
     font-size: 1.5rem;
     color:#8c8c8c;
     font-weight: bold;
   }
   .dot {
     height: 50px;
     width: 50px;
     background-color: gray;
     border-radius: 50%%;
     display: inline-block;
     column-gap: 30px;
   }
  </style>
<title>FreeRTOS Web Server</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,">
</head>
<body>
  <div class="topnav">
    <h1>FreeRTOS WebSocket Server</h1>
  </div>
  <div class="content">  
    <div class="card">
      <h2>Current LED Status</h2>
    <div style="text-align:center">
      <span class="dot" id = "blueLED" style=background-color:gray></span>
      <span class="dot" id = "redLED" style=background-color:gray></span>
      <span class="dot" id = "greenLED" style=background-color:gray></span>
      <p>             </p>
      <p><button id="buttonSlow" class="button">Slower</button>   <button id="buttonFast" class="button">Faster</button></p>
    </div>
  </div>
<script>
  var gateway = `ws://${window.location.hostname}/ws`;
  var websocket;
  window.addEventListener('load', onLoad);
  function initWebSocket() {
    console.log('Trying to open a WebSocket connection...');
    websocket = new WebSocket(gateway);
    websocket.onopen    = onOpen;
    websocket.onclose   = onClose;
    websocket.onmessage = onMessage; // <-- add this line
  }
  function onOpen(event) {
    console.log('Connection opened');
  }
  function onClose(event) {
    <!-- set all LEDs to gray to indicate no signal is available -->
    document.getElementById('blueLED').style.backgroundColor='gray';
    document.getElementById('redLED').style.backgroundColor='gray';
    document.getElementById('greenLED').style.backgroundColor='gray';
    console.log('Connection closed');
    setTimeout(initWebSocket, 2000);
  }
  function onMessage(event) {
    var LEDStatus;
    LEDStatus = event.data;
    console.log(LEDStatus);
    LEDInt = parseInt(LEDStatus);
    if (LEDInt%%2 == 0) {document.getElementById('redLED').style.backgroundColor='red';}
    else {document.getElementById('redLED').style.backgroundColor='gray';}
    if (LEDInt%%3 == 0) {document.getElementById('greenLED').style.backgroundColor='lightgreen';}
    else {document.getElementById('greenLED').style.backgroundColor='gray';}
    if (LEDInt%%5 == 0) {document.getElementById('blueLED').style.backgroundColor='blue';}
    else {document.getElementById('blueLED').style.backgroundColor='gray';}
  }
  function onLoad(event) {
    initWebSocket();
    initButtonFast();
    initButtonSlow();
  }
  function initButtonSlow() {
    document.getElementById('buttonSlow').addEventListener('click', slower);
  }
  function slower(){
    websocket.send('slower');
  }
  function initButtonFast() {
    document.getElementById('buttonFast').addEventListener('click', faster);
  }
  function faster(){
    websocket.send('faster');
  }
</script>
</body>
</html>
)rawliteral";

 

 

이 코드의 세부 사항을 줄마다 살펴보지는 않겠지만, 내용에 대한 몇 가지 사항은 다음과 같습니다.

 

1. 첫 번째 줄과 마지막 줄은 Arduino 컴파일러가 원시 콘텐츠를 사용하여 index_html이라는 이름의 char 상수를 채우도록 하는 지시문입니다.

2. 파일의 첫 번째 줄은 콘텐츠를 설명하는 HTML 명령입니다.

3. 마커 사이의 선에는 CSS 정의가 포함되어 있습니다.

 

4. 

마커 사이의 코드에는 JavaScript 코드가 포함되어 있습니다.

5. JavaScript 코드에는 디스플레이를 초기화하고, 웹소켓 서버에 연결하고, 버튼을 눌렀을 때 발생하는 일과 웹소켓 연결에서 새 데이터를 수신했을 때 발생하는 일을 정의하는 코드 함수가 포함되어 있습니다.

6. 다시 한번, 여기의 코딩을 보다 완벽하게 이해하려면 여기에서 찾을 수 있는 ESP32 WebSocket 서버: 제어 출력(Arduino IDE) 이라는 제목의 문서를 참조하세요 .

 

다음으로, Lesson4Start 스케치를 다시 참조하여 코드 파일을 열고 방금 만든 FreeRTOSWebServer.h 파일을 로드하는 참조를 파일 상단에 추가할 수 있습니다. 동시에 웹 서버 및 웹소켓 코드에 필요한 여러 개의 포함 파일과 몇 가지 새로운 전역 변수를 추가합니다. 

 

// 2 - define LCD display settings
#define OLED_ADDR 0x3C
#define OLED_RESET 4
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_SSD1306.h>

// include files for Lesson4
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include "FreeRTOSWebServer.h" //HTML file in same directory

// other definitions & declarations for Lesson 4
// Replace with your network credentials
const char* ssid = "your network SSID";
const char* password = "your password";
int statusLED = 1;

 

ssid 및 password 변수 값에 Wi-Fi 네트워크 로그인 정보를 입력해야 합니다. 또한, Arduino IDE에 AsyncTCP 및 ESPAsyncWebServer 호출을 처리할 두 개의 새 라이브러리를 추가해야 합니다. 도구 메뉴에서 라이브러리 관리를 선택 하고 이름을 입력한 후 설치를 선택하여 프로젝트에 추가합니다. 바로 아래에는 아래와 같이 webserver 및 webupdate 작업에 대한 추가 정의 정보가 있습니다. 

 

// Create AsyncWebServer object on port 80
AsyncWebServer server(80);
AsyncWebSocket ws("/ws");

// define FreeRTOS task functions and handles 
void TaskWebServer(void *pvParameters);
void TaskWebUpdate(void *pvParameters);
TaskHandle_t taskWeb;
TaskHandle_t taskUpdate;

 

이러한 줄은 웹 서버와 웹 소켓에 대한 전역 객체를 정의하고 TaskWebServer와 TaskWebUpdate라는 새로운 FreeRTOS 작업을 정의합니다.

 

void setup() 문 바로 앞에 아래와 같이 레슨 4에서 필요한 새로운 함수 정의를 추가합니다. 

 

// global functions added in Lesson4
void notifyClients() {
  // send all clients the newest value of statusLED
  ws.textAll(String(statusLED));
}
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;
    if (strcmp((char*)data, "slower") == 0) 
      {
        BaseType_t xStatus;
        cmdData sendCmd = {"speed","slower"};
        xStatus = xQueueSendToBack( queCmd, &sendCmd, 0 );
        if( xStatus != pdPASS ) {Serial.println("queue send failed!");}
      }
    if (strcmp((char*)data, "faster") == 0) 
      {
        BaseType_t xStatus;
        cmdData sendCmd = {"speed","faster"};
        xStatus = xQueueSendToBack( queCmd, &sendCmd, 0 );
        if( xStatus != pdPASS ) {Serial.println("queue send failed!");}
      }
  }
}
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;
  }
}
String processLED(const String& var){
  // set inital value for LED status - all LEDs lit
  return String("30");
}
void TaskWebServer(void *pvParameters)  // This is a task template
{
   // set up websocket handlers
  ws.onEvent(onEvent);
  server.addHandler(&ws);

  // send HTML/CSS/Javascript to new client when called
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
  request->send_P(200, "text/html", index_html, processLED);
  });

  // Start web server
  server.begin(); 
  while(true){}
} 
void TaskWebUpdate(void *pvParameters)  // This is a task template
{
  while (true)
  {
    ws.cleanupClients(); //cleans up disconnected websocket connections

  int newStatus = 1;
  if(redLED ==1) {newStatus *= redLED*2;}
  if(greenLED == 1) {newStatus *= greenLED*3;}
  if (blueLED == 1) {newStatus *= blueLED*5;}   
  if (newStatus != statusLED)
    //Serial.println(newStatus);
    {statusLED = newStatus;
    notifyClients();
    vTaskDelay(pdMS_TO_TICKS(250));
    }
  }

}
// the setup function runs once when you press reset or power the board

 

이러한 기능은 다음과 같은 작업을 수행합니다.

 

1. notifyClients는 연결된 모든 클라이언트에 업데이트 LED 상태를 전송합니다.

2. handleWebSocketMessage는 웹소켓 클라이언트로부터 메시지가 수신될 때마다 호출됩니다. 이 경우 사용자가 디스플레이에서 "빠르게" 또는 "느리게" 버튼을 눌렀음을 의미합니다. 이 코드는 3장에서 생성한 명령 큐(queCMD)에 새 메시지를 추가하는 것에 불과합니다.

3. onEvent는 웹소켓 연결이 생성되거나 종료될 때마다 호출됩니다. 이 경우에는 콘솔에 메시지를 작성하기만 하면 됩니다.

4. processLED는 첫 번째 웹소켓 메시지가 처리되기 전에 디스플레이를 초기화하는 데 사용됩니다. 세 개의 LED를 모두 켜짐 상태로 설정합니다.

5. TaskWebServer 는 웹 서버를 실행하는 FreeRTOS 태스크입니다. 웹 클라이언트의 연결을 위한 요청을 처리하고 수신되는 웹소켓 메시지를 수신합니다.

6. TaskWebUpdate 는 1/4초마다 실행되어 세 개의 LED 상태를 확인하는 FreeRTOS 작업입니다. 상태가 변경된 LED가 있으면 현재 연결된 모든 클라이언트에 업데이트 메시지가 전송됩니다. 앞서 언급한 소수 루틴을 사용하여 LED 상태 코드를 생성합니다.

 

다음으로 설정 루틴에 몇 가지 코드를 추가합니다. 

 

// initialize serial communication at 115200 bits per second:
  Serial.begin(115200);

  // Lesson 4 setup additions
  // Connect to Wi-Fi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi..");
  }

  // Print ESP Local IP Address
  Serial.println(WiFi.localIP());

 

설정을 시작할 무렵 WiFi 네트워크를 시작하고 로컬 IP 주소를 출력하는 코드를 추가합니다. 이를 통해 웹 브라우저에서 연결할 위치를 알 수 있습니다.

 

이제 아래와 같이 두 개의 새로운 FreeRTOS 작업을 시작하는 코드를 추가합니다. 이 코드들은 이전 정의 바로 아래에 추가됩니다. 

 

// new tasks for Lesson 4
xTaskCreatePinnedToCore
    (
    TaskWebServer, 
    "FreeRTOS Web Server",  
    10000,  
    NULL,
    3, 
    &taskWeb, 
    ARDUINO_RUNNING_CORE
    );

xTaskCreatePinnedToCore
    (
    TaskWebUpdate, 
    "Update clients",  
    4096,  
    NULL,
    3, 
    &taskUpdate, 
    ARDUINO_RUNNING_CORE
  );

 

마지막으로 TaskHighWater 코드를 업데이트하여 응답에 두 개의 새 작업을 포함합니다. 아래와 같이 taskCmdExec의 두 줄 뒤에 다음 줄을 삽입합니다. 

 

    
    Serial.print("High Water Mark for Command Execute: ");
    Serial.println(uxTaskGetStackHighWaterMark(taskCmdExec));
    Serial.print("High Water Mark for Web Server: ");
    Serial.println(uxTaskGetStackHighWaterMark(taskWeb));
    Serial.print("High Water Mark for Web Update: ");
    Serial.println(uxTaskGetStackHighWaterMark(taskUpdate));

 

최종 코드는 다음과 같습니다. Lesson4라는 새 스케치로 저장할 수 있습니다. 

 

/*********
This code was developed for use with the tutorial series entitled
"Implementing FreeRTOS Solutions on ESP 32 Devices using Arduino"  

Refer to https://medium.com/@tomw3115/implementing-freertos-solutions-on-esp-32-devices-using-arduino-114e05f7138a for more information.

This software is provided as-is for hobbyist use and educational purposes only.

published by Tom Wilson - September 2024 *********/

// 1 - define freeRTOS settings
#if CONFIG_FREERTOS_UNICORE
#define ARDUINO_RUNNING_CORE 0
#else
#define ARDUINO_RUNNING_CORE 1
#endif
#define SYSTEM_RUNNING_CORE 0

// 2 - define LCD display settings
#define OLED_ADDR 0x3C
#define OLED_RESET 4
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_SSD1306.h>

// include files for Lesson4
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include "FreeRTOSWebServer.h" //HTML file in same directory

// other definitions & declarations for Lesson4
// Replace with your network credentials
const char* ssid = "Pres2";
const char* password = "pass5876";
int statusLED = 1;

// Create AsyncWebServer object on port 80
AsyncWebServer server(80);
AsyncWebSocket ws("/ws");

// define FreeRTOS task functions and handles 
void TaskWebServer(void *pvParameters);
void TaskWebUpdate(void *pvParameters);
TaskHandle_t taskWeb;
TaskHandle_t taskUpdate;

// 3 - set up task handles for the FreeRTOS tasks
TaskHandle_t taskGreen;
TaskHandle_t taskRed;
TaskHandle_t taskBlue;
TaskHandle_t taskSpeed;
TaskHandle_t taskTally;
TaskHandle_t taskHighwater;
TaskHandle_t taskCmdParse;
TaskHandle_t taskCmdExec;
TaskHandle_t taskCmd = NULL; // task handle used to pause, restart, or stop running tasks

// 4 - define global variable for LED status
int greenLED = 0;  // LED status indicator 0-off, 1-on
int redLED = 0;
int blueLED = 0;
int *greenPtr = &greenLED;  //pointers to pass to tasks to update led status
int *redPtr = &redLED;
int *bluePtr = &blueLED;
float speedMult = 1.0; // define speed multipier value

// 5 - define structure for data to pass to each task
struct BlinkData {
  int pin;
  int delay;
  float *speedMult;
  int *ptr;
};

// 6 - load up data for each LED task
BlinkData blinkGreen = {15, 2500, &speedMult, &greenLED };
BlinkData blinkRed = {4, 3300, &speedMult, &redLED };
BlinkData blinkBlue = {16, 1800, &speedMult, &blueLED };


// 8 - define the function for highwater mark processing
UBaseType_t uxTaskGetStackHighWaterMark(TaskHandle_t xTask);
TimerHandle_t highwaterTimer; // define data types for the highwater mark timer
BaseType_t hwTimerStarted;


// New section for Lesson 3
//define semaphore object handle
SemaphoreHandle_t syncFlag;

// define mutex object handle
SemaphoreHandle_t printMutex;

// define the queue handle for the queCmd queue
QueueHandle_t queCmd;

// define structure for data in the cmdQueue
const byte numChars = 24;
struct cmdData {
  char  Cmd[numChars];
  char  Obj[numChars];
};

// define  global variables for keyboard entry
char receivedChars[numChars];
char tempChars[numChars];        // temporary array for use when parsing
cmdData valueToSend;

// define global variables to hold the parsed input data
char cmdFromPC[numChars] = {0};
char objFromPC[numChars] = {0};
const char* cmdList = "kill...pause...resume...speed"; // list of accepted values for command
const char* objListLED = "red...green...blue";   // list of accepted values 
const char* objListSpeed = "faster...slower";
boolean newData = false;

// 9 - define interupt processing for the push button
int pinButton = 23; //GPIO pin used for pushbutton
void IRAM_ATTR buttonPush() 
{
  //vTaskResume(taskSpeed); // run the speed program once
  BaseType_t xStatus;
  cmdData sendCmd = {"speed","faster"};
  xStatus = xQueueSendToBack( queCmd, &sendCmd, 0 );
  if( xStatus != pdPASS ) {Serial.println("queue send failed!");}
}

// global functions added in Lesson4
void notifyClients() {
  // send all clients the newest value of statusLED
  ws.textAll(String(statusLED));
}
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;
    if (strcmp((char*)data, "slower") == 0) 
      {
        BaseType_t xStatus;
        cmdData sendCmd = {"speed","slower"};
        xStatus = xQueueSendToBack( queCmd, &sendCmd, 0 );
        if( xStatus != pdPASS ) {Serial.println("queue send failed!");}
      }
    if (strcmp((char*)data, "faster") == 0) 
      {
        BaseType_t xStatus;
        cmdData sendCmd = {"speed","faster"};
        xStatus = xQueueSendToBack( queCmd, &sendCmd, 0 );
        if( xStatus != pdPASS ) {Serial.println("queue send failed!");}
      }
  }
}
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;
  }
}
String processLED(const String& var){
  // set inital value for LED status - all LEDs lit
  return String("30");
}
void TaskWebServer(void *pvParameters)  // This is a task template
{
   // set up websocket handlers
  ws.onEvent(onEvent);
  server.addHandler(&ws);

  // send HTML/CSS/Javascript to new client when called
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
  request->send_P(200, "text/html", index_html, processLED);
  });

  // Start web server
  server.begin(); 
  while(true){}
} 
void TaskWebUpdate(void *pvParameters)  // This is a task template
{
  while (true)
  {
  ws.cleanupClients(); //cleans up disconnected websocket connections

  int newStatus = 1;
  if(redLED ==1) {newStatus *= redLED*2;}
  if(greenLED == 1) {newStatus *= greenLED*3;}
  if (blueLED == 1) {newStatus *= blueLED*5;}   
  if (newStatus != statusLED)
    //Serial.println(newStatus);
    {statusLED = newStatus;
    notifyClients();
    vTaskDelay(pdMS_TO_TICKS(200));
    }
  }

}

// the setup function runs once when you press reset or power the board
void setup() {

  // initialize serial communication at 115200 bits per second:
  Serial.begin(115200);

  // Lesson 4 setup additions
  // Connect to Wi-Fi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi..");
  }

  // Print ESP Local IP Address
  Serial.println(WiFi.localIP());
  
  // create the binary semaphore
  syncFlag = xSemaphoreCreateBinary();
  if (syncFlag != NULL){Serial.println("flag OK");}

  // start printer mutex access
  
  printMutex = xSemaphoreCreateMutex();
  if (printMutex != NULL){Serial.println("printMutex created");}

  // initialize queue for commands
  queCmd = xQueueCreate( 5, sizeof( cmdData ) );
  if( queCmd != NULL ) {Serial.println("command queue created");}

  // setup button for interupt processing
  pinMode(pinButton, INPUT_PULLUP);
  attachInterrupt(pinButton, buttonPush, FALLING); 

  highwaterTimer = xTimerCreate("Highwater Timer",pdMS_TO_TICKS(10000),pdTRUE,0,hwCallback);
  hwTimerStarted = xTimerStart( highwaterTimer, 0 );

  // Now set up tasks to run independently.
  xTaskCreatePinnedToCore
    (
    TaskBlink, // name of function to invoke
    "TaskBlinkGreen" , // label for the task
    4096 , // This stack size can be checked & adjusted by reading the Stack Highwater   
    (void *)&blinkGreen, // pointer to global data area
    3 , // Priority, 
    &taskGreen, //pointer to task handle
    SYSTEM_RUNNING_CORE // run on the arduino process core
    );

  xTaskCreatePinnedToCore
    (
    TaskBlink, 
    "TaskBlinkRed",
    4096,
    (void *)&blinkRed, 
    3,
    &taskRed, 
    SYSTEM_RUNNING_CORE
    );

  xTaskCreatePinnedToCore
    (
    TaskBlink, 
    "TaskBlinkBlue",  
    4096,  
    (void *)&blinkBlue,
    3, 
    &taskBlue, 
    SYSTEM_RUNNING_CORE
    );

  xTaskCreatePinnedToCore
  (
    TaskTally,
    "TaskTally", 
    4096,  
    NULL, 
    2, 
    &taskTally, 
    ARDUINO_RUNNING_CORE
    );
xTaskCreatePinnedToCore
  (
    TaskHighWater,
    "High Water Mark Display", 
    4096,
    NULL,
    2,
    &taskHighwater, 
    0
    );
xTaskCreatePinnedToCore(
    TaskCmdParse, 
    "Command Parser",  // A name just for humans
    4096,  // This stack size can be checked & adjusted by reading the Stack Highwater
    NULL,
    3,  
    &taskCmdParse,
    ARDUINO_RUNNING_CORE);  //run this in core separate from LED tasks if there are two cores
xTaskCreatePinnedToCore(
    TaskCmdExec, 
    "Execute Commands From Queue",  // A name just for humans
    4096,  // This stack size can be checked & adjusted by reading the Stack Highwate
    NULL, 
    3,  // Priority
    &taskCmdExec,
    ARDUINO_RUNNING_CORE);  //run this in core separate from LED tasks if there are two cores
// new tasks for Lesson 4
xTaskCreatePinnedToCore
    (
    TaskWebServer, 
    "FreeRTOS Web Server",  
    4096,  
    NULL,
    3, 
    &taskWeb, 
    ARDUINO_RUNNING_CORE
    );

xTaskCreatePinnedToCore
    (
    TaskWebUpdate, 
    "Update clients",  
    4096,  
    NULL,
    3, 
    &taskUpdate, 
    ARDUINO_RUNNING_CORE
  );    

} //end of setup

// Now the task scheduler, which takes over control of scheduling individual tasks, is automatically started.
 
void loop() {
  // Hooked to Idle task, it will run whenever CPU is idle (i.e. other tasks blocked)
  // DO NOTHING HERE...
 
  /*--------------------------------------------------*/
  /*---------------------- Tasks ---------------------*/
  /*--------------------------------------------------*/
}
void TaskBlink(void *xStruct)  // This is a task template
{
  BlinkData *data = (BlinkData *)xStruct; //passed data cast as BlinkData structure
  // unpack data from the BlinkData structure passed by reference
  int pin = data->pin;
  int delay = data->delay;
  float *speedMult = data->speedMult;
  int *statePtr = data->ptr;

  // set pinMode on output pin
  pinMode(pin, OUTPUT);


  while(true)  // A Task shall never return or exit.
  {
    int delayInt = (delay * (*speedMult)); // get nearest int to new delay time
    digitalWrite(pin, HIGH);           // turn the LED on (HIGH is the voltage level)
    *statePtr = 1;                     // set the LED state to 1
    vTaskDelay(pdMS_TO_TICKS(delayInt));  // unblock delay for on cycle time
    digitalWrite(pin, LOW);            // turn the LED off by making the voltage LOW
    *statePtr = 0; // set the LED state to 0
    vTaskDelay(pdMS_TO_TICKS(delayInt)); // unblock delay for off cycle                
  }
}

void TaskTally(void *pvParameters)  // This is a task template
{
  (void)pvParameters;

  TickType_t xLastWaitTime;  //updated after first call by vTaskDelaUntil

  // initialize xLastWaitTime
  xLastWaitTime = xTaskGetTickCount();

  // initialize the lcd display (once)
  Adafruit_SSD1306 Display(OLED_RESET);
  Display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR);
  Display.clearDisplay();
  Display.display();

  while (true)  // A Task shall never return or exit.
  {
    int numLit = redLED + greenLED + blueLED;
    //display data on LCD display
    Display.clearDisplay();
    Display.setTextSize(1);
    Display.setTextColor(WHITE);
    Display.setCursor(40, 0);
    Display.println("LEDs Lit");
    Display.setTextSize(2);
    Display.setTextColor(WHITE);
    Display.setCursor(60, 15);
    Display.println(String(numLit));
    Display.display();
    // short unblocked delay between readings (1/10 second)
    xTaskDelayUntil(&xLastWaitTime, pdMS_TO_TICKS(100));
  }
}
void TaskHighWater(void *pvParameters)
{
  while (true) //run forever
  {
    //vTaskDelay(pdMS_TO_TICKS(5000));   // wait 5 seconds so system is fully running
    // display highwater marks for all 6 tasks
    Serial.println("***************************");
    Serial.print("High Water Mark for Green LED : ");
    Serial.println(uxTaskGetStackHighWaterMark(taskGreen));
    Serial.print("High Water Mark for Red LED : ");
    Serial.println(uxTaskGetStackHighWaterMark(taskRed));
    Serial.print("High Water Mark for Blue LED : ");
    Serial.println(uxTaskGetStackHighWaterMark(taskBlue));
    Serial.print("High Water Mark for Tally : ");
    Serial.println(uxTaskGetStackHighWaterMark(taskTally));
    Serial.print("High Water Mark for Highwater Task: ");
    Serial.println(uxTaskGetStackHighWaterMark(taskHighwater));
    Serial.print("High Water Mark for Speed: ");
    Serial.println(uxTaskGetStackHighWaterMark(taskSpeed));
    Serial.print("High Water Mark for Command Parse: ");
    Serial.println(uxTaskGetStackHighWaterMark(taskCmdParse));
    Serial.print("High Water Mark for Command Execute: ");
    Serial.println(uxTaskGetStackHighWaterMark(taskCmdExec));
    Serial.print("High Water Mark for Web Server: ");
    Serial.println(uxTaskGetStackHighWaterMark(taskWeb));
    Serial.print("High Water Mark for Web Update: ");
    Serial.println(uxTaskGetStackHighWaterMark(taskUpdate));
    Serial.flush(); // make sure last data is written
    vTaskSuspend(NULL); // run this task only once
  }
}

void hwCallback( TimerHandle_t xTimer )
{
vTaskResume(taskHighwater); // run the highwater program once
}

void TaskCmdParse(void *pvParameters)  // This is a task template
{
  // This uses non-blocking input routines to read and check user input
  // If values are acceptable the command is sent to the command processor
  // The input format is "command object" where command and object must conform
  // to values in lists.  If either is not valid the user is notified. 

  // define sub functions used here
  (void)pvParameters;
  void parseData(void);
  void checkParsedData(void);
  void recvWithEndMarker(void);


  while (true)  // A Task shall never return or exit. 
  {
    BaseType_t xStatus;  // *** 1 *** typedef statement
    
    recvWithEndMarker();
    if (newData == true) {
      strcpy(tempChars, receivedChars);
      strcat (tempChars, " 1 2 3"); //add spaces in case user didn't enter any
        // this temporary copy is necessary to protect the original data
        //   because strtok() used in parseData() replaces the commas with \0
      parseData();
      checkParsedData();
      newData = false;

      // load values into the struct used to send message in queue and send
      strcpy(valueToSend.Cmd, cmdFromPC);
      strcpy(valueToSend.Obj, objFromPC);
      // *** 2 *** add the input values to the back of the queue
      xStatus = xQueueSendToBack( queCmd, &valueToSend, 0 );
        if( xStatus != pdPASS ) {Serial.println("queue send failed!");}
    } 
  }
}

  void parseData()       // split the data into its parts
    {
      
      char * strtokIndx; // this is used by strtok() as an index

      strtokIndx = strtok(tempChars," ");      // get the first part - the string
      strcpy(cmdFromPC, strtokIndx); // copy it to cmdFromPC

      strtokIndx = strtok(NULL, " "); // this continues where the previous call left off
      strcpy(objFromPC,strtokIndx);  // copy it to objFromPC
    }

  void checkParsedData()
    {
      // *** 3 *** take the printer mutex
      xSemaphoreTake(printMutex,portMAX_DELAY);
      char * cmd;
      char * obj;
      int validRequest = 0;
    cmd = strstr(cmdList,cmdFromPC);
      if (cmd){
        validRequest += 1;}
      else{
        Serial.println("rejected - unrecognized command");
        validRequest  -= 1;
      }
      if (strstr("speed",cmdFromPC))
        {
        obj = strstr(objListSpeed,objFromPC);
        if (obj)
          {
          validRequest  += 1;
          }
        else
          {
          Serial.println("rejected - unrecognized request");
          validRequest -= 1;
          }
        }
      else 
        {
        obj = strstr(objListLED,objFromPC);
        if (obj)
          {
          validRequest  += 1;
          }
        else
          {
          Serial.println("rejected - unrecognized request");
          validRequest -= 1;
          }
        }
      if (validRequest == 2){
        Serial.println("command accepted");
        Serial.println(String(cmdFromPC) + "  " + String(objFromPC));
      }
    Serial.flush();
    vTaskDelay(pdMS_TO_TICKS(3000));  // *** 4 *** flush the print data and wait three seconds for the user to read the response
    xSemaphoreGive(printMutex);
    }

  void recvWithEndMarker() 
    {
      static byte ndx = 0;
      char endMarker = '\n';
      char rc;
      
      while (Serial.available() > 0 && newData == false) {
        rc = Serial.read();

        if (rc != endMarker) {
          receivedChars[ndx] = rc;
          ndx++;
          if (ndx >= numChars) {
              ndx = numChars - 1;
          }
        }
        else {
          receivedChars[ndx] = '\0'; // terminate the string
          ndx = 0;
          newData = true;
        }
      }
    }
    
void TaskCmdExec(void *pvParameters) 
{
(void)pvParameters;
cmdData receivedValue;
BaseType_t xStatus;
const TickType_t xTicksToWait = pdMS_TO_TICKS( 100 );
char nameObj[8];

  while (true) 
  {
    xStatus = xQueueReceive( queCmd, &receivedValue, xTicksToWait );
    if( xStatus == pdPASS )
      {
      xSemaphoreTake(printMutex,portMAX_DELAY);  // get the printer mutex for use of the printer
      // check which led color to modify and set approriate task
      if (strstr("red",receivedValue.Obj))
        {
        strcpy(nameObj,"red");
        taskCmd = taskRed;
        }
      else if (strstr("green",receivedValue.Obj))
        {strcpy(nameObj,"green");
        taskCmd = taskGreen;}
      else if (strstr("blue",receivedValue.Obj)) 
        {strcpy(nameObj,"blue");
        taskCmd = taskBlue;}

      // check which command is sent and respond accordingly
      if (strstr("pause",receivedValue.Cmd)) 
        {
        vTaskSuspend(taskCmd);
        Serial.print(nameObj);
        Serial.println(" paused");
        }
      else if (strstr("resume",receivedValue.Cmd))
        {
          vTaskResume(taskCmd);
          Serial.print(nameObj);
          Serial.println(" resumed");
        }  
      else if (strstr("kill",receivedValue.Cmd))
              {
                vTaskDelete(taskCmd);
                Serial.print(nameObj);
                Serial.println(" killed");
        }
      else if (strstr("speed" , receivedValue.Cmd) ) 
        {
          if (strstr ( "slower" , receivedValue.Obj))
            {speedMult *= 1.5;
            Serial.println("speed is slower");}
          else if (strstr("faster" , receivedValue.Obj)) 
            {speedMult *= 0.67;
            Serial.println("speed is faster");} 
        }
      Serial.flush(); 
      vTaskDelay(pdMS_TO_TICKS(5000)); 
      xSemaphoreGive(printMutex);   
    } 
    else
      {
      /* Do nothing here - Wait for a new entry to be added into the command queue */
      }
    }
}

 

이 코드를 컴파일하여 ESP 32 브레드보드 회로에 다운로드하세요. 시작 시 아래와 같이 시리얼 콘솔 디스플레이 출력이 표시됩니다. 

 

 

새 웹 서버에 연결하려면 다른 기기에서 브라우저를 열고 콘솔 창에 표시된 URL(예: 위의 경우 192.168.0.173)을 입력하세요. 브라우저 화면은 다음과 같아야 하며, 브레드보드의 실제 LED 색상에 따라 LED 색상이 순차적으로 동적으로 업데이트되어야 합니다. 

 

 

Slower 및 Faster 버튼을 누르면 표시 순서가 그에 따라 더 빠르거나 느리게 변경됩니다. 다음으로, 다른 기기에서 다른 브라우저 창을 열어 서버가 모든 화면을 동시에 업데이트하고 모든 기기의 입력에 선입 선출 방식으로 응답하는 모습을 확인할 수 있습니다. 실제 동작 모습은 다음과 같습니다. 

 

 

축하합니다 . 이제 ESP 32 점멸 LED 보드용 양방향 웹 인터페이스를 만들었습니다. 하지만 더 중요한 것은 (그리고 바라건대) 향후 프로젝트에 웹 서버 인터페이스를 통합하는 방법을 배우셨다는 것입니다. 장치에는 간단한 웹 인터페이스를 통해 모니터링하고 제어해야 하는 다양한 종류의 센서가 많이 포함되어 있을 수 있습니다. FreeRTOS 작업 관리 기능과 통합 웹 서비스의 조합은 ESP 32 시스템을 다양한 IoT 및 홈 자동화 프로젝트에 이상적인 솔루션으로 만들어줍니다.

 

이 4부작 튜토리얼이 도움이 되고 유익한 시간이 되었기를 바랍니다. 더 중요한 것은, 이 튜토리얼을 통해 배운 기술을 활용하여 새로운 프로젝트를 직접 만들어 보는 데 도움이 되었기를 바랍니다. 깜빡이는 LED 디스플레이를 만드는 것 외에도 이 모든 지식을 활용하여 뭔가 할 수 있는 일이 분명 있을 거예요. 댓글이나 박수 부탁드립니다. 

 

여기까지 고생하셨습니다.

 

 

반응형

더욱 좋은 정보를 제공하겠습니다.~ ^^