How to Issue MQTT Requests Under Microvisor

Microvisor provides a set of system calls which allow your application to communicate with Internet-hosted servers that use MQTT , a lightweight network protocol for machine-to-machine messaging. These servers are called brokers in MQTT jargon, and your application takes the role of an MQTT client.

MQTT is a pub-sub protocol, short for ‘publish-subscribe’. The broker provides one or more topics to which clients may subscribe. Subscription to a topic allows clients to receive messages published by other clients to that topic. Clients may publish messages to any topic — these messages are then relayed to all of the topic’s other subscribers.

This guide will show you how your application works with Microvisor to connect to public and private MQTT brokers, to subscribe to topics, and to post messages to those topics, and to be notified of inbound messages. Your application asks Microvisor to perform MQTT tasks on its behalf, and Microvisor will respond immediately to say it has accepted the command or to provide a reason why it is unable to do so. It will respond asynchronously with the outcome of accepted operations. Microvisor’s notification system is used to manage this asynchronicity.

Microvisor’s MQTT support is built upon its core networking system. In other words, you ask Microvisor to establish a network connection and then to open a data-transfer channel through which MQTT requests and the responses they generate will flow. For this reason, you should review A Guide to Microvisor Networking before continuing. We’ll assume that you know how to set up networking and notifications, and work with channels.

If you’re not familiar with MQTT, read through HiveMQ‘s MQTT Essentials series to get up to speed.

Check out our sample code collection for full MQTT demos covering generic MQTT brokers, AWS IoT, and Azure IoT.

MQTT channels

Microvisor MQTT communications are hosted by network data channels of the type MV_CHANNELTYPE_MQTT. You will use this channel type when you call mvOpenChannel() and pass in a reference to a standard MvOpenChannelParams structure as described in A Guide to Microvisor Networking and the Network system calls documentation.

MQTT channels’ send and receive buffers must both be sized to a multiple of 512 bytes and their addresses must be 512-byte aligned. For example:

// Set up the MQTT channel's send and receive buffers
static volatile uint8_t mqtt_channel_rx_buffer[1536] __attribute__((aligned(512)));
static volatile uint8_t mqtt_channel_tx_buffer[1536] __attribute__((aligned(512)));

Make MQTT requests

Issuing an MQTT request is a two-stage process:

  1. Your code submits the request to Microvisor which immediately accepts or rejects it.
  2. If the request is accepted, it is relayed by Microvisor to the Microvisor cloud and then transmitted to the broker.

If Microvisor accepts the request, the system call used to submit the request returns MV_STATUS_OKAY. If Microvisor rejects the request, indicated by a non-zero return value from the system call, then the request is not considered to be issued, and the channel can be used to re-issue the request.

Accepted requests can still fail. The Microvisor cloud may be unable to reach the target broker, for example, or its URL may not resolve. In such cases, the reason will be reported to your application by notification. Whatever the outcome of a successfully accepted MQTT request, a response will always be generated and your application notified via the notification center you set up when you established the channel. Each response contains data relevant to the type of request that initiated it plus a value to indicate success or failure. This value is of type MvMqttRequestState, which has the following possible values:

Constant Connection Only Description
MV_MQTTREQUESTSTATE_REQUESTCOMPLETED No The request was completed successfully
MV_MQTTREQUESTSTATE_INVALIDPARAMETERS No Invalid connection parameters were specified
MV_MQTTREQUESTSTATE_ALREADYCONNECTED No An MQTT connection has already been attempted on this channel
MV_MQTTREQUESTSTATE_NOTCONNECTED No The MQTT connection was not established or failed at the point the request was made
MV_MQTTREQUESTSTATE_NXDOMAIN Yes DNS resolution of the broker URL failed
MV_MQTTREQUESTSTATE_UNKNOWNCA Yes An unknown certificate authority was specified
MV_MQTTREQUESTSTATE_SOCKETERROR Yes A socket error was encountered on connect
MV_MQTTREQUESTSTATE_CONNECTIONCIRCUITBREAKER No The Microvisor Cloud dropped the connection because incoming messages were rate limited

All Microvisor MQTT request calls take the form mvMqttRequest<ACTION>() and their responses are accessed using mvMqttRead<ACTION>Response(). The availability of responses to requests are signaled by notification: specifically, the notification center you’ve assigned to the MQTT channel receives a notification of type MV_EVENTTYPE_CHANNELDATAREADABLE. When this occurs, your code should call mvMqttGetNextReadableDataType() and use a switch statement to retrieve the appropriate response type.

There are also MQTT system calls for handling messages, but they do not take the form described above. We’ll cover these calls later.

Connect to a broker

The first MQTT operation your code needs to perform is to connect to the target broker. To do so, call mvMqttRequestConnect() and pass in the handle of an available MQTT channel and a pointer to an MvMqttConnectRequest structure which contains session-level configuration data:

struct MvMqttConnectRequest {
  enum MvMqttProtocolVersion protocol_version;
  struct mvSizedString host;
  uint16_t port;
  struct mvSizedString clientid;
  struct MvMqttAuthentication *authentication;
  struct MvTlsCredentials *tls_credentials;
  uint32_t keepalive;
  uint8_t clean_start;
  struct MvMqttWill *will;

The values of host and clientid are data structures that hold pointers to bytes containing, respectively, the broker’s URL and an identifier for the client, and the number of bytes in each of those values. The client ID must be unique to the broker. MQTT 3.1.1 allows you to send a zero-byte client ID if you don’t need its state to be maintained by the broker. In this case, the property clean_start must be set to a non-zero value, or the broker will reject the connection.

Specifically, clean_start is a flag which can be set to a non-zero value to inform the broker you don’t wish to establish a persistent session, i.e., the broker should completely forget about the client when the connection closes. Pass zero for a persistent session: in this case, the broker will then retain across disconnections any subscriptions established by the client plus all messages with an MQTT quality of service (QoS) level 1 or 2 from subscribed topics.

Microvisor’s MQTT implementation does not support QoS level 2.

The value of protocol_version should be set to indicate the MQTT protocol version (3.1.1 or 5) used by the broker, either MV_MQTTPROTOCOLVERSION_V3_1_1 or MV_MQTTPROTOCOLVERSION_V5.

port is the broker’s port number, and keepalive is the connection keepalive interval in seconds.

An MQTT client may specify a will message when it connects to a broker. A will is a normal MQTT message which the broker keeps. If the broker later detects that the client has disconnected unexpectedly, it sends the will to all other clients that have subscribed to its will messages topic (see your broker’s documentation for this topic’s name). If the client disconnects gracefully, the broker discards the stored will.

Wills are optional. You specify a will message when you configure your connection to the broker: the MvMqttConnectRequest structure’s will property takes a pointer to an MvMqttWill structure:

struct MvMqttWill {
  struct MvSizedString topic;
  struct MvSizedString payload;
  uint32_t qos;
  uint8_t retain

Set will to NULL if you don’t wish to provide a will message.

The values of topic and payload are data structures holding, respectively, the target topic name and the body of the message, plus the the number of bytes in each of those parameters. qos is the required MQTT QoS setting: 0 (no mandated delivery) or 1 (must be delivered at least once).

retain is the MQTT message retention flag. Set this to a non-zero value to instruct the broker to set the message as its topic’s retained message. Each topic can have only one retained message, which will be automatically sent to every new subscriber.

Authenticate with the broker

Production MQTT brokers may require clients to authenticate before they will be granted access. Additionally, client and server may need to authenticate each other before they agree to establish an encrypted connection. Microvisor supports these methods as follows.

The MvMqttConnectRequest structure’s authentication property is a structure of type MvMqttAuthentication:

struct MvMqttAuthentication {
  enum MvMqttAuthenticationMethod method;
  struct MvMqttUsernamePassword *username_password;

The value of method is MV_MQTTAUTHENTICATIONMETHOD_NONE or MV_MQTTAUTHENTICATIONMETHOD_USERNAMEPASSWORD. If you are using the former, there is no need to include a value for the username_password property — set it as NULL — which takes an MvMqttUsernamePassword structure:

struct MvMqttUsernamePassword {
  struct mvSizedString username;
  struct mvSizedString password;

Provide each credential as a data structure that comprises the item and its length.

Unless you also provide SSL certificate authentication data, you will communicate with the MQTT broker in plain text, which can reveal your access credentials if you are using any. To configure SSL authentication, your MvMqttConnectRequest structure must include a tls_credentials property: its value is a structure of type MvTlsCredentials which has two properties: the server’s certificate chain and that of the device, for the mutual authentication process. The device data also includes the device’s RSA private key for signing purposes.

Here are the structures you need to complete and add to your MvMqttConnectRequest:

struct MvTlsCredentials {
  struct MvTlsCertificateChain cacert;
  struct MvOwnTlsCertificateChain clientcert;

struct MvTlsCertificateChain {
  uint32_t num_certs;
  struct MvSizedString *certs;

struct MvOwnTlsCertificateChain {
  struct TlsCertificateChain chain;
  struct MvSizedString key;

The value of each certificate included in an MvTlsCertificateChain’s certs array comprises the certificate data and the number of bytes it comprises. The data itself is of the DER (Distinguished Encoding Rules) binary data form. You can convert the more familiar PEM form to DER data with:

openssl x509 -in cert.pem -inform pem -out cert.der -outform der

And for keys:

openssl pkcs8 -topk8 -in key.pem -inform pem -out key.der -outform der -nocrypt

The Microvisor API does not currently support the transfer of binary files, so you will first need to encode your DER files as Ascii text that can be uploaded. Your application will need to decode these Ascii strings back to the binary form before the DER data is used in client-broker communications. Our MQTT demo repo shows you one way to do this.

Connection outcomes

Having issued your connect request to Microvisor, and assuming that the call returns MV_STATUS_OKAY, wait for a notification of type MV_EVENTTYPE_CHANNELDATAREADABLE. When this arrives, call mvMqttGetNextReadableDataType() and pass in the channel’s handle and a pointer to four bytes into which Microvisor will write the type of the MQTT data available. In this case, it will be MV_MQTTREADABLEDATATYPE_CONNECTRESPONSE and you can therefore call mvMqttReadConnectResponse() to retrieve the response itself. This call takes the channel handle and the pointer to an MvMqttConnectResponse structure:

struct MvMqttConnectResponse {
  enum MvMqttRequestState request_state;
  uint32_t reason_code;

The value Microvisor will write to reason_code is supplied by the broker and will be a standard MQTT value. Zero indicates no error.

If request_state is MV_MQTTREQUESTSTATE_REQUESTCOMPLETED then you are connected to the broker and good to proceed.

Publish messages

Call mvMqttRequestPublish() to post a message to a topic. This call takes the MQTT channel’s handle and a pointer to an MvMqttPublishRequest structure:

struct MvMqttPublishRequest {
  uint32_t correlation_id;
  struct MvSizedString topic;
  struct MvSizedString payload;
  uint32_t payload_len;
  uint32_t desired_qos;
  uint32_t retain;

The value of correlation_id is yours to choose — use it to help you match response to request. The values of topic and payload are data structures that hold pointers to bytes containing, respectively, the target topic name and the message payload, and the number of bytes in each of those values.

The value of desired_qos is the MQTT QoS level: 0 or 1. Set retain to a non-zero value to instruct the broker to set the message as the chosen topic’s retained message.

Publish request responses

If Microvisor accepts the publish request, it will begin the process of issuing the request to the broker: the message will be sent to the Microvisor cloud and then on to the broker. Success or failure is indicated by a notification of type MV_MQTTREADABLEDATATYPE_PUBLISHRESPONSE. You can now call mvMqttReadPublishResponse() to access the response. This call takes the MQTT channel’s handle and a pointer to an MvMqttPublishResponse structure:

struct MvMqttPublishResponse {
  enum MvMqttRequestState request_state;
  uint32_t correlation_id;
  uint32_t reason_code;

First check request_state: its value will be MV_MQTTREQUESTSTATE_REQUESTCOMPLETED if the broker accepted the message. You can use the value of correlation_id to match the response to the request that generated it: if you issued the same message to multiple topics, for example.

Subscribe to topics

Call mvMqttRequestSubscribe() to subscribe to one or more topics. Subscription requests are handled one at a time, so it’s best to subscribe to multiple topics by passing all of them into a single call, rather than make one call per topic.

As always, the request call takes the MQTT channel’s handle. It also takes a pointer to an MvMqttSubscribeRequest structure:

struct MvMqttSubscribeRequest {
  uint32_t correlation_id;
  const struct MvMqttSubscription *subscriptions;
  uint32_t num_subscriptions;

The value of correlation_id is an application-defined ID that will be included in the response to enable you to match the response to the source request. Requests may be fulfilled in a non-deterministic order.

The subscriptions property takes a pointer to an array of MvMqttSubscription structures. Set the number of array elements as the num_subscriptions property. Each element is a structure:

struct MvMqttSubscription {
  struct MvSizedString topic;
  uint32_t desired_qos;
  uint32_t nl;
  uint32_t rap;
  uint32_t rh;

This structure’s properties are:

  • topic — A data structure comprising the topic name as bytes, which need not be nul-terminated, and the number of bytes.
  • desired_qos — The MQTT quality of service (QoS) setting (0 or 1; Microvisor does not support QoS level 2) you’d like to be applied.
  • nl — Your preferred MQTT no-local flag to request. MQTT v5 only.
  • rap — Your preferred MQTT retain-as-published flag. MQTT v5 only.
  • rh — Your preferred MQTT retain setting. MQTT v5 only.

Subscription request responses

Success or failure is indicated in the response. Its availability is signaled by a notification of type MV_MQTTREADABLEDATATYPE_SUBSCRIBERESPONSE. Call mvMqttReadSubscribeResponse() and pass in an MvMqttSubscribeResponse record which Microvisor will use to write back response data:

struct MvMqttSubscribeResponse {
  enum MvMqttRequestState *request_state;
  uint32_t *correlation_id;
  uint32_t *reason_codes;
  uint32_t reason_codes_size;
  uint32_t *reason_codes_len;

Provide values for reason_codes (a pointer to memory into which Microvisor will write an array of 32-bit MQTT reason codes, one for each topic included in the source request) and reason_codes_size (the number of codes the buffer can hold). When Microvisor writes the data back out, it will populate the memory addressed by reason_codes and store the number of records written in reason_codes_len. The order of reason codes in the array matches the order of topics in the source request — use correlation_id to match the response to is source subscription request. A reason code of zero indicates a successful subscription.

Process incoming messages

When your MQTT channel receives a notification of type MV_EVENTTYPE_CHANNELDATAREADABLE and an immediate call to mvMqttGetNextReadableDataType() returns MV_MQTTREADABLEDATATYPE_MESSAGERECEIVED, you should call mvMqttReceiveMessage() to ask Microvisor to pass the message to the application. This call takes the MQTT channel handle and a pointer to an MvMqttMessage structure:

struct MvMqttMessage {
  uint32_t *correlation_id;
  struct MvSizedStringBuffer topic;
  struct MvSizedStringBuffer payload;
  uint32_t *qos;
  uint8_t *retain;

The value of correlation_id is a pointer to memory into which Microvisor will write a value to help you match the message to the source subscription. The values of topic and payload are data structures which provide a buffer into which the incoming data — respectively the topic name and the message payload — will be written, the sizes of those buffers, and pointers to memory into which Microvisor will write the number of bytes actually written to the buffers.

The value of retain is a flag: a non-zero value indicates that the received message is the topic’s retained message.

Acknowledge messages

If the message received was delivered with MQTT QoS levels 1 or 2, you must acknowledge receipt. You can check the message’s QoS setting by reading the value of the MvMqttMessage record’s qos property. To acknowledge the message, you will also need the value of correlation_id: pass this value to mvMqttAcknowledgeMessage() after the MQTT channel’s handle.

Other actions

The actions covered so far — connect to a broker, publish messages, subscribe to topics, and handle incoming messages — are those you are most likely to perform in your code, but there are others. We won’t go into those in detail: you should view the relevant sections of the system calls documentation.

If you need to end a subscription — perhaps your application has received the data it needs — you can do so by calling mvMqttRequestUnsubscribe(). The outcome of the operation will be managed by calling mvMqttReadUnsubscribeResponse().

To break the connection with the broker cleanly, call mvMqttRequestDisconnect(). The results of the action will be accessible via mvMqttReadDisconnectResponse().