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 rules
  • disabled - (input, toggle, dropdown) a boolean, either true or false
  • min,max - (gauge, progress) a number
  • min,max,step - (number input) a number
  • readonly - (input) a boolean, either true or false

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:

  1. 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
  2. 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
  3. 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 call glue_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: Successful heartbeat indicator

If a device misses hearbeats, it becomes red: Successful heartbeat indicator

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