This hands-on tutorial teaches you how to craft and deploy a sleek web interface directly onto a real-world microcontroller-powered embedded device. The tutorial includes code examples that enable hardware control using a web interface. We will use an easily obtainable low-cost microcontroller. You can still follow this tutorial if you are not keen on using the specific microcontroller used since the examples can be run on, for example, Windows and Linux using our Mako Server. However, you cannot use the hardware specifics when running on Windows or Linux. The ESP32 microcontroller used in this tutorial and the ESP32-specific hardware interaction performed in this tutorial enables hardware control without having to work with or compile C code. Instead, we will use a web-based code editor that allows programming in the high-level Lua language. See the end of this tutorial for how to use the Mako Server if you are not keen on investing a few dollars in purchasing an ESP32.
Do you know the difference between traditional server-generated web apps and single-page applications (SPAs)? Traditional web apps reload the entire page with each interaction, while SPAs dynamically update content without full page reloads. Keep reading to learn more.
There's no fundamental difference between a regular web interface and an embedded web interface. Both are built using standard web technologies like HTML, CSS, and JavaScript. However, the key distinction lies in their application and user interaction.
An embedded web interface is typically designed to function more like a standalone application rather than a traditional website. Instead of providing information or facilitating e-commerce, these interfaces serve as control panels for hardware devices. They allow users to monitor, control, and supervise the operations of a specific device, be it an IoT sensor, a home automation system, or an industrial machine. See the tutorials What is an Embedded Application Server and How Lua Speeds Up Embedded Web Application Development for a more in-depth explanation.
We can break embedded web development into two main categories:
In a traditional server-side web interface, each user interaction triggers a request to the server, which then processes the request and sends back a new HTML page to the browser.
Benefits for Embedded Devices:
Drawbacks for Embedded Devices:
In contrast, a Single Page Application (SPA) loads a single HTML page and dynamically updates content as the user interacts with the app. Leveraging popular frameworks like React or Vue.js, SPAs load all essential code (HTML, CSS, and JavaScript) just once, thereby minimizing the need for repetitive server requests.
Benefits for Embedded Devices:
Drawbacks for Embedded Devices:
Given that this tutorial is tailored for embedded developers, we'll zero in on crafting a traditional server-side generated web interface. Why? The learning curve is significantly gentler. It's worth noting that many companies opt to outsource client-side web development - often built on frameworks like React - due to the steep learning curve that embedded developers might encounter. See the tutorial Your First Single Page Application if you prefer to use a SPA.
Let's talk about the elephant in the room: Cascading Style Sheets (CSS). Many embedded developers are in a tangle when trying to learn and perfect their CSS. We have employed the Pure.css framework in this tutorial to make this smoother. This ready-to-use CSS framework simplifies CSS layout design for developers unfamiliar with CSS intricacies.
In this hands-on tutorial, we'll use a ready-to-go, customizable web-based dashboard application. Our goal is to modify this dashboard to control a servo in real-time. Check out the tutorial How to Build an Interactive Dashboard App for a deeper understanding of how this dashboard application works. That tutorial explains how to set up and run the dashboard on your host computer using the Mako Server.
If you're ready to dive into the hands-on part of this tutorial and use an actual ESP32 microcontroller, you’ll first need to install Xedge32 on your ESP32 microcontroller. For step-by-step instructions on setting up and using the Xedge32 IDE, be sure to refer to our Getting Started with Xedge32 tutorial.
Upon navigating to the ESP32's root URL, http://xedge32.local/, you should no longer encounter Xedge's default "Whoops! You forgot to design this page!" 404 error page. Instead, you'll be greeted by the Light Dashboard App, as depicted in Figure 1.
Figure 1: The light dashboard template app, a ready-to-use web framework.
The Light Dashboard Template app is a pre-configured device management Dashboard app optimized for resource-constrained environments such as microcontrollers. The Dashboard app serves as a foundation for crafting professional device management applications as it is designed for easy customization. You can add or remove pages, which are automatically reflected in the left-side menu. While it is not essential to understand the inner workings of the Dashboard app, we suggest reviewing the tutorial How to Build an Interactive Dashboard App and the accompanying GitHub readme file. With its more basic server-generated navigation menu, the tutorial Dynamic Navigation Menu can provide clarity if the server-side Lua code mechanism in the Dashboard app appears complex. See the online interactive LSP tutorial for a complete noobs guide. Note that the Dashboard app is not exclusive to Xedge32; it is compatible with any system powered by the Barracuda App Server, including the Mako Server. For initial web development, you might find starting with the Mako Server easier before moving on to an embedded device to incorporate the ESP32 Lua South Bridge API for direct device resource management such as General Purpose Input/Output (GPIO) management.
Incorporating a new page with a toggle button for LED control can be rewarding. Begin by connecting an LED to your ESP32, following the LED wiring instructions in the tutorial Getting Started with Lua for Device Management. Once your hardware setup is in place, proceed with the following steps:
Figure 2: screenshots of the steps required for adding a new file to the Dashboard app
After adding the new page led-control.html to the Dashboard app and modifying menu.json, it's important to know that the Dashboard app doesn't automatically refresh to include these changes. To make the new "LED Control" section visible in the Dashboard app's left pane menu, a restart through the Xedge IDE is required (step 10 above).
For a more efficient workflow, open two separate browser windows or tabs: one for the Xedge IDE at http://xedge32.local/rtl/ and another for the Dashboard app at http://xedge32.local/. This setup allows for easy toggling between the two whenever you update led-control.html.
There's no need to restart the Dashboard app after adjustments to led-control.html - any edits are instantaneously accessible. For instance, if you insert the word "Hello" into led-control.html within the Xedge IDE and then save it, simply refreshing the page in the other browser window or tab at http://xedge32.local/ will display your updates. As an initial test, input the word "Hello" into the led-control.html file within the Xedge IDE and save it. Then, refresh the Dashboard app window or tab at http://xedge32.local/led-control.html. You should now see the word "Hello".
Now, remove all content in led-control.html and insert the following LSP code using the Xedge IDE:
1 2 3 4 5 6 7 8 9 10 | <?lsp if "POST" == request:method() then local ledState = request:data "ledstate" and true or false trace( "Led state:" ,ledState) end ?> <form method= "post" > Light On/Off: <input name= "ledstate" type= "checkbox" > <input type= "submit" > </form> |
Example 1: LSP code handles POST requests to log the LED state set by the user when clicking the submit button
The LSP code first checks if the incoming request is a POST request. If it is, the script retrieves the data for the 'led' field from the request. If the 'led' checkbox is checked when the form is submitted, request:data("led") will return a non-nil value; otherwise, it will return nil; consequently ledState will be set to true or false. This state is then printed to the trace log for debugging purposes. The Xedge IDE displays the trace log in the bottom pane. We will later add the actual code for turning the LED on and off.
Insert the LSP code from Example 1 into led-control.html as an initial test, but note that there's a caveat here. The current form does not save the state of the checkbox after you click submit. In other words, if you check the box and submit the form, the box will revert to its unchecked state upon reload, even if the LED is actually on. To fully reflect the LED's actual state, additional LSP code, as shown in Example 3, will be necessary to make the checkbox retain its state after form submission.
The next step involves integrating the LED GPIO API. While it's technically possible to directly interact with Xedge32's GPIO API from the LSP page, doing so is not recommended. The reason is that when you allocate a GPIO pin for a specific function, like controlling a LED, it's crucial to maintain the GPIO object for the entire lifespan of the application - in this case, the Dashboard app.
LSP pages operate in what's known as an "ephemeral environment," which exists only for the duration of a client request and the server's subsequent response. Therefore, directly interfacing with the GPIO API from an LSP page would not be suitable for long-term object maintenance.
To address this, we should design an API that preserves the GPIO object instance for as long as the Dashboard app runs. This can be achieved by injecting the necessary code into the Dashboard app's .preload script.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | -- Initialize GPIO pin for LED control -- Pin number is 9, and it's set as an output pin local ledPin = esp32.gpio(9, "OUT" ) -- Variable to keep track of the current state of the LED pin local pinState = false -- Function, available via the app table, to get or set the state of the LED -- state: Boolean value to set the LED state; if nil, returns current state function ledState(state) if nil ~= state then -- If state is provided (not nil) ledPin:value( not state) -- Set the LED pin to the given state pinState = state -- Update the pinState variable end return pinState -- Return the current state of the LED pin end ledState(false) -- Set off -- Function to run when the Xedge application is terminated -- This function closes the GPIO pin and logs the termination function onunload() ledPin:close() -- Close the GPIO pin; release the HW resource trace( "Stopping dashboard" ) -- Log the termination end -- Log that the dashboard application has started trace( "Starting dashboard" ) |
Example 2: GPIO API, available via the 'app' table, suitable for use in an LSP page.
Append the Lua code provided in Example 2 to the end of the .preload file using the Xedge IDE without modifying any existing content. Figure 2 shows the .preload file in the left pane. Once the code has been added, restart the Dashboard app as outlined earlier. Upon successful restart, you should observe the "Starting dashboard" message displayed in the console.
We can finalize the code in Example 1 to control the LED via the browser. While it's possible to toggle the LED state by adding 'app.ledState(ledState)', this alone will not preserve the checkbox state. The refined LSP code below addresses this by retaining the checkbox state between submissions and includes HTML elements that align with the Pure.css framework we use. Functions declared in the .preload file are accessible in LSP pages via the 'app' table.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | <?lsp if "POST" == request:method() then local ledState = request:data "ledstate" and true or false trace( "Led state:" ,ledState) app.ledState(ledState) end ?> <div class= "header" > <h1>LED Control</h1> </div> <div class= "content" > <form method= "post" class= "pure-form" > <fieldset> <div class= "pure-control-group" > <label for= "ledstate" >LED On/Off: </label> <input type= "checkbox" id= "ledstate2" name= "ledstate" <?lsp= app.ledState() and "checked" or "" ?>/> </div> <div class= "pure-control-group" > <button type= "submit" class= "pure-button pure-button-primary" >Set LED</button> </div> </fieldset> </form> </div> |
Example 3: a refined version of Example one, which maintains checkbox state and calls our ledState() function in the .preload script.
After inserting Example 3 into led-control.html and saving the file, you should now be able to control the LED by checking/unchecking the checkbox and clicking submit.
Using JavaScript to automatically submit the form upon a checkbox change provides a more seamless and efficient user experience. It eliminates the two-step process of first checking the box and then clicking a separate "Submit" button, thereby reducing the number of interactions required from the user. This not only speeds up the interaction but also minimizes the cognitive load, making the application feel more responsive and intuitive.
Here's how you can implement this streamlined approach using JavaScript:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | <?lsp if "POST" == request:method() then local ledState = request:data( "ledstate" ) and true or false trace( "Led state:" , ledState) app.ledState(ledState) end ?> <script> function submitForm() { document.getElementById( "ledForm" ).submit(); } </script> <div class= "header" > <h1>LED Control</h1> </div> <div class= "content" > <form id= "ledForm" method= "post" class= "pure-form" > <fieldset> <div class= "pure-control-group" > <label for= "ledstate" >LED On/Off: </label> <input type= "checkbox" id= "ledstate" name= "ledstate" <?lsp=app.ledState() and "checked" or "" ?> onchange= "submitForm()" /> </div> </fieldset> </form> </div> |
Example 4: eliminating the two-step procedure required by Example 3.
After inserting Example 4 into led-control.html (replacing Example 3) and saving the changes, you enable LED control directly through the checkbox. There's no need for a separate submit button or additional clicks. Note that you do not need to restart the Dashboard app. Simply modify the content in led-control.html, save the file, and refresh the browser page to see the changes take effect.
The URL http://xedge32.local/WebSockets.html provides an SMQ-powered WebSocket example that illustrates real-time event communication from your browser to the target device. In this example, we leverage this capability to manipulate a servo in real-time. To follow along, you'll need a servo, and you should wire it according to Figure 3.
Figure 3: Servo Wiring
Note that when using the XIAO-ESP32-S3 as depitcted above, the GPIO pin used is pin-6
For this upgrade, you'll once again need to open the .preload file using the Xedge IDE. Inside this file, look for the existing slider function. Here's how it initially appears:
1 2 3 4 | local function slider(d) angle=d.angle trace( "Slider angle" ,angle) end |
Example 5: The original 'slider' function
Replace the original slider function with the updated version, including its associated servo control logic, as shown below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | local gpioPin=14 -- The GPIO servo control pin used. It must be changed to the pin you use. local bits=13 -- pwm resolution = 13 bits local maxPwm=2^bits - 1 -- any value between 0 and maxPwm local minPulseWidth = 1000 -- 1 ms local cycleTime = 20000 -- 20 ms local function calculatePwmDutyCycle(angle) return (maxPwm / cycleTime) * (angle * 1000 / 180 + minPulseWidth) end local ok,err=esp32.pwmtimer{ mode= "LOW" , -- speed_mode bits=bits, -- duty_resolution (bits) timer=0, -- timer_num freq=50, } assert (ok,err) local duty = 2000 / 20000 * 100 pwm,err=esp32.pwmchannel{ mode= "LOW" , channel=1, timer=0, -- timer_sel gpio=gpioPin, duty = calculatePwmDutyCycle(180), hpoint=0, } assert (pwm,err) local function slider(d) angle=d.angle trace( "Slider angle" ,angle) pwm:duty(calculatePwmDutyCycle(angle)) end |
Example 6: The modified 'slider' function with associated servo control logic
Since we are modifying the .preload file, the Dashboard app must be restarted, as previously explained, after saving the file. You should now be able to control the servo by adjusting the browser-based slider. Open two separate browser windows to the URL http://xedge32.local/WebSockets.html, and you will see real-time changes in both windows when the slider is moved in one of them. To further understand the advantages of utilizing the SMQ protocol over raw WebSockets, refer to our tutorial Modern Approach to Device Management.
Example 6's slider function and servo control code have been adapted from our GitHub example file servo.lsp. For a more in-depth explanation of the servo control code mechanics, please see the comments at the top of that Lua source file. You should also consult the Lua PWM API documentation.
In this tutorial, we have navigated the complexities of developing a web-based interface for microcontroller-powered embedded devices, focusing on Xedge32's easy GPIO control benefits. From the creation of customizable dashboards to the real-time manipulation of hardware like LEDs and servos, Xedge32 has proven to be an invaluable tool.
In summary, Xedge32 offers an accessible yet robust framework for anyone looking to tap into the world of embedded systems programming. It simplifies complex processes and provides an easy platform to create, customize, and control your projects. You also have the flexibility to utilize Mako Server in conjunction with Xedge32 for development purposes. Mako Server provides an environment for running Lua Server Pages (LSP) applications, making it a suitable choice for developing and testing your code before deploying it onto a microcontroller. Whether you're a seasoned developer or new to the world of the Internet of Things (IoT), the tools and methods discussed here offer a flexible framework for developing and controlling embedded systems.
Instructions for running the Dashboard app with the Mako Server can be found on the Dashboard app's GitHub page.
This tutorial was just the beginning. To truly master Xedge32, check out our full range of Xedge32 tutorials and take your skills to the next level. To learn more about building robust IoT and Embedded Web Server solutions, check out our list of embedded web server tutorials.
Navigating the world of embedded web servers and IoT can be daunting. Our consulting services are here to provide instant expertise. But our tutorials are ready if you're looking for a self-paced journey. Every challenge, every ambition, we've got your back.
Expedite your IoT and edge computing development with the "Barracuda App Server Network Library", a compact client/server multi-protocol stack and IoT toolkit with an efficient integrated scripting engine. Includes Industrial Protocols, MQTT client, SMQ broker, WebSocket client & server, REST, AJAX, XML, and more. The Barracuda App Server is a programmable, secure, and intelligent IoT toolkit that fits a wide range of hardware options.
SharkSSL is the smallest, fastest, and best performing embedded TLS stack with optimized ciphers made by Real Time Logic. SharkSSL includes many secure IoT protocols.
SMQ lets developers quickly and inexpensively deliver world-class management functionality for their products. SMQ is an enterprise ready IoT protocol that enables easier control and management of products on a massive scale.
SharkMQTT is a super small secure MQTT client with integrated TLS stack. SharkMQTT easily fits in tiny microcontrollers.
An easy to use OPC UA stack that enables bridging of OPC-UA enabled industrial products with cloud services, IT, and HTML5 user interfaces.
Use our user programmable Edge-Controller as a tool to accelerate development of the next generation industrial edge products and to facilitate rapid IoT and IIoT development.
Learn how to use the Barracuda App Server as your On-Premises IoT Foundation.
The compact Web Server C library is included in the Barracuda App Server protocol suite but can also be used standalone.
The tiny Minnow Server enables modern web server user interfaces to be used as the graphical front end for tiny microcontrollers. Make sure to check out the reference design and the Minnow Server design guide.
Why use FTP when you can use your device as a secure network drive.
PikeHTTP is a compact and secure HTTP client C library that greatly simplifies the design of HTTP/REST style apps in C or C++.
The embedded WebSocket C library lets developers design tiny and secure IoT applications based on the WebSocket protocol.
Send alarms and other notifications from any microcontroller powered product.
The RayCrypto engine is an extremely small and fast embedded crypto library designed specifically for embedded resource-constrained devices.
Real Time Logic's SharkTrust™ service is an automatic Public Key Infrastructure (PKI) solution for products containing an Embedded Web Server.
The Modbus client enables bridging of Modbus enabled industrial products with modern IoT devices and HTML5 powered HMIs.