Web UI builder for microcontrollers
Mongoose Web UI Builder is a visual tool for building web interfaces and dashboards for microcontrollers and embedded devices. It generates ready-to-use projects that run directly on embedded hardware, enabling developers to create configuration pages, monitoring dashboards, and device control interfaces accessible from a web browser.
Web UI Builder supports a wide range of microcontrollers as well as Windows, macOS, and Linux, making it easy to develop, test, and iterate quickly.
You can start using Web UI Builder immediately at https://mongoose.ws/wizard/
For a quick introduction, watch the tutorials: Mongoose Web UI Wizard Tutorials
Concept
Web UI Builder is based on a single JSON configuration file that defines your embedded application: device settings, target hardware, web UI, and REST API.
The visual editor provides a convenient way to create and modify this configuration, while advanced users can edit it directly in any code editor.
The backend takes this configuration and generates a complete, ready-to-build project. The generated firmware runs on microcontrollers and includes the web interface, networking, and device control logic. You can build and flash it locally, or directly from the browser using WebUSB (Chrome).
Networking functionality is powered by the Mongoose Library, which provides TCP/IP, HTTP, WebSocket, and MQTT support. The generated code integrates with your firmware through a small “glue” layer (mongoose_glue.{c,h}), allowing you to connect the web UI and APIs to your device hardware.
For example, a Web UI can include a toggle button to control a device output (e.g., an LED). Web UI Builder automatically generates the corresponding firmware interface:
- a structure representing the device state
- getter and setter functions to access and modify that state
Generated in mongoose_glue.h:
struct leds {
bool led1;
};
Generated in mongoose_glue.c:
// Generated default code maps Web UI toggle button to the structure
struct leds s_leds = {false};
void glue_get_leds(struct leds *leds) {
// Insert your code here to sync s_leds to your hardware
*leds = s_leds;
}
void glue_set_leds(struct leds *leds) {
s_leds = *leds;
// Insert your code here to sync s_leds to your hardware
}
Custom API Handlers
Web UI Builder generates default API handlers in mongoose_glue.{c,h}. You can replace them with your own implementation to connect the web UI to your hardware.
Implement custom handlers. Copy the generated handlers from mongoose_glue.c into your main.c, rename the glue_ prefix to avoid conflicts, and connect them to your hardware logic. For example, controlling an LED:
data API
void my_get_leds(struct leds *leds) {
leds->led1 = gpio_read(LED1); // Read hardware state
}
void my_set_leds(struct leds *leds) {
gpio_write(LED1, leds->led1); // Apply state to hardware
}
// ....
mongoose_init();
mongoose_set_http_handlers("leds", my_get_leds, my_set_leds); // <-- Add this
Modbus API
// See modbus definitions at https://github.com/cesanta/mongoose/blob/modbus/src/modbus.h
static void my_modbus_handler(struct mg_modbus_req *req) {
if (req->func == MG_MODBUS_FUNC_READ_COILS) {
for (uint16_t i = 0; i < req->len; i++) {
req->u.bits[i] = READ_COIL(i);
}
} else {
req->error = MG_MODBUS_ERR_DEVICE_FAILURE;
}
}
// ....
mongoose_init();
mongoose_set_modbus_handler(my_modbus_handler); // <-- Add this
Reference
Different REST API endpoint types generate use different callbacks. The list below contains the summary:
mongoose_set_http_handlers("data", my_get_XXX, my_set_XXX);
mongoose_set_http_handlers("array", my_get_XXX, my_set_XXX);
mongoose_set_http_handlers("action", my_check_XXX, my_start_XXX);
mongoose_set_http_handlers("file", my_read_XXX, my_write_XXX);
mongoose_set_http_handlers("ota", my_open_XXX, my_close_XXX, my_write_XXX);
Similarly, callback for other protocols can be overridden too. Here is the relevant API:
struct mongoose_mqtt_handlers {
struct mg_connection *(*connect_fn)(mg_event_handler_t);
void (*tls_init_fn)(struct mg_connection *);
void (*on_connect_fn)(struct mg_connection *, int);
void (*on_message_fn)(struct mg_connection *, struct mg_str, struct mg_str);
void (*on_cmd_fn)(struct mg_connection *, struct mg_mqtt_message *);
};
void mongoose_set_mqtt_handlers(struct mongoose_mqtt_handlers *);
struct mongoose_modbus_handlers {
bool (*read_reg_fn)(uint16_t address, uint16_t *value);
bool (*write_reg_fn)(uint16_t address, uint16_t value);
};
void mongoose_set_modbus_handlers(struct mongoose_modbus_handlers *);
void mongoose_set_sntp_handler(void (*fn)(uint64_t epoch_ms));
void mongoose_set_auth_handler(int (*fn)(const char *user, const char *pass));
void mongoose_set_modbus_handler(void (*fn)(struct mg_modbus_req *req));
Configuration file format
Configuration file has several sections, annotated below:
{
"version": "1.0.1", // format version
"api": { ... }, // RESTful API definitions
"ui": { ... }, // Web UI controls
"http": { ... }, // HTTP protocol support
"mqtt": { ... }, // MQTT protocol support
"dns": { ... }, // DNS/MDNS protocol support
"sntp": { ... }, // SNTP (network time sync) protocol support
"modbus": { ... }, // Modbus-TCP protocol support
"build": { ... } // Target hardware, IDE, OS
}
REST API
Below is the annotated REST API snippet from the configuration file:
"api": { // RESTful API endpoints. 4 types: data, array, action, upload, ota
"reboot": {
"type": "action", // An action endpoint maps to a button. A button click triggers an action
"read_level": 3, // Read access level
"write_level": 7, // Write access level
"value": false // Default action value - false (not triggered)
},
"firmware_update": {
"type": "ota", // A firmware update endpoint - maps to a button that trigger file upload
"read_level": 3, // Read access level
"write_level": 7 // Write access level
},
"file": {
"type": "file", // A file upload endpoint - maps to a button that trigger file upload
"read_level": 3, // Read access level
"write_level": 7 // Write access level
},
"state": { // struct state will be generated in the glue code
"type": "data", // Data endpoint maps to a Web UI panel, and a C structure
"readonly": true, // Optional attribute that prevents generation of the setter function
"read_level": 3, // Read access level
"attributes": { // Structure attributes
"speed": { "type": "int", "value": 42}, // Integer and its default value
"humidity": { "type": "double", "format": "%.4f", "value": 12.34}, // Double, its format and its default value
"version": { "type": "string", "size": 20, "value": "1.0.0"}, // String, its size and its default value
"online": { "type": "bool", "value": true} // A Boolean and its default value
}
},
"levels": {
"type": "array", // Generates array of objects
"read_level": 3, // Read access level
"write_level": 7 // Write access level
"attributes": {
"name": { "type": "string", "value": "info", "size": 20},
"level": { "type": "int", "value": 2}
}
},
"local": {
"type": "local", // Local to Web UI, does not create any REST endpoint
"read_level": 3, // Read access level
"write_level": 7 // Write access level
"attributes": {...} // Just like for the data endpoint
}
},
See this video for the detailed explanation of the REST API endpoints: https://www.youtube.com/watch?v=gM-bzh_H-tM
Below is a brief explanation of the REST API configuration and the
corresponding usage using the curl command.
REST API of type 'data'
Configuration:
"settings": {
"type": "data",
"attributes": {
"level": { "type": "int", "value": 42},
"name": { "type": "string", "value": "unit1", "size": 20}
}
}
Usage:
$ curl IP/api/settings
{"level": 42, "name": "unit1"}
$ curl IP/API/settings -d '{"level": 10}'
{"level": 10, "name": "unit1"}
REST API of type 'array'
Configuration:
"wifi": {
"type": "sensors",
"attributes": {
"type": { "type": "int", "value": 10},
"name": { "type": "string", "value": "temp1", "size": 20}
}
}
Usage:
$ curl IP/api/sensors
[{"type": 0, "name": "temp1"}, {"type": 1, "name": "humidity1"}]
$ curl IP/API/sensors/1 -d '{"type": 2}'
[{"type": 0, "name": "temp1"}, {"type": 2, "name": "humidity1"}]
Web UI
If Web UI is enabled in the http settings (http.ui set to true), then
the ui section of the configuration describes the UI dashboard that will
be rendered.
"ui": {
"pages": [ // Describes UI pages. Each page is mapped on a sidebar
{
"title": "Dashboard", // Page title
"icon": "desktop", // Page icon
"level": 0, // Access level
"classes": "page", // CSS classes (optional)
"css": "", // Inline CSS styles (optional)
"layout": [ ... ] // Child elements (optional)
}
]
},
The "layout" attribute for pages describe UI elements, which could be nested. Here's the format for the UI element:
{
"type": "", // Optional. Can be "toggle", "action", "dropdown", "input", "upload", "ota". If absent, generates "div" element
"format": "hi!", // Optional. Static text / HTML code. Can contain references to API data: "Current temperature: ${state.temperature}"
"classes": "flex", // Optional. CSS classes
"css": "color: red;", // Optional. Inline CSS styles
"layout": [ ... ] // Optional. Nested elements
}
Expressions
Element's attributes can have embedded Javascript expressions inside.
For example, element's css attribute, which translates to the HTML style,
can be a simple text like color: red;, but can also contain expressions.
Javascript expressions are specified inside the curly brackets prepended
by the dollar sign: ${...}. Expressions can be conditional, which allows
to alter any element depending on the value of the REST API.
For example, this alters the background of element:
"css": "color: cyan; background: ${state.online ? 'green': 'red'};"
Attributes
Below is the list of valid attributes that any element can have:
classes- (any element) a string, space separated list of CSS classes, e.g.css- (any element) a string, semicolon-separated list of CSS rulesdisabled- (input, toggle, dropdown) a boolean, eithertrueorfalsemin,max- (gauge, progress) a numbermin,max,step- (number input) a numberreadonly- (input) a boolean, eithertrueorfalse
In the Wizard UI, those can be set in the "Extra" field, which should be a valid JSON object that one or more of the above keys.
API access levels
Each entry in the ui.api generates a RESTful endpoint /api/ENTRY_NAME.
An entry can be given a read and write access level. When a user logs in to
the UI, a user is given an access level, so the API's read and write level
can restrict who's able to read and write to the given API.
For example, a default dashboard project defines two users, "user" with
access level 3 and "admin" with access level 7. A "leds" API endpoint
allows "user" to read /api/leds, but not modify it. "admin" can both
read and write to it:
$ curl -u admin:admin DEVICE_IP/api/leds
{"led1": false, "led2": false}
$ curl -u admin:admin DEVICE_IP/api/leds -d '{"led2": true}'
{"led1": false, "led2": true}
$ curl -u user:user DEVICE_IP/api/leds -d '{"led2": true}'
Forbidden
Web UI login
If Web UI login is activated on a settings page, then Wizard enables granular access control to pages, panels, and variables in the following way:
- Every user has an associated access level, visible to both UI and the device.
A User - defined
glue_authenticate()function assigns group ID to the users. An access level is an integer from 1 to 9 - Access levels are hierarchical: the higher the level, the more privileged it is User group 9 is the most privileged. Level 1 is the least privileged
- Access level 0 means that a privilege access check is disabled, and anyone can access that API
HTTP protocol support
"http": {
"http": true, // Enable/disable HTTP server
"https": false, // Enable/disable HTTPS (secure) server
"ui": true, // Generate Web UI
"login": true // Enable/disable user login for the Web UI
},
Autoupdate interval
The ui.heartbeat setting is an integer value, which is an interval in
seconds for the Web UI to auto-refresh. If that value is 0, then auto-refresh
is disabled. Otherwise, Web UI makes an /api/heartbeat API call every
ui.heartbeat seconds.
The /api/heartbeat API call returns a number, which in an internal version
count. If that version count changes (increments), then Web UI re-fetches
all API values, and refreshes the UI. That is, in order to automatically
update UI elements, that internal counter should change. That internal counter
is incremented by the glue_update_state() function.
The internal version counter is automatically incremented every time
a glue_set_* function is called - in other words, if someone clicks on the
save button.
If API value is changed somewhere else in the firmware code, the UI must be
notified about this change by calling glue_update_state() function.
Call
glue_update_state()in your firmware to force UI refresh. Note that refreshing UI is expensive, so callglue_update_state()only if any of the API variable values really changes.
Also, that periodic poll updates a toolbar indicator, that
shows device connection status. Normally it is green:

If a device misses hearbeats, it becomes red:

In other words, auto-refresh ensures that you're looking at the Web UI that is not stale.
Websockets updates
If Websocket support is enabled in the HTTP settings, then device can send frequent periodic updates to all connected Web UI clients. The API function that enabled websocket updates is this:
void mongoose_add_ws_reporter(unsigned timeout_ms, const char *name);
Where timeout_ms specifies how frequently the websocket updates should
be sent by the device, and name is the name of the "data" API endpoint
that will be reported.
Therefore, if you'd like to send a real-time data from a device, first
configure the "data" API endpoint, override its handlers, and then call
the mongoose_add_ws_reporter(). The getter function will be called with
the specified frequency, sending data to the UI:
#include "mongoose_glue.h"
static void my_getter(struct real_time_data *data) {
data->value1 = read_adc();
}
int main(void) {
mongoose_init();
mongoose_set_http_handlers("real_time_data", my_getter, NULL);
mongoose_add_ws_reporter(200, "real_time_data");
for (;;) {
mongoose_poll();
}
}
Then the UI can use ${real_time_data.value1} expression to display values.
Custom REST API handlers
Mongoose Web UI builder makes it easy to create REST API handlers that are represented by JSON objects or array of objects. Those objects are mapped to C structures which are easy to handle.
However sometimes it is required to create an API handler that returns arbitrary data. For such cases, Mongoose Web UI builder provides an API function to register a custom API handler:
void mongoose_add_custom_handler(const char *url_pattern, mg_event_handler_t fn);
The url_pattern is a glob expression that should match an URI, and
fn is an event handler function to call. Here is an example:
#include "mongoose/mongoose_glue.h"
// curl -qs IP/my/api
// curl -qs IP/my/api/1/2/3?foo=bar -d '{"a":42}'
static void my_ev_handler(struct mg_connection *c, int ev, void *ev_data) {
if (ev == MG_EV_HTTP_MSG) {
struct mg_http_message *hm = (struct mg_http_message *) ev_data;
MG_INFO(("POST data: [%.*s], URI: [%.*s], QUERY: [%.*s]", //
hm->body.len, hm->body.buf, //
hm->uri.len, hm->uri.buf, //
hm->query.len, hm->query.buf));
mg_http_reply(c, 200, "", "ok\n");
}
}
int main(void) {
mongoose_init();
mongoose_add_custom_handler("/my/api#", my_ev_handler);
for (;;) {
mongoose_poll();
}
return 0;
}
Supported boards
Below is the list of development boards Mongoose Web UI builder supports.
| Architecture | Board | Connectivity |
|---|---|---|
| STM32 | Nucleo-F207ZG | Built-in Ethernet |
| STM32 | Nucleo-F207ZG | Built-in Ethernet |
| STM32 | Nucleo-F429ZI | Built-in Ethernet |
| STM32 | Nucleo-F439ZI | Built-in Ethernet |
| STM32 | Nucleo-F746ZG | Built-in Ethernet |
| STM32 | Nucleo-F756ZG | Built-in Ethernet |
| STM32 | Nucleo-F767ZI | Built-in Ethernet |
| STM32 | Nucleo-H563ZI | Built-in Ethernet |
| STM32 | Nucleo-H723ZG | Built-in Ethernet |
| STM32 | Nucleo-H743ZI | Built-in Ethernet |
| STM32 | Nucleo-H753ZI | Built-in Ethernet |
| STM32 | Nucleo-H755ZI-Q | Built-in Ethernet |
| STM32 | Nucleo-H7S3L8 | Built-in Ethernet |
| STM32 | Nucleo-N657X0-Q | Built-in Ethernet |
| STM32 | STM32H735G-DK | Built-in Ethernet |
| STM32 | STM32H745I-DISCO | Built-in Ethernet |
| STM32 | STM32H747I-DISCO | Built-in Ethernet |
| STM32 | STM32H573I-DK | Built-in Ethernet |
| STM32 | Portenta H7 | Built-in Ethernet, CYW43439 WiFi |
| NXP | Teensy4.1 | Built-in Ethernet |
| NXP | RT1170-EVKB | Built-in Ethernet |
| NXP | RT1020-EVK | Built-in Ethernet |
| NXP | RT1024-EVK | Built-in Ethernet |
| NXP | RT1040-EVK | Built-in Ethernet |
| NXP | RT1050-EVKB | Built-in Ethernet |
| NXP | RT1060-EVKB | Built-in Ethernet |
| NXP | RT1064-EVK | Built-in Ethernet |
| NXP | FRDM-MCXN947 | Built-in Ethernet |
| NXP | FRDM-RW612 | Built-in Ethernet & WiFi |
| Texas Instruments | EK-TM4C1294XL | Built-in Ethernet |
| Texas Instruments | TMS570 | Built-in Ethernet |
| Espressif | ESP32, ESP32xx | Built-in WiFi |
| Infineon | XMC4400 2Go | Built-in Ethernet |
| Infineon | XMC4700 2Go | Built-in Ethernet |
| Infineon | KIT_XMC72_EVK | Built-in Ethernet |
| Infineon | CY8CPROTO-062S2-43439 | Built-in WiFi |
| Raspberry PI | RP2040, RP2350 | W5500, W5100 Ethernet |
| Raspberry PI | Pico-W, Pico2-W | CYW43439 WiFi |
| Nordic | nRF9160 Thingy:91 | Built-in Cellular |
| Renesas | RA6M4 | Built-in Ethernet |
| Renesas | RA8M1 | Built-in Ethernet |
| Zephyr | any | Built-in Ethernet |
| Zephyr | any | W5500 Ethernet |
| Arduino | any | W5500 Ethernet |
| Arduino | any | any WiFI (e.g. ESP32) |
| Windows | - | - |
| Linux,MacOS | - | - |
FAQ
Can I manually edit the wizard-generated UI ?
The generated UI cannot be manually edited. The Wizard does not generate HTML.
Instead, the Wizard UI engine renders the configuration ui section
dynamically. If you want full manual control over your UI, do not use the
Wizard and create your own UI manually, see the device dashboard
tutorial