Designing Your First Professional Embedded Web Interface

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 enable 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.

What exactly is an embedded web interface?

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.

Types of Embedded Web Interfaces

We can break embedded web development into two main categories:

Traditional Server-Side Web Interfaces

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:

  • Easier to develop and debug, especially on a platform that supports a server-side programming language such as LSP.
  • Lower client-side computational requirements, especially for mobile devices.

Drawbacks for Embedded Devices:

  • Slightly slower response times due to round-trip server requests and full page rendering on response.
  • Slightly Increased load on the embedded device's processor to handle server-side logic.
  • More complicated to maintain persistent WebSocket connections when navigating to a new page.

Client-Side Web Interfaces (Single Page Applications or SPAs)

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:

  • Faster response times due to reduced server interaction, especially when channeling all communication over SMQ.
  • Offloads computational tasks to the client, reducing device processor load.

Drawbacks for Embedded Devices:

  • Extensive and potentially very time-consuming learning curve, especially for web frameworks such as React.
  • More complex development and debugging process.
  • Higher client-side computational requirements, which may not be suitable when using mobile devices.

The Type of Embedded Web Interface Featured in This Tutorial

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.

The Importance of Templates and CSS

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.

Getting Started

The following instructions are for uploading the ready-to-use web framework we have prepared onto an Xedge32-powered ESP32. If you're unfamiliar with the Xedge32 environment, we recommend starting with our Xedge32 Introduction, which guides you through selecting the appropriate ESP32 hardware and the firmware upload process.

When you have the Xedge32 powered ESP32 running, follow these steps:

  1. Clone the LSP-Examples GitHub repo.
  2. Navigate to the directory using the computer’s file explorer: LSP-Examples/Light-Dashboard/.
  3. Using a browser, navigate to the ESP32 using the URL http://ip-addr/rtl/apps/disk/ to access the Web File Manager.
  4. When you have verified you can navigate to this URL, copy the URL and use the URL to map the device as a WebDAV drive. See the video tutorial Connect a Windows and Mac computer to a WebDAV server if you have not used WebDAV, but note that the URL must be the one you copied and not the URL in the video.
  5. You should now have a WebDAV file explorer window at the root of the ESP32’s file system. Use the other file explorer window from step two above; drag and drop the www directory onto the ESP32’ file explorer window to upload all files (see Figure 1).
  6. Navigate to the URL http://ip-addr/rtl/ to access the Xedge IDE.
  7. Expand disk in the left pane, right click on the www directory, and select 'New App'.
  8. In the "Application Configuration" dialog window click the LSP button to enable LSP, remove ‘www’ from the 'Directory Name' field i.e. leave it blank, click the run button, and then click the save button (see Figure 2).
Web File Manager Drag and Drop

Figure 1: Dropping the www directory onto the ESP32’s root directory

Xedge Application Configuration

Figure 2: Creating and configuring an LSP enabled root application as explained in step 7 and 8 above.

The Light Dashboard Template App

Upon navigating to the ESP32's root URL, http://ip-addr/, 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's login page, as depicted in Figure 3. Log in using the username 'admin' and' qwerty' password.

Light Dashboard Template

Figure 3: The light dashboard template app, a ready-to-use web framework; log in using the username 'admin' and 'qwerty' password.

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 "south bridge" API for direct device resource management, such as General Purpose Input/Output (GPIO) management.

Adding a Page to the Light Dashboard Template App

Incorporating a new page with a toggle button for LED control can be rewarding. Begin by connecting a 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:

  1. Navigate to the Xedge IDE by entering http://ip-addr/rtl/ in the browser.
  2. In the left pane tree view, expand www and then expand .lua
  3. Click on menu.json to open the Dashboard app’s page configuration file.
  4. Add the following JSON to this file (see figure 4): {"name": "LED Control", "href": "led-control.html"},
  5. Save the JSON file.
  6. Using the left pane tree view, navigate to the inner www directory i.e., to www/.lua/www.
  7. Right-click www and select New File.
  8. Enter the name led-control.html and click the Enter button.
  9. Click on the new file led-control.html to open it in the editor, delete all content, and click save.
  10. Restart the Dashboard app by clicking on .appcfg in the left pane; click the running button twice to restart the app.
Xedge: Add New File

Figure 4: 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://ip-addr/rtl/ and another for the Dashboard app at http://ip-addr/. 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://ip-addr/ 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://ip-addr/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:

if "POST" == request:method() then
   local ledState = request:data"ledstate" and true or false
   trace("Led state:",ledState)
<form method="post">
   Light On/Off: <input name="ledstate" type="checkbox">
   <input type="submit">

Example 1: LSP code handles POST requests to log the LED state set by 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.

Adding a LED API suitable for LSP pages

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.

-- Initialize GPIO pin for LED control
-- Pin number is 4, and it's set as an output pin
local ledPin = esp32.gpio(4, "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(state)  -- Set the LED pin to the given state
        pinState = state  -- Update the pinState variable
    return pinState  -- Return the current state of the LED pin

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

-- 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 4 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.

if "POST" == request:method() then
   local ledState = request:data"ledstate" and true or false
   trace("Led state:",ledState)
<div class="header">
  <h1>LED Control</h1>
<div class="content">
  <form method="post" class="pure-form">
      <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 class="pure-control-group">
        <button type="submit" class="pure-button pure-button-primary">Set LED</button>

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.

Simplifying the user LED control interaction

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:

if "POST" == request:method() then
   local ledState = request:data("ledstate") and true or false
   trace("Led state:", ledState)
  function submitForm() {

<div class="header">
  <h1>LED Control</h1>
<div class="content">
  <form id="ledForm" method="post" class="pure-form">
      <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()"/>

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.

Servo Control using the Dashboard App’s Slider Example

The URL http://ip-addr/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 5.

ESP32 Servo Wiring

Figure 5: Servo Wiring

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:

local function slider(d)
   trace("Slider angle",angle)

Example 5: The original 'slider' function

Replace the original slider function with the updated version, including its associated servo control logic, as shown below:

local gpioPin=14 -- The GPIO servo control pin used
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)

local ok,err=esp32.pwmtimer{
   mode="LOW", -- speed_mode
   bits=bits, -- duty_resolution (bits)
   timer=0, -- timer_num
local duty = 2000 / 20000 * 100
   timer=0, -- timer_sel
   duty = calculatePwmDutyCycle(180),

local function slider(d)
   trace("Slider angle",angle)

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://ip-addr/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.

Expand Your Horizons:

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.

Looking for Guidance?

Our consulting services are your first line of defense when complexities arise in networking, security, or device management. And for those keen to take the reins, our extensive tutorials on embedded web servers and IoT are your perfect companion. With Real Time Logic, expertise meets empowerment. Your vision, our mission.


OPC-UA Client & Server

An easy to use OPC UA stack that enables bridging of OPC-UA enabled industrial products with cloud services, IT, and HTML5 user interfaces.

Edge Controller

Edge Controller

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.

On-Premises IoT

On-Premises IoT Platform

Learn how to use the Barracuda App Server as your On-Premises IoT Foundation.

Embedded Web Server

Barracuda Embedded Web Server

The compact Web Server C library is included in the Barracuda App Server protocol suite but can also be used standalone.

WebSocket Server

Microcontroller Friendly

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.

WebDAV Server

Network File System

Why use FTP when you can use your device as a secure network drive.

HTTP Client

Secure HTTP Client Library

PikeHTTP is a compact and secure HTTP client C library that greatly simplifies the design of HTTP/REST style apps in C or C++.

WebSocket Client

Microcontroller Friendly

The embedded WebSocket C library lets developers design tiny and secure IoT applications based on the WebSocket protocol.

SMTP Client

Secure Embedded SMTP Library

Send alarms and other notifications from any microcontroller powered product.

Crypto Library

RayCrypto C Library

The RayCrypto engine is an extremely small and fast embedded crypto library designed specifically for embedded resource-constrained devices.

Embedded PKI Service

Automatic SSL Certificate Management for Devices

Real Time Logic's SharkTrust™ service is an automatic Public Key Infrastructure (PKI) solution for products containing an Embedded Web Server.


Modbus TCP client

The Modbus client enables bridging of Modbus enabled industrial products with modern IoT devices and HTML5 powered HMIs.

Posted in Xedge32