Device Dashboard
Overview
This tutorial shows an example of how to build a device dashboard, what can be very useful for headless devices.
- This Web dashboard provides:
- User Authentication: login protection with multiple permission levels
- The web UI is optimised for size and for TLS usage
- Logged users can view/change device settings
- The web UI is fully embedded into the firmware binary, and does not need a filesystem to serve it, making it resilient
The concept is to decouple the user interface (UI) from the supporting API. The UI runs on a web browser and interacts with the device via a RESTful API.
The UI is developed using the Preact framework, in order to fit on very small devices.
The device employs a hybrid server architecture which serves both static and dynamic content. Static files, like CSS/JS/HTML or images, are packed and compiled to an embedded filesystem that is linked into the server binary, in order to not rely on nor require that a filesystem be present on the device.
REST API
The RESTful server API provides the following authenticated endpoints:
/api/login
- logs the user in, setting the access token; returns user name as a JSON object/api/logout
- logs the user out, clearing the access token/api/debug
- sets Mongoose debug level from a JSON object/api/stats/get
- returns statistics (variable values) as an array in a JSON object/api/events/get
- returns the event log as an array of JSON objects/api/settings/get
- returns the device settings as a JSON object/api/settings/set
- sets the device settings from a JSON object
Build and try
Follow the Build Tools tutorial to setup your development environment.
Start a terminal in the project directory; if you've not already done so, clone the Mongoose Library repo
git clone https://github.com/cesanta/mongoose
Build the example, this will also start Mongoose:
cd mongoose/examples/device-dashboard make clean all
Start your browser on
http://localhost:8000
, the main page is loaded but authorization will fail, so you should see a simple login page:Login, the JavaScript code on the web page renders the UI and retrieves the necessary data by calling the REST API
We can simulate that manually by connecting to the REST API with curl, for example:
curl -u admin:admin localhost:8000/api/stats/get {"temperature":21,"humidity":67,"points":[21,22,22,19,18,20,23,23,22,22,22,23,22]}
Selecting
Events
in the menu on the left, you'll open the Events screen. What you see there has been retrieved by the UI requesting (GET) the REST API/api/events/get
endpoint. We can simulate that manually by connecting to the REST API with curl, for example:We can simulate that manually by connecting to the REST API with curl, for example:
curl -u admin:admin localhost:8000/api/events/get {"arr":[{"time":1700101028,"type":3,"prio":0,"text":"event#0"},{"time":1700101328,"type":3,"prio":0,"text":"event#1"},{"time":1700101639,"type":2,"prio":1,"text":"event#2"},{"time":1700101919,"type":2,"prio":2,"text":"event#3"},{"time":1700102325,"type":1,"prio":0,"text":"event#4"},{"time":1700102561,"type":3,"prio":2,"text":"event#5"},{"time":1700102763,"type":1,"prio":2,"text":"event#6"},{"time":1700103250,"type":1,"prio":1,"text":"event#7"},{"time":1700103293,"type":0,"prio":2,"text":"event#8"},{"time":1700103816,"type":3,"prio":0,"text":"event#9"},{"time":1700104129,"type":3,"prio":2,"text":"event#10"},{"time":1700104448,"type":3,"prio":0,"text":"event#11"},{"time":1700104645,"type":0,"prio":2,"text":"event#12"},{"time":1700105030,"type":2,"prio":2,"text":"event#13"},{"time":1700105102,"type":3,"prio":0,"text":"event#14"},{"time":1700105648,"type":1,"prio":0,"text":"event#15"},{"time":1700105768,"type":0,"prio":1,"text":"event#16"},{"time":1700106190,"type":1,"prio":2,"text":"event#17"},{"time":1700106306,"type":2,"prio":0,"text":"event#18"},{"time":1700106699,"type":1,"prio":0,"text":"event#19"}], "totalCount":400}
Selecting
Settings
in the menu on the left, you'll open the Device Settings screen. What you see there has been retrieved by the UI requesting (GET) the REST API/api/settings/get
endpoint.If you now click on
Save settings
the UI will POST a JSON object containing the new settings to the REST API/api/settings/set
endpointSelecting
Firmware Update
in the menu on the left, you'll access the corresponding menu. These OTA features are described in a separate tutorial
SSL/TLS (HTTPS) support
In order to build with mbedTLS support, run:
make TLS=mbedtls
Once the executable is rebuilt and is running, you can point your browser to https://localhost:8443 to access the Web UI via HTTPS.
Note: the example uses a self-signed certificate, thus, your browser will show a security warning. Ignore the warning and proceed to the Web UI
Check the "How to build" section of the TLS tutorial for specific information on other building options for your OS
Backend implementation
Let's now dive into the internals of how to build a device dashboard like this one. We'll start from the server backend, running on the device itself.
UI files and embedded filesystem
When you connect your browser to http://localhost:8000
, it will ask to GET the index file, which contains references to JavaScript files, including the Preact code, and images. All these are static files and are served by the static server section of our hybrid server architecture. The event handler function calls the mg_http_serve_dir() function when it receives an MG_EV_HTTP_MSG
event for a non-API URI:
static void fn(struct mg_connection *c, int ev, void *ev_data, void *fn_data) {
...
if (ev == MG_EV_HTTP_MSG) {
if (mg_http_match_uri(hm, "/api/#")) {
...
} else {
struct mg_http_serve_opts opts = {...};
mg_http_serve_dir(c, ev_data, &opts);
}
}
}
All the web files can be embedded in a packed filesystem (more information on the embedded filesystem tutorial). For this example we've chosen not to do so, this way the developer can work on the web files serving them on a computer, and embed them later when deploying to an embedded device
In fact, this example suggests a productive way to develop a UI for an MCU: develop it on a workstation using stubs for the hardware API, then just build on the MCU.
Auth and login
The JavaScript code will try to GET /api/login
, which requires authorization. If it fails, it will present the login screen. Once you enter your credentials, it will GET /api/login
again but this time using the authorization headers. If user and password are valid, the server will return a token that can be used for further accesses. A logout clears the token. In the next section we'll see how these functions get called.
The hybrid server handles authorization by checking for a valid user and denying all /api/
URIs in case there is not.
User credentials are extracted from the headers using the mg_http_creds() function. This code snippet shows a simple way of doing user authentication, valid for the purpose of this example:
RESTful API
The RESTful server makes use of mg_http_match_uri() for URI matching, and mg_http_reply() to generate responses. Here, the %m
extension, through the macro MG_ESC()
, calls mg_print_esc() that simplifies printing JSON strings.
Take a look at the JSON-RPC tutorial for details on how to use Mongoose's built-in JSON and RPC APIs.
main() and init
The main()
function maintains a structure similar to that of an HTTP server, in which we just initialize an event manager and
start the event loop, as usual.
The function web_init()
takes care of initializing the HTTP listener and the periodic timer that will start an SNTP request to get current time
Frontend implementation
This is an integration tutorial, for more details on some of the techniques used here consult the tutorials in the Web UI section, or those referenced further on in this text.
As we've seen above, all frontend files are read from disk in this example, with the idea of being embedded on a packed filesystem when deploying to an MCU. In order to pack some of the JavaScript files, npm needs to be installed.
Overview
The UI is developed using the Preact framework, an alternative to React with the same modern API and a smaller footprint, ideal to fit on small devices with limited room for flash memory. Once served via the HTTP server, it will run on the browser.
On a content refresh, each section fetches its corresponding RESTful API endpoint to get the data it will render on screen:
The settings handling section will POST to the /api/settings/set
URI the new config on a button press:
For more detailed information on working with Preact, check the Preact-based UI tutorial
Login process
- The initialization section tries to GET the
/api/login
URI with no authorization headers; if it fails, it will render the login screen
- When credentials are entered at the login screen, the following GET will use proper authorization headers
- Once the REST API grants access and returns the user token; this token will be utilized for all remaning API transactions. A logout simply calls the REST API function that clears the token
For more detailed information on the login process, check the Web UI login tutorial