Mongoose LogoMongoose

MQTT Dashboard

Overview

This tutorial shows an example of how to build an MQTT dashboard, what can be very useful to manage remote devices.

The concept is to decouple the user interface (UI) from the device. The UI runs on a web browser and interacts with the device through an MQTT broker.

High level architecture

There is a root topic shared by everyone. Each device connects to the broker via MQTT and publishes its status on a subtopic including its device id. A dashboard (a web browser) connects to the broker using MQTT over WebSocket and subscribes to the root topic, getting all status messages published by all devices. Clients then know all devices and whether they are online or not.

When a user wants to operate on a device, chooses one from the list. The dashboard can then send commands to the device on a subtopic, and receive responses on another subtopic.

Topic structure

  • root_topic/device_id/status - each device publishes its status here
  • root_topic/device_id/rx - clients publish commands for a device in this topic
  • root_topic/device_id/tx - each device responds to received commands here

RPC API

MQTT messages are JSON RPC messages. The API for this example is the following:

  • state.set - set the LED state using led_status
  • ota.upload - upload base64-encoded firmware chunks using offset, total, and chunk

The firmware upload is a custom chunked JSON-RPC transport; its protocol is described in the firmware update tutorial

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 dashboard, this will also start Mongoose to serve it:

    cd mongoose/tutorials/mqtt/mqtt-dashboard/dashboard
    make clean all
    ...
    1778e85b 2 net.c:142:fn                1 CONNECTED to mqtt://broker.hivemq.com:1883
    1778e85b 2 net.c:98:subscribe          1 SUBSCRIBED to topic_mg_device/2tyInbE5q/rx
    1778e85b 3 mqtt.c:328:mg_mqtt_pub       1 [topic_mg_device/2tyInbE5q/status] -> [{"method":"status.notify","params":{"status":"online","led_status":false,"firmware_version":"1.0.0"}}]
    17
    
  • Start a second terminal in the project directory and build the device, this will also start it:

    cd mongoose/tutorials/mqtt/mqtt-dashboard/device
    make clean all
    
  • Start your browser on http://localhost:8000, the main page is loaded:

    demo web page

    What you see there has been populated with the retained messages the broker publishes to the dashboard when it connects. Each device, when it connects, handles a last-will status message indicating it is offline, then publishes an online status message. Should a device go offline, the broker will publish its last-will message. Hence, when a dashboard connects, it gets all status messages retained, from all devices that once published a retained message in the root_topic tree.

    • We can simulate that manually by connecting to the broker with an MQTT or MQTT over WS client, for example:

      mosquitto_sub -v -h  broker.hivemq.com -p 1883 -t topic_mg_device/#
      topic_mg_device/WBiGPTLSB/status {"method":"status.notify","params":{"status":"offline"}}
      ...
      topic_mg_device/2tyInbE5q/status {"method":"status.notify","params":{"status":"online"}}
      ...
      topic_mg_device/QK5tL5cJ2/status {"method":"status.notify","params":{"status":"offline"}}
      
  • Selecting a device in the menu on the left opens its status and control screen. Toggling the LED publishes a message on the corresponding device rx topic. The broker then publishes that message to the device, which processes it and responds on its tx topic, followed by a new status notification on its status topic. The broker then publishes those messages to the clients.

    • We can simulate that manually by connecting to the broker with an MQTT or MQTT over WS client, for example:

      mosquitto_sub -v -h  broker.hivemq.com -p 1883 -t topic_mg_device/#
      ...
      topic_mg_device/2tyInbE5q/rx {"method":"state.set","id":"rle42tlds7b","params":{"led_status":true}}
      topic_mg_device/2tyInbE5q/tx {"id":"rle42tlds7b","result":"ok"}
      topic_mg_device/2tyInbE5q/status {"method":"status.notify","params":{"status":"online","led_status":true,"firmware_version":"1.0.0"}}
      

    The id field is used to correlate responses to requests. This simple method can handle a fair quantity of clients as the probability of having a repeated id is very low. For large networks, other more involved strategies exist.

  • The OTA features are described in a separate tutorial

Dashboard implementation

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; it can be served by any web server, including of course Mongoose, so it can actually be served by a small embedded device, a Raspberry Pi, etc.

For more information on building Web User interfaces, check our set of respective tutorials.

The Makefile in this example builds the UI and the Mongoose http-server example, then starts the server on port 8000

Browse latest code

Device implementation

Let's now dive into the internals of how to build a headless device like this one.

MQTT message handling

All MQTT events are handled by this event handler function:

  • When the connection to the broker is established, we receive an MG_EV_MQTT_OPEN event, then we:

    • Subscribe to the receive topic:

    • Send a status notification, telling the world we are online:

    The status message is printed to an IO buffer with mg_xprintf(). MG_ESC() JSON-escapes the string fields.

  • The connection is established and kept open by a timer, that always checks it is up; otherwise it retries. On connection, we request the broker to send a retained last-will message on our behalf; that message contains a status notification indicating we are offline:

  • When an MQTT message is received from the broker on our rx topic (originally published by a dashboard), we get an MG_EV_MQTT_MSG event. This means we got ourselves a JSON message to process. We'll put that inside the frame of a request structure and handle it to mg_rpc_process() to invoke the requested method. Note we pass a pointer to mg_pfn_iobuf(), a character printing function, and initialize an IO buffer (a simple data structure that inserts or deletes chunks of data at arbitrary offsets and grows/shrinks automatically) prior to handle it to the processor.

    Then, if there is a result, we'll publish a response in our tx topic and also update our status

For more information on MQTT clients, check this tutorial

RPC handling

The RPC callbacks implement LED control and the custom OTA upload transport:

The RPC processor is initialized by adding the used methods:

This is done at the web_init() function

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 we are used to, in which we just initialize an event manager and start the event loop.

The function web_init() takes care of initializing the necessary ids, and starts the periodic timers that will keep the MQTT connection up and running, as we've seen, and to send the necessary keepalives

Browse latest code

Embedded device examples

The following are hardware examples that implement a remote MQTT controllable device as seen above.

Board Example code
Raspberry Pi Pico (USB RNDIS) tutorials/pico-sdk/pico-rndis-device