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.
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:
1// Set up the MQTT channel's send and receive buffers2static volatile uint8_t mqtt_channel_rx_buffer[1536] __attribute__((aligned(512)));3static volatile uint8_t mqtt_channel_tx_buffer[1536] __attribute__((aligned(512)));
Issuing an MQTT request is a two-stage process:
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_CERTIFICATEEXPIRED | Yes | A certificate is out of date |
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.
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:
1struct MvMqttConnectRequest {2enum MvMqttProtocolVersion protocol_version;3struct mvSizedString host;4uint16_t port;5struct mvSizedString clientid;6struct MvMqttAuthentication *authentication;7struct MvTlsCredentials *tls_credentials;8uint32_t keepalive;9uint8_t clean_start;10struct MvMqttWill *will;11}
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:
1struct MvMqttWill {2struct MvSizedString topic;3struct MvSizedString payload;4uint32_t qos;5uint8_t retain6}
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 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.
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
:
1struct MvMqttAuthentication {2enum MvMqttAuthenticationMethod method;3struct MvMqttUsernamePassword *username_password;4}
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:
1struct MvMqttUsernamePassword {2struct mvSizedString username;3struct mvSizedString password;4}
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
:
1struct MvTlsCredentials {2struct MvTlsCertificateChain cacert;3struct MvOwnTlsCertificateChain clientcert;4}56struct MvTlsCertificateChain {7uint32_t num_certs;8struct MvSizedString *certs;9}1011struct MvOwnTlsCertificateChain {12struct TlsCertificateChain chain;13struct MvSizedString key;14}
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.
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:
1struct MvMqttConnectResponse {2enum MvMqttRequestState request_state;3uint32_t reason_code;4}
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.
Call mvMqttRequestPublish()
to post a message to a topic. This call takes the MQTT channel's handle and a pointer to an MvMqttPublishRequest
structure:
1struct MvMqttPublishRequest {2uint32_t correlation_id;3struct MvSizedString topic;4struct MvSizedString payload;5uint32_t payload_len;6uint32_t desired_qos;7uint32_t retain;8}
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.
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:
1struct MvMqttPublishResponse {2enum MvMqttRequestState request_state;3uint32_t correlation_id;4uint32_t reason_code;5}
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.
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:
1struct MvMqttSubscribeRequest {2uint32_t correlation_id;3const struct MvMqttSubscription *subscriptions;4uint32_t num_subscriptions;5}
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:
1struct MvMqttSubscription {2struct MvSizedString topic;3uint32_t desired_qos;4uint32_t nl;5uint32_t rap;6uint32_t rh;7}
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.
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:
1struct MvMqttSubscribeResponse {2enum MvMqttRequestState *request_state;3uint32_t *correlation_id;4uint32_t *reason_codes;5uint32_t reason_codes_size;6uint32_t *reason_codes_len;7}
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.
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:
1struct MvMqttMessage {2uint32_t *correlation_id;3struct MvSizedStringBuffer topic;4struct MvSizedStringBuffer payload;5uint32_t *qos;6uint8_t *retain;7}
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.
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.
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()
.