Implementing FreeRTOS Solutions on ESP 32 Devices using Arduino

Tom Wilson
28 min readSep 10, 2024

--

Tutorial Lesson 4 of 4

Through the completion of Tutorial Lesson 3, we have built an ESP 32 based breadboard circuit and successfully programmed its functionality using FreeRTOS and the Arduino IDE. We learned the basics of real-time operating system programming using tasks, priorities, interrupts, mutexes, queues, and timers to control the flow of execution and ensure that the system is processing the most important task at any given time. We ended up with a system that is running six simultaneous tasks and that responds to inputs from the console keyboard and a simple push button. In Lesson 4 we will add a webserver to the configuration and use a websocket connection to provide a bidirectional communication link with the system.

The ESP 32 board contains a wifi transceiver and the Arduino IDE includes libraries that can be used to create and manage web servers and websocket connections. In this tutorial we will demonstrate how these capabilites can be used in conjunction with FreeRTOS to extend our previous system. I don’t propose to make this a tutorial about webservers and websockets in general — it would take too many pages. Instead I refer you to the article ESP32 WebSocket Server: Control Outputs (Arduino IDE) that can be found here. It covers a lot of the methodogy used here in much greater detail. In fact, you can run the code in the attached article on your ESP 32 breadboard without any modifications. My focus will be on the unique characteristics of our FreeRTOS-based system.

Web Server and Web Sockets Overview

To build an interactive front end to our system that will run in most modern web browsers we will be using a webserver that processes standard HTML code and a client interface that uses standard HTML, CSS, and Javascript services. At a high level, when a client connects to the web server by entering the URL:

1. The webserver (i.e. our ESP 32 device) sends the web page definition information back to the client.

2. The client loads the webpage locally and then executes a startup script. The startup script defines what happens when buttons are pushed or when new messages are received from the server. In our case the startup script also sends a request back to the server to open a web socket connection.

3. The server opens the web socket connection and then transmits periodic update information (regarding the state of the LED lights) to the client. It also listens for requests coming from the client (Faster or Slower) and responds to them.

4. This two way communication is maintained as long as the connection remains active. When the link is broken the web socket is closed.

The web server on the ESP 32 is capable of maintaining multiple connections at the same time, sending updates to all the connected clients and responding to requests from all of them as well. We will use the command queue we built in Lesson 3 to manage the requests coming from multiple clients.

Lesson 4 Objectives

In Lesson 4 we will expand on the system we’ve developed so far by adding two additional FreeRTOS tasks: A WebServer task to manage the requests and connections from the web clients, and a WebUpdate task to periodically (every 0.25 seconds) send the LED light status to all connected clients. Before we get into the code there are several other topics I would like to cover.

Website Definition Files

If you’ve developed web sites previously, you have probably worked with html, css, and js files in defining the look and feel of your site.

  • HTML (Hypertext Markup Language) is used to control the interface between standard browsers and servers. It can be considered the control language for the internet.
  • CSS (Cascading Style Sheets) is used to define the look and feel of a site, defining such things as colors, fonts, sizes, and responses to events (tooltips, hover over, etc), and many others. Separating the look and feel from the actual interface definition allows developers to maintain consistency between multiple web pages and also to change the look and feel of many pages at once by changing the CSS definitions.
  • JS (JavaScript) files are used to contain Java scripts that can be used to perform calculations and conditional logic, update values or characteristics of page elements, or interact with the web server through the web socket connection.

Typically these three types of information are split up into three or more files such as Project.html, Project.css, and Project.js. But for our simple interface project we are going to take a different approach in three ways:

  1. We will include everything in one “file”. You can include css and js information in an html file by using the <style> …</style> markers to encapsulate css commands and the <script>…</script> markers to encapsulate Java script commands.
  2. Since our ESP device doesn’t have a disk of any type, we will store the information in a character array and serve it to the clients from this array. This is accomplished by using an Arduino/C++ rawliteral command that loads text information line by line into a character array. If your ESP 32 device has an SD card reader device, you can load the file data onto it and then install a special driver called SPIFFS to access the data. See the article here for details. If you have a complex web interface that takes a lot of space this a better solution.
  3. Since the html/css/js contents take up a lot of code lines (149 to be exact), I decided to move them into a header file that is loaded when the system compiles the code. In this way we don’t have to scroll through it all the time when updating our system.

One lesson I learned the hard way. If the rawliteral stream contains a % sign it does not get recorded correctly — it is assumed to be a control character. You must insert two percent signs %% and it will include one in the character stream. It took me a long time to uncover this fact.

Fun With Prime Numbers

As mentioned previously, the webserver sends periodic updates to the web clients to pass on the current lit/unlit status of each of the three lights. There are many ways this information could be passed, but I decided to use the fact that prime numbers are only divisable by themselves to create a status number (integer) to transfer the information. On the server side, the newStatus value is calculated as below.

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

Where 2,3,and 5 refer to the first 3 prime numbers. The integer value will be between 1 (no LEDs lit) and 30 (all LEDs lit). On the client side we used code as below (written in Java script).

    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';}

We simply divide the status value passed to the client (LEDInt) by 2,3, or5 and determine if the remainder is 0. If so it means the corresponding LED is lit — so we change the color of the LED on the display appropriately. If you have additional binary status fields to update in your application you can simply multiply and divide by successive prime numbers such as 7, 11, 13, 17, 19, 23, 29, etc. Also note the use of double %% signs — the actual command sent to the web client is if (LEDInt%2 == 0)… where % is the remainder operator in JavaScript.

Lesson 4 Coding

We are going to make a few changes to our Lesson 3 code results before starting the Lesson 4 coding.

  1. Revert the blinking LEDs to use the taskBlink code (from Lesson 2) rather that taskBlinkSeq code (from Lesson 3) as the display will then have multiple LEDs lit simultaneously.
  2. Modify the taskTally code to display the number of LEDs lit as in Lesson 2.
  3. Remove the taskSpeed code as it is no longer used. It was replaced by the command execution code in Lesson 3.
  4. Change the frequency of the highwater mark timer so that it prints out once every 60 seconds instead of every 10 seconds. This will declutter the serial console display.

Of course we could simply comment out the unused code with // line markers, but instead we will remove the unused code so that our code is easier to read and understand. Using the Arduino IDE, open the code below in a new sketch called 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 */
}
}
}

Now let’s start by adding the code for the website definition as described previously. The first step is create a new file in the sketch folder for Lesson4Start called FreeRTOSWebServer.h and paste the code below into the file. I used the notepad app on Windows to do this but any text editor will work.

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";

I won’t go through the details of this code line by line, but here are a few points about the content.

  1. The first and last lines are directives to the Arduino compiler to use the raw content to fill a char constant named index_html
  2. The initial lines in the file are HTML commands describing the content.
  3. The lines between the <style> and </style> markers contain CSS definitions.
  4. The code between the <script> and </script> markers contain JavaScript code.
  5. The JavaScript code includes code functions that initialize the display, connect to the websocket server, and define what happens when the buttons are pushed and also what happens when new data is received from the websocket connection.
  6. Again, for a more complete understanding of the coding here, please refer to the article entitled ESP32 WebSocket Server: Control Outputs (Arduino IDE) that can be found here.

Next, referring back to the Lesson4Start sketch, open the code file and we can add the reference to load the FreeRTOSWebServer.h file we just created near the top of the file. At the same time we will add several other include files needed for the webserver and websocket code and also some new global variables.

// 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;

Notice that you will need to insert your wifi network login information in the ssid and password variable values. Also, you will need to add two new libraries in the Arduino IDE to handle the calls to AsyncTCP and ESPAsyncWebServer. From the Tools menu select Manage Libraries and enter the names, and select Install to add them to your project. Just below this we can additional definition information for the webserver and webupdate tasks as shown below.

// 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;

These lines define global objects for the webserver and the websocket, and define the new FreeRTOS tasks called TaskWebServer and TaskWebUpdate.

Just before the void setup() statement we add the new function definitions required in Lesson 4 as below.

// 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

These functions perform the following tasks:

  1. notifyClients sends the update LED status to all connected clients.

2. handleWebSocketMessage gets invoked whenever a message is received from a websocket client. In this case it means a user has pushed the Faster or Slower button on the display. Notice that the code simply adds a new message to the command queue (queCMD) that was created in Lesson 3.

3. onEvent gets invoked whenever a websocket connection is created or ended. In this case we simply write a message to the console.

4. processLED is used to initalize the display before the first websocket message is processed. It sets all three LEDs to the ON position.

5. TaskWebServer is the FreeRTOS task that runs the web server. It processes requests from the webclients to make a connection and listens for incoming websocket messages.

6. TaskWebUpdate is a FreeRTOS task that runs every 1/4 second to check the status of the three LEDs. If any have changed their status an update mesage is sent to all the clients currently attached. Notice that it uses the prime number routine mentioned previously to generate the LED status code.

Next we add some code to the setup routine.

// 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());

Near the beginning of setup we add the code to start the wifi network and print the local IP address — so we know where to point our web browser for a connection.

Now we add the code to start the two new FreeRTOS tasks as shown below. These are added immediately below the previous definitions.

// 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
);

And finally we update the TaskHighWater code to include the two new tasks in the response. Insert these lines after the two lines for the taskCmdExec as shown below.

    
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));

The final code should look like this. You can save it as a new sketch called 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 */
}
}
}

Compile and download this code to your ESP 32 breadboard circuit. In the startup you should see the serial console display output as below.

To connect to your new webserver, open a browser on another device and type in the URL as displayed in the console window (i.e. 192.168.0.173 above). The browser display should look like this and update the LED colors dynamically in sequence with the physical LEDs on the breadboard.

When you hit the Slower and Faster buttons the display sequence will change correspondingly faster or slower. Next, you can open another browser window on another device, and see how the server updates all the displays at the same time and responds to inputs from all devices on a first-in first-out basis. Here is what it looks like in action.

Congratulations — you have now created a bidirectional web interface for your ESP 32 blinking LED board. But more importantly (and hopefully) you have learned how to integrate a webserver interface into your future projects. Your device might include numerous sensors of various kinds that need to be monitored and controlled through a simple web interface. The combination of FreeRTOS task management and integrated webservices make the ESP 32 system ideal for many IOT and home automation projects.

I hope you found this 4 part tutorial to be helpful and enlightening. More importantly, I hope it has encouraged you to utilize the techniques you’ve learned to create new projects on your own. Surely there must be something you can do with all this knowledge besides making a blinking LED display. I would appreciate hearing from you through comments and/or claps.

--

--

Tom Wilson

I am a retired engineer living the Pura Vida lifestyle in a beachfront community along the Pacific coast of Costa Rica.