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.

High level architecture

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:

    demo web page
  • Login, the JavaScript code on the web page renders the UI and retrieves the necessary data by calling the REST API

    demo web page
    • 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:

    demo web page
    • 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 endpoint

  • Selecting 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

Browse latest code