In this guide you’ll learn how to create a web server with the ESP32 that displays a web page to control a stepper motor. The web page allows you to insert the number of steps and select clockwise or counterclockwise direction. Additionally, it also shows whether the motor is currently spinning or if it is stopped. The communication between the client and the server is achieved via WebSocket protocol. All clients are updated with the current motor state.

This tutorial is the second part of this article ESP32 Web Server: Control Stepper Motor (HTML Form), but it can also be followed as a standalone tutorial.
To better understand how this project works, you can take a look at the following tutorials:
Before proceeding with the tutorial, make sure you check the following prerequisites.
To follow this tutorial, you need the following parts:
You can use the preceding links or go directly to MakerAdvisor.com/tools to find all the parts for your projects at the best price!
We’ll program the ESP32 using Arduino IDE. So, you must have the ESP32 add-on installed. Follow the next tutorial if you haven’t already:
If you want to use VS Code with the PlatformIO extension, follow the next tutorial instead to learn how to program the ESP32:
To upload the HTML, CSS, and JavaScript files needed to build this project to the ESP32 filesystem (LittleFS), we’ll use a plugin for Arduino IDE: LittleFS Filesystem uploader. Follow the next tutorial to install the filesystem uploader plugin if you haven’t already:
If you’re using VS Code with the PlatformIO extension, read the following tutorial to learn how to upload files to the filesystem:
To build this project, you need to install the following libraries:
You can install these libraries in the Arduino Library Manager. Open the Library Manager by clicking the Library icon at the left sidebar.
Search for ESPAsyncWebServer and install the ESPAsyncWebServer by ESP32Async.

Then, install the AsyncTCP library. Search for AsyncTCP and install the AsyncTCP by ESP32Async.

The following schematic diagram shows the connections between the stepper motor and the ESP32.

Note: you should power the ULN2003APG motor driver using an external 5V power supply.
| Motor Driver | ESP32 |
| IN1 | GPIO 19 |
| IN2 | GPIO 18 |
| IN3 | GPIO 5 |
| IN4 | GPIO 17 |
The following video shows a quick demonstration of what you’ll achieve by the end of this tutorial.
The following image shows the web page you’ll build for this project.


steps&directionSo, if you submit 2000 steps and clockwise direction, it will send the following message:
2000&CW
The files you want to upload to the ESP filesystem should be placed in a folder called data under the project folder. We’ll move three files to that folder:

You should save the HTML, CSS, and JavaScript files inside a folder called data inside the Arduino sketch folder, as shown in the previous diagram. We’ll upload those files to the ESP32 filesystem (LittleFS).
You can download all project files:
Create a file called index.html with the following content:
<!DOCTYPE html> <html> <head> <title>Stepper Motor</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" type="text/css" href="style.css"> <link rel="icon" type="image/png" href="favicon.png"> <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous"> </head> <body> <div class="topnav"> <h1>Stepper Motor Control <i class="fas fa-cogs"></i></h1> </div> <div class="content"> <form> <input type="radio" id="CW" name="direction" value="CW" checked> <label for="CW">Clockwise</label> <input type="radio" id="CCW" name="direction" value="CCW"> <label for="CW">Counterclockwise</label><br><br><br> <label for="steps">Number of steps:</label> <input type="number" id="steps" name="steps"> </form> <button onclick="submitForm()">GO!</button> <p>Motor state: <span id="motor-state">Stopped</span></p> <p><i id="gear" class="fas fa-cog"></i> </p> </div> </body> <script src="script.js"></script> </html>
This HTML file is very similar to the one used in the previous tutorial. You can click here for a complete explanation of the HTML file.
We’ve added ids to the HTML elements we want to manipulate using JavaScript—the radio buttons and the input field:
<input type="radio" id="CW" name="direction" value="CW" checked> <label for="CW">Clockwise</label> <input type="radio" id="CCW" name="direction" value="CCW"> <label for="CW">Counterclockwise</label><br><br><br> <label for="steps">Number of steps:</label> <input type="number" id="steps" name="steps">
We want to send the form results to the server (ESP32) via WebSocket protocol. So, we’ve added a button, that when clicked (onclick event) calls the submitForm() user-defined javascript function that sends the results to the server as you’ll see later in the JavaScript section.
<button onclick="submitForm()">GO!</button>
Additionally, we also added a paragraph to display the motor state. We’ve added a <span> tag with the motor-state id so that we’re able to manipulate the text between the <span> tags using Javascript.
<p>Motor state: <span id="motor-state">Stopped</span></p>
Finally, there’s a paragraph displaying a gear with the id=”gear”. We need this id to make the gear move.
<p><i id="gear" class="fas fa-cog"></i> </p>
Don’t forget that you need to reference the JavaScript file (scrip.js) in the HTML file as follows:
<script src="script.js"></script>
Create a file called style.css with the following content:
html { font-family: Arial, Helvetica, sans-serif; } h1 { font-size: 1.8rem; color: white; } p{ font-size: 20px; text-align: center; } .topnav { overflow: hidden; background-color: #0A1128; text-align: center; } body { margin: 0; } .content { padding: 20px; max-width: max-content; margin: 0 auto; } input[type=number], select { width: 100%; padding: 12px 20px; margin: 8px 0; display: inline-block; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; } form{ border-radius: 5px; background-color: #f2f2f2; padding: 20px; } button { background-color: #034078; border: none; padding: 14px 20px; text-align: center; font-size: 20px; border-radius: 4px; transition-duration: 0.4s; width: 100%; color: white; cursor: pointer; } button:hover { background-color: #1282A2; } input[type="radio"] { -webkit-appearance: none; -moz-appearance: none; appearance: none; border-radius: 50%; width: 16px; height: 16px; border: 2px solid #999; transition: 0.2s all linear; margin-right: 5px; position: relative; top: 4px; } input[type="radio"]:checked{ border: 6px solid #1282A2; } #motor-state{ font-weight: bold; color: red; } #gear{ font-size:100px; color:#2d3031cb; } .spin { -webkit-animation:spin 4s linear infinite; -moz-animation:spin 4s linear infinite; animation:spin 4s linear infinite; } .spin-back { -webkit-animation:spin-back 4s linear infinite; -moz-animation:spin-back 4s linear infinite; animation:spin-back 4s linear infinite; } @-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } } @-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } } @keyframes spin { 100% { -webkit-transform: rotate(360deg); transform:rotate(360deg); } } @-moz-keyframes spin-back { 100% { -moz-transform: rotate(-360deg); } } @-webkit-keyframes spin-back { 100% { -webkit-transform: rotate(-360deg); } } @keyframes spin-back { 100% { -webkit-transform: rotate(-360deg); transform:rotate(-360deg); } }
We already covered how the CSS for the HTML form works. You can click here for a detailed explanation. Let’s take a look at the relevant parts for this tutorial.
We format the motor state text font-weight (bold) and color (red). To refer to a specific id in CSS, use # followed by the id (#motor-state).
#motor-state{ font-weight: bold; color: red; }
The following line formats the gear icon color and size—remember that its id is gear, so we refer to it with #gear:
#gear{ font-size:100px; color:#2d3031cb; }
Then, we format two classes spin and spin-back that are not attributed to any HTML element yet. We’ll attribute the spin and spin-back classes to the gear using JavaScript when the motor starts moving.
These classes use the animation property to rotate the gear. To learn more about how the animation property works, we recommend taking a look at this quick tutorial.
.spin { -webkit-animation:spin 4s linear infinite; -moz-animation:spin 4s linear infinite; animation:spin 4s linear infinite; } .spin-back { -webkit-animation:spin-back 4s linear infinite; -moz-animation:spin-back 4s linear infinite; animation:spin-back 4s linear infinite; } @-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } } @-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } } @keyframes spin { 100% { -webkit-transform: rotate(360deg); transform:rotate(360deg); } } @-moz-keyframes spin-back { 100% { -moz-transform: rotate(-360deg); } } @-webkit-keyframes spin-back { 100% { -webkit-transform: rotate(-360deg); } } @keyframes spin-back { 100% { -webkit-transform: rotate(-360deg); transform:rotate(-360deg); } }
Create a file called script.js with the following content:
var gateway = `ws://${window.location.hostname}/ws`; var websocket; window.addEventListener('load', onload); var direction; function onload(event) { initWebSocket(); } function initWebSocket() { console.log('Trying to open a WebSocket connection…'); websocket = new WebSocket(gateway); websocket.onopen = onOpen; websocket.onclose = onClose; websocket.onmessage = onMessage; } function onOpen(event) { console.log('Connection opened'); } function onClose(event) { console.log('Connection closed'); setTimeout(initWebSocket, 2000); } function submitForm(){ const rbs = document.querySelectorAll('input[name="direction"]'); direction; for (const rb of rbs) { if (rb.checked) { direction = rb.value; break; } } document.getElementById("motor-state").innerHTML = "motor spinning..."; document.getElementById("motor-state").style.color = "blue"; if (direction=="CW"){ document.getElementById("gear").classList.add("spin"); } else{ document.getElementById("gear").classList.add("spin-back"); } var steps = document.getElementById("steps").value; websocket.send(steps+"&"+direction); } function onMessage(event) { console.log(event.data); direction = event.data; if (direction=="stop"){ document.getElementById("motor-state").innerHTML = "motor stopped" document.getElementById("motor-state").style.color = "red"; document.getElementById("gear").classList.remove("spin", "spin-back"); } else if(direction=="CW" || direction=="CCW"){ document.getElementById("motor-state").innerHTML = "motor spinning..."; document.getElementById("motor-state").style.color = "blue"; if (direction=="CW"){ document.getElementById("gear").classList.add("spin"); } else{ document.getElementById("gear").classList.add("spin-back"); } } }
Let’s see how the JavaScript for this project works.
The gateway is the entry point to the WebSocket interface. window.location.hostname gets the current page address (the web server IP address)
var gateway = `ws://${window.location.hostname}/ws`;
Create a new global variable called websocket.
var websocket;
Create another global variable called direction that will hold the motor’s current direction: clockwise, counterclowise or stopped.
var direction;
Add an event listener that will call the onload function when the web page loads.
window.addEventListener('load', onload);
The onload() function calls the initWebSocket() function to initialize a WebSocket connection with the server.
function onload(event) { initWebSocket(); }
The initWebSocket() function initializes a WebSocket connection on the gateway defined earlier. We also assign several callback functions that will be triggered when the WebSocket connection is opened, closed or when a message is received.
function initWebSocket() { console.log('Trying to open a WebSocket connection…'); websocket = new WebSocket(gateway); websocket.onopen = onOpen; websocket.onclose = onClose; websocket.onmessage = onMessage; }
When the connection is opened, print a message in the console for debugging purposes.
function onOpen(event) { console.log('Connection opened'); }
If for some reason the web socket connection is closed, call the initWebSocket() function again after 2000 milliseconds (2 seconds).
function onClose(event) { console.log('Connection closed'); setTimeout(initWebSocket, 2000); }
Finally, we need to handle what happens when the form is submitted and when the client receives a new message (onMessage event).
When the form is submitted, the submitForm() function is called:
function submitForm(){
We start by getting which radio button is selected. We save the value of the selected radio button in the direction variable.
const rbs = document.querySelectorAll('input[name="direction"]'); var direction; for (const rb of rbs) { if (rb.checked) { direction = rb.value; break; } }
Then, we change the motor state text to motor spinning… and its color to blue. We refer to that HTML element by its id motor-state.
document.getElementById("motor-state").innerHTML = "motor spinning..."; document.getElementById("motor-state").style.color = "blue";
Then, we check whether we’ve selected clockwise or counterclockwise direction to spin the gear in the right direction. To do that, we add the class spin or spin-back to the element with the gear id.
if (direction=="CW"){ document.getElementById("gear").classList.add("spin"); } else{ document.getElementById("gear").classList.add("spin-back"); }
We get the number of steps inserted and save it in the steps variable.
var steps = document.getElementById("steps").value;
Then, we finally send a message via WebSocket protocol to the server (ESP32) with the number of steps and direction separated by a &.
websocket.send(steps+"&"+direction);
The server (your ESP board) will send a message when it is time to change the motor state. When that happens, we save the message in the direction variable.
We check the content of the message and change the motor state and gear animation accordingly.
function onMessage(event) { console.log(event.data); direction = event.data; if (direction=="stop"){ document.getElementById("motor-state").innerHTML = "motor stopped" document.getElementById("motor-state").style.color = "red"; document.getElementById("gear").classList.remove("spin", "spin-back"); } else if(direction=="CW" || direction=="CCW"){ document.getElementById("motor-state").innerHTML = "motor spinning..."; document.getElementById("motor-state").style.color = "blue"; if (direction=="CW"){ document.getElementById("gear").classList.add("spin"); } else{ document.getElementById("gear").classList.add("spin-back"); } } }
Before uploading, you can use the following link to:
Copy the following code to the Arduino IDE. Insert your network credentials and it will work straight away.
/* Rui Santos & Sara Santos - Random Nerd Tutorials Complete project details at https://RandomNerdTutorials.com/stepper-motor-esp32-websocket/ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. */ #include <Arduino.h> #include <WiFi.h> #include <AsyncTCP.h> #include <ESPAsyncWebServer.h> #include "LittleFS.h" #include <Stepper.h> const int stepsPerRevolution = 2048; // change this to fit the number of steps per revolution #define IN1 19 #define IN2 18 #define IN3 5 #define IN4 17 Stepper myStepper(stepsPerRevolution, IN1, IN3, IN2, IN4); String message = ""; // Replace with your network credentials const char* ssid = "REPLACE_WITH_YOUR_SSID"; const char* password = "REPLACE_WITH_YOUR_PASSWORD"; // Create AsyncWebServer object on port 80 AsyncWebServer server(80); // Create a WebSocket object AsyncWebSocket ws("/ws"); //Variables to save values from HTML form String direction ="STOP"; String steps; bool newRequest = false; // Initialize LittleFS void initLittleFS() { if (!LittleFS.begin(true)) { Serial.println("An error has occurred while mounting LittleFS"); } else{ Serial.println("LittleFS mounted successfully"); } } // Initialize WiFi void initWiFi() { WiFi.mode(WIFI_STA); WiFi.begin(ssid, password); Serial.print("Connecting to WiFi .."); while (WiFi.status() != WL_CONNECTED) { Serial.print('.'); delay(1000); } Serial.println(WiFi.localIP()); } void notifyClients(String state) { ws.textAll(state); } void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) { AwsFrameInfo *info = (AwsFrameInfo*)arg; if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) { data[len] = 0; message = (char*)data; steps = message.substring(0, message.indexOf("&")); direction = message.substring(message.indexOf("&")+1, message.length()); Serial.print("steps"); Serial.println(steps); Serial.print("direction"); Serial.println(direction); notifyClients(direction); newRequest = true; } } 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()); //Notify client of motor current state when it first connects notifyClients(direction); break; case WS_EVT_DISCONNECT: Serial.printf("WebSocket client #%u disconnected\n", client->id()); break; case WS_EVT_DATA: handleWebSocketMessage(arg, data, len); break; case WS_EVT_PONG: case WS_EVT_ERROR: break; } } void initWebSocket() { ws.onEvent(onEvent); server.addHandler(&ws); } void setup() { // Serial port for debugging purposes Serial.begin(115200); initWiFi(); initWebSocket(); initLittleFS(); myStepper.setSpeed(5); // Web Server Root URL server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(LittleFS, "/index.html", "text/html"); }); server.serveStatic("/", LittleFS, "/"); server.begin(); } void loop() { if (newRequest){ if (direction == "CW"){ myStepper.step(steps.toInt()); Serial.print("CW"); } else{ myStepper.step(-steps.toInt()); } newRequest = false; notifyClients("stop"); } ws.cleanupClients(); }
The Arduino sketch is very similar to the previous tutorial, but it handles the client-server communication using WebSocket protocol. Let’s see how it works or skip to the demonstration section.
First, include the required libraries. The WiFi, AsyncTCP, and ESPAsyncWebServer to create the web server, the LittleFS library to use the ESP32 filesystem, and the Stepper library to control the stepper motor.
#include <Arduino.h> #include <WiFi.h> #include <AsyncTCP.h> #include <ESPAsyncWebServer.h> #include "LittleFS.h" #include <Stepper.h>
Define the steps per revolution of your stepper motor—in our case, it’s 2048:
const int stepsPerRevolution = 2048; // change this to fit the number of steps per revolution
Define the motor input pins. In this example, we’re connecting to GPIOs 19, 18, 5, and 17, but you can use any other suitable GPIOs.
#define IN1 19 #define IN2 18 #define IN3 5 #define IN4 17
Initialize an instance of the stepper library called myStepper. Pass as arguments the steps per revolution and the input pins. In the case of the 28BYJ-48 stepper motor, the order of the pins is IN1, IN3, IN2, IN4—it might be different for your motor.
Stepper myStepper(stepsPerRevolution, IN1, IN3, IN2, IN4);
Insert your network credentials in the following lines.
// Replace with your network credentials const char* ssid = "REPLACE_WITH_YOUR_SSID"; const char* password = "REPLACE_WITH_YOUR_PASSWORD";
Create an AsyncWebServer object called server on port 80.
AsyncWebServer server(80);
The ESPAsyncWebServer library includes a WebSocket plugin that makes it easy to handle WebSocket connections. Create an AsyncWebSocket object called ws to handle the connections on the /ws path.
AsyncWebSocket ws("/ws");
The following variables will save the direction and number of steps received via WebSocket protocol. When the program first starts, the motor is stopped.
String direction ="stop"; String steps;
The newRequest variable will be used to check whether a new request occurred. Then, in the loop(), we’ll spin the motor when a new request is received—when the newRequest variable is true.
bool newRequest = false;
The initLittleFS() function initializes the ESP32 Filesystem.
void initLittleFS() { if (!LittleFS.begin(true)) { Serial.println("An error has occurred while mounting LittleFS"); } else{ Serial.println("LittleFS mounted successfully"); } }
The initWiFi() function initializes WiFi.
// Initialize WiFi void initWiFi() { WiFi.mode(WIFI_STA); WiFi.begin(ssid, password); Serial.print("Connecting to WiFi .."); while (WiFi.status() != WL_CONNECTED) { Serial.print('.'); delay(1000); } Serial.println(WiFi.localIP()); }
Previously, you’ve seen how to handle the WebSocket connection on the client side (browser). Now, let’s take a look on how to handle it on the server side.
The notifyClients() function notifies all clients with a message containing whatever you pass as a argument. In this case, we’ll want to notify all clients of the current motor state whenever there’s a change.
void notifyClients(String state) { ws.textAll(state); }
The AsyncWebSocket class provides a textAll() method for sending the same message to all clients that are connected to the server at the same time.
The handleWebSocketMessage() function is a callback function that will run whenever we receive new data from the clients via WebSocket protocol.
void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) { AwsFrameInfo *info = (AwsFrameInfo*)arg; if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) { data[len] = 0; message = (char*)data; steps = message.substring(0, message.indexOf("&")); direction = message.substring(message.indexOf("&")+1, message.length()); Serial.print("steps"); Serial.println(steps); Serial.print("direction"); Serial.println(direction); notifyClients(direction); newRequest = true; } }
We split the message to get the number of steps and direction.
message = (char*)data; steps = message.substring(0, message.indexOf("&")); direction = message.substring(message.indexOf("&")+1, message.length());
Then, we notify all clients of the motor direction so that all clients change the motor state on the web interface.
notifyClients(direction);
Finally, set the newRequest variable to true, so that the motors starts spinning in the loop().
newRequest = true;
Now we need to configure an event listener to handle the different asynchronous steps of the WebSocket protocol. This event handler can be implemented by defining the onEvent() as follows:
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()); //Notify client of motor current state when it first connects notifyClients(direction); 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; } }
The type argument represents the event that occurs. It can take the following values:
There’s a section to notify any client of the current motor state when it first connects:
case WS_EVT_CONNECT: Serial.printf("WebSocket client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str()); //Notify client of motor current state when it first connects notifyClients(direction); break;
Finally, the initWebSocket() function initializes the WebSocket protocol.
void initWebSocket() { ws.onEvent(onEvent); server.addHandler(&ws); }
In the setup(), initialize the Serial Monitor.
Serial.begin(115200);
Call the initWiFi() function to initialize WiFi.
initWiFi();
Call the initLittleFS() function to initialize the filesystem.
initWebSocket();
And set the stepper motor speed in rpm.
myStepper.setSpeed(5);
Then, handle the web server. When you receive a request on the root (/) URL—this is when you access the ESP IP address— send the HTML text to build the web page:
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(200, "text/html", index_html); });
When the HTML file loads on your browser, it will make a request for the CSS and JavaScript files. These are static files saved on the same directory (LittleFS). So, we can simply add the following line to serve files in a directory when requested by the root URL. It will serve the CSS and JavaScript files automatically.
server.serveStatic("/", LittleFS, "/");
Finally, start the server.
server.begin();
Let’s take a look at the loop() section.
If the newRequest variable is true, we’ll check what’s the spinning direction: CW or CCW. If it is CW, we move the motor the number of steps saved in the steps variable using the step() method on the myStepper object. To move the motor counterclockwise, we just need to pass the number of steps but with a minus (–) sign.
if (direction == "CW"){ // Spin the stepper clockwise direction myStepper.step(steps.toInt()); } else{ // Spin the stepper counterclockwise direction myStepper.step(-steps.toInt()); }
After spinning the motor, set the newRequest variable to false, so that it can detect new requests again.
newRequest = false;
Additionally, notify all clients that the motor has stopped.
notifyClients("stop");
After inserting your network credentials, save the code. Go to Sketch > Show Sketch Folder, and create a folder called data.

Inside that folder, you should save the HTML, CSS, and JavaScript files.
Then, upload the code to your ESP32 board. Make sure you have the right board and COM port selected. Also, make sure you’ve added your network credentials.
After uploading the code, you need to upload the files.
Press [Ctrl] + [Shift] + [P] on Windows or [⌘] + [Shift] + [P] on MacOS to open the command palette. Search for the Upload LittleFS to Pico/ESP8266/ESP32 command and click on it.
If you don’t have this option is because you didn’t install the filesystem uploader plugin. Check this tutorial.

Important: make sure the Serial Monitor is closed before uploading to the filesystem. Otherwise, the upload will fail.
When everything is successfully uploaded, open the Serial Monitor at a baud rate of 115200. Press the ESP32 EN/RST button, and it should print the ESP32 IP address.
Open a web browser or multiple web browser windows on your local network and you’ll access the web page to control the motor. Submit the form to control the motor.

The gear on the web page starts spinning in the right direction and the motor starts working.

When it stops, the gear on the web page and the motor state change accordingly.

Notice that if you have multiple clients connected, all clients update the motor state almost instantaneously.
Copyright ©2025. All Rights Reserved Emblab THE RAVE INNOVATION