Web Dashboard

The dashboard module provides a small HTTP/WebSocket UI for inspecting and controlling runtime state. You describe what to expose as one or more named field sets; the dashboard can read values (via a set reader function) and apply changes (via a set writer function).

Header: https://github.com/cesanta/mongoose/blob/master/src/dash.h

enum mg_val_type

enum mg_val_type {
  MG_VAL_INT,
  MG_VAL_BOOL,
  MG_VAL_DBL,
  MG_VAL_STR,
  MG_VAL_RAW,
};

Value type for a field.

struct mg_field

struct mg_field {
  const char *name;
  enum mg_val_type type;
  void *value;
  size_t value_size;
};
  • name - Field name as shown in the UI / JSON
  • type - Field type, see enum mg_val_type
  • value - Pointer to the backing storage
  • value_size - Size of the backing storage in bytes (used for MG_VAL_STR / MG_VAL_RAW)

struct mg_field_set

struct mg_field_set {
  const char *name;
  struct mg_field *fields;
  void (*reader)(void);
  void (*writer)(void);
  int read_level;
  int write_level;
  struct mg_field_set *next;
};

A named group of fields.

  • fields - Array of fields (typically static)
  • reader - Optional callback that refreshes value before a read
  • writer - Optional callback invoked after a write is applied
  • read_level / write_level - Access control levels for reads and writes

struct mg_dash

struct mg_dash {
  struct mg_field_set *sets;
  struct mg_dash_custom_handler *custom_handlers;
};

Dashboard context.

struct mg_dash_custom_handler

struct mg_dash_custom_handler {
  struct mg_dash_custom_handler *next;
  struct mg_str uri_pattern;
  mg_event_handler_t handler;
  void *handler_data;
};

Linked list of additional handlers registered with MG_DASH_REGISTER_CUSTOM_HANDLER().

struct mg_dash_file

struct mg_dash_file {
  struct mg_dash_file *next;
  char *name;
  size_t size;
};

Entry used by the dashboard file manager.

MG_DASH_ADD_FIELD_SET()

#define MG_DASH_ADD_FIELD_SET(dash_, set_)

Registers a struct mg_field_set with a dashboard context.

MG_DASH_REGISTER_CUSTOM_HANDLER()

#define MG_DASH_REGISTER_CUSTOM_HANDLER(dash_, uri_, fn_, data_)

Registers an additional URI handler (pattern match) under the dashboard server.

File manager

extern struct mg_dash_file *mg_dash_files;
void mg_dash_file_add(struct mg_str name, size_t size);
void mg_dash_file_del(struct mg_str name);

Tracks named files (e.g. for exposing uploads / generated assets).

mg_dash_ev_handler()

void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data);

Dashboard HTTP/WebSocket event handler.

mg_dash_send_change()

void mg_dash_send_change(struct mg_mgr *mgr, struct mg_field_set *);

Notifies connected dashboard clients that a field set has changed.

mg_dash_init(), mg_dash_poll()

void mg_dash_init(struct mg_mgr *);
void mg_dash_poll(struct mg_mgr *);

Initialises and runs the dashboard module. Call mg_dash_poll() periodically from your main loop.

Default listen addresses

#define MG_HTTP_ADDR  "http://0.0.0.0:8000"  // desktop
#define MG_HTTPS_ADDR "https://0.0.0.0:8443" // desktop
#define MG_MODBUS_ADDR "tcp://0.0.0.0:8502"  // desktop

On embedded targets the defaults typically use ports 80/443/502.

Minimal setup example

See https://github.com/cesanta/mongoose/tree/master/tutorials/device-dashboard/minimal

The conditionals in dashboard.c make it portable across both desktop and microcontroller builds. This lets you iterate quickly on the UI in a desktop environment, then reuse the same code on the target device to implement hardware-specific functionality.

Reference: general architecture

The diagram below shows a general overview of the flow. For the Web UI dashboard and your device to communicate, both sides must implement a "glue" layer.

  1. On the device side, integrate Mongoose. Add a dashboard.c file that acts as a glue layer on the device side. You need to implement dashboard.c yourself, using examples as a reference.

  2. Develop the Web UI using any method, for example with AI, and include the dashboard.js library in your UI. The dashboard.js file is provided ready to use. After adding it, include special attributes on selected HTML elements so that dashboard.js can take control of them and bind them to your device.

Dashboard Architecture

As soon as your dashboard loads the dashboard.js library, it establishes a WebSocket connection to your device and observes all requests and change notifications.

When dashboard.js receives a change, it scans the UI, updates all ${...} expressions, and processes all data-... HTML attributes.

On the dashboard.c side, split your device data into fields, group fields into field sets, and combine all field sets into the struct mg_dash dashboard descriptor.

The dashboard code serialises each field set into a JSON object, where each field becomes a key. Every field set maps to a REST API endpoint that returns and accepts a JSON object with the corresponding keys.

As an example, consider the device settings panel in the tutorials/device-dashboard/minimal example. The device state is represented as follows:

When the dashboard needs to fetch device status, it calls the read_status() function. This function populates variables so that field definitions can read them and report their values to the UI.

When a user toggles a control, the UI sends a JSON request to update the status. The dashboard code receives a request like this:

{"status": {"led1": true}}

The dashboard code parses it, figures out that the "status" field set and "led1" field reference a boolean variable s_led1. The dashboard code then updates s_led1 with the value from the request, and calls write_status() so user code can synchronise s_led1 to the hardware.

Reference: dashboard.js

Certain HTML elements in the dashboard.html, which should be bound to the device, should be instrumented in the following way:

  • Add data-bind="FIELDSET.FIELD" to toggles, inputs and dropdowns, to bind them to a specific field.
  • Add data-autosave="1" if you want user edits to the bound keys automatically saved to the device
  • Add data-save="FIELSET" to the buttons create a "save" button for user edits. It will watch the values that in the fieldset and if they are edited, a button would activate, otherwise it'll stay disabled
  • Add data-cancel="FIELDSET" similar to data-save, but for cancelling edits

This is the example code that binds a UI toggle button to the device's LED and shows LED status using expression:

Expressions

You can add ${...} JS expressions in your HTML. When the expression is evaluated, its evaluation context contains your Dashboard data, and all global JS variables like window. For example, ${settings.volume + 1} will be evaluated exactly as you think.

Conditional display

Expressions are useful for the conditional display: expression can add CSS classes, or CSS styles depending on some conditions. For example, this snippet adds orange warning style, or red alert style, to the metric display:

Normally, the notification text is hidden. But depending on a condition, the warning or the alert notification is shown.

Reference: embedding dashboard.html

First, bundle your UI into a single, self-contained HTML file with no external dependencies. There are several ways to do this. A simple option is to use the inline.js tool from the Mongoose repository:

curl -sO https://raw.githubusercontent.com/cesanta/mongoose/master/resources/inline.js
node inline.js dashboard.html > inline.html

Now that you have a self-contained inline.html, you can compress it and convert it into a .c file that contains a C or C++ byte array with the file data. There are several ways to do this. We recommend using the pack.js tool from the Mongoose repository:

curl -sO https://raw.githubusercontent.com/cesanta/mongoose/master/resources/pack.js
node pack.js inline.html:dashboard.html:gzip > file_data.c

Now include file_data.c in your project sources and rebuild.

For background, see the serving HTML/CSS/JS guide. Embedding the UI in firmware makes it resilient to filesystem issues, such as a failing SD card, and keeps it in sync with the device backend. For example, if the UI on the SD card is updated but the firmware rolls back, the dashboard can break.

Reference: JSON-RPC communication

The dashboard.js library, when included in HTML, creates a global window.Dashboard object with the following properties:

Dashboard.init() initializes the library. Dashboard.call(name, args) executes a function on the device and returns the response. Dashboard.on(name, func) registers a handler function.

When the page loads, dashboard.js opens a WebSocket connection to the /api/websocket endpoint. This creates a real-time, bidirectional communication channel between the UI and the device. If the connection is lost, it retries every second.

The UI and the device communicate using JSON-RPC over WebSocket, with three simple commands: get, set, and change.

The get command is sent from the UI to the device. It fetches data fields and updates the Dashboard.data object.

Without parameters, the get command retrieves all fields:

// Request and response
{"id": 1, "method": "get"}
{"id": 1, "result": {"led1": true, "led2": false, "volume": 17}}

With a parameter, get fetches only the requested field:

// Request and response
{"id": 1, "method": "get", "params": "volume"}
{"id": 1, "result": {"volume": 17}}

Once the WebSocket connection is established, the device sends change notifications for all registered data sets, and dashboard.js updates the Dashboard.data object. The object defined in the init() function is replaced, since the device is the source of truth.

The set command is sent from the Web UI to the device. It instructs the device to update one or more fields with specified values. The command returns true if the change was applied, or false otherwise.

// Request and response
{"id": 1, "method": "set", "params": {"led1": true, "volume":0}}
{"id": 1, "result": true}

The change command is notification sent by a device to all connected clients, informing about the value change for a certain field. Since it is a JSON-RPC notification, it does not have an id, and does not expect a response:

{"method": "change", "params": {"volume":12}}

When change is received, device.js applies it to the Dashboard.data object.

NOTE: if there are multiple browsers connected to one device, any change made on one browser, automatically reflects on all other browsers because of this automatic change broadcasting.

When device.js mutates the Dashboard.data object, it scans all HTML elements on the dashboard page, and looks for the data-... attributes in them. If such an attribute is found, dashboard.js modifies the HTML element accordingly.

NOTE: the Dashboard.call() essentially relays the call from the UI to the device, so you can think of it as a call is executing on a device and you see device's response. By default however only the get and set methods are handled.