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 / JSONtype- Field type, seeenum mg_val_typevalue- Pointer to the backing storagevalue_size- Size of the backing storage in bytes (used forMG_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 refreshesvaluebefore a readwriter- Optional callback invoked after a write is appliedread_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.
On the device side, integrate Mongoose. Add a
dashboard.cfile that acts as a glue layer on the device side. You need to implementdashboard.cyourself, using examples as a reference.Develop the Web UI using any method, for example with AI, and include the dashboard.js library in your UI. The
dashboard.jsfile is provided ready to use. After adding it, include special attributes on selected HTML elements so thatdashboard.jscan take control of them and bind them to your device.
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 thegetandsetmethods are handled.