In the previous section we covered some of the wireless basic, we should now have a good idea about working with Contiki. This section introduces two widely used protocols for the IoT: CoAP and MQTT. We will explain the basics and wrap up with ready to use examples.
The CoAP implementation in Contiki is based on Erbium (Er), a low-power REST Engine for Contiki. The REST Engine includes a comprehensive embedded CoAP implementation, which became the official one in Contiki.
More information about its implementation and author is available in the Erbium site.
The Representational State Transfer (REST) relies on a stateless, client-server, cacheable communications protocol - and in virtually all cases, the HTTP protocol can be used.
The key abstraction of a RESTful web service is the resource, not a service. Sensors, actuators and control systems in general can be elegantly represented as resources and their service exposed through a RESTful web service.
RESTful applications use HTTP-like requests to post data (create and/or update), read data (e.g., make queries), and delete data. Thus, REST uses HTTP for all four CRUD (Create/Read/Update/Delete) operations.
Despite being simple, REST is fully-featured; there’s basically nothing you can do in web services that can’t be done with a RESTful architecture. REST is not a standard.
The Constrained Application Protocol (CoAP) is a software intended to be used in very simple electronics devices that allows them to communicate interactively over the Internet. It is particularly targeted for small, low power sensors, switches, valves and similar components that need to be controlled or supervised remotely, through standard Internet networks. CoAP is an application layer protocol that is intended for use in resource-constrained internet devices, such as WSN nodes. CoAP is designed to easily translate to HTTP for simplified integration with the web, while also meeting specialized requirements such as multicast support, very low overhead and simplicity.
CoAP can run on most devices that support UDP. CoAP makes use of two message types, requests and responses, using a simple binary base header format. The base header may be followed by options in an optimized Type-Length-Value format. CoAP is by default bound to UDP and optionally to DTLS (Datagram Transport Layer Security), providing a high level of communications security.
Any bytes after the headers in the packet are considered the message body (if any is present). The length of the message body is implied by the datagram length. When bound to UDP the entire message MUST fit within a single datagram. When used with 6LoWPAN as defined in RFC 4944, messages should fit into a single IEEE 802.15.4 frame to minimize fragmentation.
The CoAP implementation in Contiki is located in apps/er-coap
. The Erbium REST engine is implemented in apps/rest-engine
.
The coap engine (currently the CoAP-18 version) is implemented in er-coap-engine.c
. The engine interface is provided by the following structure:
const struct rest_implementation coap_rest_implementation = {
coap_init_engine,
coap_set_service_callback,
coap_get_header_uri_path,
(...)
}
It is possible then to invoke the CoAP engine as follows:
REST.get_query_variable();
Web services are viewed as resources, and can be uniquely identified by their URLs. The basic REST design uses the HTTP or COAP protocol methods for typical CRUD
operations (create, read, update, delete):
-
POST: Create a resource
-
GET: Retrieve a resource
-
PUT: Update a resource
-
DELETE: Delete a resource
There are various resources that are available at the server. Each resource at the server has a handler function which the REST layer calls to serve the client’s request. The REST server sends the response back to the client with the contents of the resource requested.
The following macros are available in apps/rest-engine
, recommended when creating a new CoAP resource.
A normal resource is defined by a static Uri-Path that is associated with a resource handler function. This is the basis for all other resource types.
#define RESOURCE(name, attributes, get_handler, post_handler, put_handler, delete_handler) \
resource_t name = { NULL, NULL, NO_FLAGS, attributes, get_handler, post_handler, put_handler, delete_handler, { NULL } }
A parent resource manages several sub-resources by evaluating the Uri-Path, which may be longer than the parent resource.
#define PARENT_RESOURCE(name, attributes, get_handler, post_handler, put_handler, delete_handler) \
resource_t name = { NULL, NULL, HAS_SUB_RESOURCES, attributes, get_handler, post_handler, put_handler, delete_handler, { NULL } }
If the server is not able to respond immediately to a CON
request, it simply responds with an empty ACK message so that the client can stop re-transmitting the request. After a while, when the server is ready with the response, it sends the response as a CON
message. The following macro allows to create a CoAP resource with separate response:
#define SEPARATE_RESOURCE(name, attributes, get_handler, post_handler, put_handler, delete_handler, resume_handler) \
resource_t name = { NULL, NULL, IS_SEPARATE, attributes, get_handler, post_handler, put_handler, delete_handler, { .resume = resume_handler } }
An event resource is similar to a periodic resource, but the second handler is called by a non periodic event such as the pressing of a button.
#define EVENT_RESOURCE(name, attributes, get_handler, post_handler, put_handler, delete_handler, event_handler) \
resource_t name = { NULL, NULL, IS_OBSERVABLE, attributes, get_handler, post_handler, put_handler, delete_handler, { .trigger = event_handler } }
If we need to declare a periodic resource, for example to poll a sensor and publish a changed value to subscribed clients, then we should use:
#define PERIODIC_RESOURCE(name, attributes, get_handler, post_handler, put_handler, delete_handler, period, periodic_handler) \
periodic_resource_t periodic_##name; \
resource_t name = { NULL, NULL, IS_OBSERVABLE | IS_PERIODIC, attributes, get_handler, post_handler, put_handler, delete_handler, { .periodic = &periodic_##name } }; \
periodic_resource_t periodic_##name = { NULL, &name, period, { { 0 } }, periodic_handler };
Notice that the PERIODIC_RESOURCE
and EVENT_RESOURCE
can be observable, meaning a client can be notified of any change in a given resource.
Once we declare and implement the resources (we will get to that in the Hands On section), we need to initialize the REST framework and start the HTTP or CoAP process. This is done using:
void rest_init_engine(void);
Then for each declared resource we want to be accessible, we need to call:
void rest_activate_resource(resource_t *resource, char *path);
So assume we have created a hello-world
resource in res-hello.c
, declared as follows:
RESOURCE(res_hello,
"title=\"Hello world: ?len=0..\";rt=\"Text\"",
res_get_handler,
NULL,
NULL,
NULL);
To enable the resource we would do:
rest_activate_resource(&res_hello, "test/hello");
Which means the resource would be available at test/hello
uri-Path.
The function above stores the resources into a list. To list the available resources, the rest_get_resources
function is used. This will return a list with the resources with the following:
rest_get_resources();
Remember that the mandatory CoAP port is 5683
.
Now let us put the above to work in the following hands-on example.
The objective of the lesson are:
-
Learn how to create a CoAP resource and build a CoAP server
-
Show how to use the Copper client plugin to interact with our CoAP server
-
Learn how to discover the resources our CoAP server exposes
-
Learn how to use the
GET
method to retrieve sensor data from the CoAP server -
Subscribe to events (be notified each time the user button is pushed)
-
Request sensor readings in different formatting (application/json, text/plain, etc)
-
Control the on-board LEDs remotely using
PUT/POST
requests
You will need two Zolertia devices: a Border Router and a node acting as a CoAP server.
This example works for both Z1
nodes and zoul
platforms like the RE-Mote
, each one will expose its on-board sensors as a resource
.
Keep in mind the Z1
has lesser number of resources
enabled due to RAM constrains.
Warning
|
If you are using the Z1 motes, ensure that the motes you will be using to test this (border router, server, client) have been flashed with a node ID to generate the MAC/IPv6 addresses as done in previous sessions, be sure to write down the addresses! Another thing, if you get an error like the following, go to #error "UIP_CONF_BUFFER_SIZE too small for REST_MAX_CHUNK_SIZE"
make: *** [obj_z1/er-coap-07-engine.o] Error 1 |
Get the Copper (Cu) CoAP user-agent.
Copper is a generic browser for the Internet of Things based on the Constrained Application Protocol (CoAP), a user-friendly management tool for networked embedded devices. As it is integrated into web browsers, it allows an intuitive interaction at the presentation layer making it easier to debug existing CoAP devices.
More information available at Copper page
The CoAP example is located in 03-coap
, the objective is to show a CoAP server exposing its resources (on-board sensors, LEDs, button) to a CoAP client, in this case our Firefox browser using Copper.
In the Makefile
we can notice two things: the resources
folder is included as a project directory, and all the resources files are added in the compilation.
REST_RESOURCES_DIR = ./resources
REST_RESOURCES_FILES = $(notdir $(shell find $(REST_RESOURCES_DIR) -name '*.c' ! -name 'res-plugtest*'))
PROJECTDIRS += $(REST_RESOURCES_DIR)
PROJECT_SOURCEFILES += $(REST_RESOURCES_FILES)
We are including the er-coap
and rest-engine
applications.
# REST Engine shall use Erbium CoAP implementation
APPS += er-coap
APPS += rest-engine
Next, let us check the project-conf.h
relevant configuration. First we make sure TCP is disabled, as CoAP is based on UDP.
/* Disabling TCP on CoAP nodes. */
#undef UIP_CONF_TCP
#define UIP_CONF_TCP 0
The REST_MAX_CHUNK_SIZE
is the maximum buffer size that is provided for resource responses. Larger data should be handled by the resource and be sent in CoAP blocks. For the Z1 mote we reduce this number and also other configuration values to save RAM.
The COAP_MAX_OPEN_TRANSACTIONS
is the number of maximum open transactions the node is able to handle.
#if CONTIKI_TARGET_ZOUL
#define REST_MAX_CHUNK_SIZE 96
#else /* Default is Z1 */
/* Override this due to RAM overflow */
#undef NBR_TABLE_CONF_MAX_NEIGHBORS
#define NBR_TABLE_CONF_MAX_NEIGHBORS 5
#undef UIP_CONF_MAX_ROUTES
#define UIP_CONF_MAX_ROUTES 5
#undef COAP_MAX_OBSERVERS
#define COAP_MAX_OBSERVERS 3
#define REST_MAX_CHUNK_SIZE 48
#endif
/* Multiplies with chunk size, be aware of memory constraints. */
#undef COAP_MAX_OPEN_TRANSACTIONS
#define COAP_MAX_OPEN_TRANSACTIONS 4
/* Enable client-side support for COAP observe */
#define COAP_OBSERVE_CLIENT 1
Let us walk through the er-example-server.c
example and understand its implementation. The first thing we notice is the presence of a folder called resources
. To facilitate debugging and maintenance the resources are implemented in a different file.
The resources to be included in the CoAP server are defined in the following declaration:
/*
* Resources to be activated need to be imported through the extern keyword.
* The build system automatically compiles the resources in the corresponding
* sub-directory.
*/
extern resource_t
res_hello,
res_leds,
res_toggle,
#if CONTIKI_TARGET_ZOUL
res_mirror,
res_push,
res_sub,
res_sht25,
res_zoul,
#else /* Default is Z1 */
res_adxl345,
#endif
res_event,
res_separate;
Then the REST engine is initialized by calling the rest_init_engine()
, and the enabled resources are bound:
/* Initialize the REST engine. */
rest_init_engine();
/* Enable the sensors and devices */
SENSORS_ACTIVATE(button_sensor);
/*
* Bind the resources to their Uri-Path.
* WARNING: Activating twice only means alternate path, not two instances!
* All static variables are the same for each URI path.
*/
rest_activate_resource(&res_hello, "test/hello");
rest_activate_resource(&res_leds, "actuators/leds");
rest_activate_resource(&res_toggle, "actuators/toggle");
rest_activate_resource(&res_event, "sensors/button");
rest_activate_resource(&res_separate, "test/separate");
#if CONTIKI_TARGET_ZOUL
rest_activate_resource(&res_push, "test/push");
rest_activate_resource(&res_sub, "test/sub");
rest_activate_resource(&res_sht25, "sensors/sht25");
rest_activate_resource(&res_zoul, "sensors/zoul");
#else
rest_activate_resource(&res_adxl345, "sensors/adxl345");
#endif
Notice there are fewer resources enabled for the Z1 mote, as it has less RAM than the RE-Mote.
The RE-Mote
as a res_zoul
resource exposing the on-board sensors as shown in the next snippet. The three ADC channels and the core temperature sensor values are available in text/plain
, application/xml
and aplication/json
.
if(accept == -1 || accept == REST.type.TEXT_PLAIN) {
REST.set_header_content_type(response, REST.type.TEXT_PLAIN);
snprintf((char *)buffer, REST_MAX_CHUNK_SIZE, "Temp %d.%u; ADC1 %u ADC2 %u ADC3 %u",
temp / 1000, temp % 1000, adc1, adc2, adc3);
REST.set_response_payload(response, (uint8_t *)buffer, strlen((char *)buffer));
} else if(accept == REST.type.APPLICATION_XML) {
REST.set_header_content_type(response, REST.type.APPLICATION_XML);
snprintf((char *)buffer, REST_MAX_CHUNK_SIZE,
"<zoul><Temp>\"%d.%u\"</Temp><ADC1>\"%u\"</ADC1><ADC2>\"%u\"</ADC2><ADC3>\"%u\"</ADC3></zoul>",
temp / 1000, temp % 1000, adc1, adc2, adc3);
REST.set_response_payload(response, buffer, strlen((char *)buffer));
} else if(accept == REST.type.APPLICATION_JSON) {
REST.set_header_content_type(response, REST.type.APPLICATION_JSON);
snprintf((char *)buffer, REST_MAX_CHUNK_SIZE, "{'Zoul':{'Temp':%d.%u,'ADC1':%u,'ADC2':%u,'ADC3':%u}}",
temp / 1000, temp % 1000, adc1, adc2, adc3);
REST.set_response_payload(response, buffer, strlen((char *)buffer));
} else {
REST.set_response_status(response, REST.status.NOT_ACCEPTABLE);
const char *msg = "Supporting content-types text/plain, application/xml, and application/json";
REST.set_response_payload(response, msg, strlen(msg));
}
Now let us take a look at the res-hello.c
resource, which implements a "hello world" for testing.
As shown before resources are defined using the RESOURCE
macro, for this particular implementation we specify the resource name as res_hello
, the link-formatted attributes and the GET
callback handler. The POST
, PUT
, and DELETE
methods are not supported by this resource, so a NULL
parameter is used as argument.
RESOURCE(res_hello,
"title=\"Hello world: ?len=0..\";rt=\"Text\"",
res_get_handler,
NULL,
NULL,
NULL);
The res_get_handler
is the event callback for GET
requests:
static void
res_get_handler(void *request, void *response, uint8_t *buffer, uint16_t preferred_size, int32_t *offset)
{
const char *len = NULL;
/* Some data that has the length up to REST_MAX_CHUNK_SIZE. For more, see the chunk resource. */
char const *const message = "Hello World! ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxy";
int length = 12; (1)
/* The query string can be retrieved by rest_get_query() or parsed for its key-value pairs. */
if(REST.get_query_variable(request, "len", &len)) { (2)
length = atoi(len);
if(length < 0) { (3)
length = 0;
}
if(length > REST_MAX_CHUNK_SIZE) { (4)
length = REST_MAX_CHUNK_SIZE;
}
memcpy(buffer, message, length);
} else {
memcpy(buffer, message, length); (5)
/* text/plain is the default, hence this option could be omitted. */
} REST.set_header_content_type(response, REST.type.TEXT_PLAIN); (6)
REST.set_header_etag(response, (uint8_t *)&length, 1); (7)
REST.set_response_payload(response, buffer, length); (8)
}
-
The default lenght of the reply, in this case from the complete string, only
Hello World!
will be sent -
If the
len
option is specified, then a number oflen
bytes of themessage
string will be sent -
If the value is a negative one, send an empty string
-
If
len
is higher than the maximum allowed, then we only send the maximum lenght value -
Copy the default
-
Set the response content type as
Content-Type:text/plain
-
Attach the header to the response, set the payload lenght field
-
Attach the payload to the response
Now compile and upload the example:
make er-example-server.upload
Compile and program a Border Router device as shown in the previous chapter. Verify the Border Router is online by making a ping6
request to it, then browse the Border Router’s web service and also ping6
the CoAP server device.
If everything works then we can start discovering the server resources, open Firefox and type the server address:
Note
|
The next screenshoots shows images of both Z1 and RE-Mote CoAP servers, images are shown indistinctively. |
coap://[aaaa::c30c:0000:0000:0001]:5683/
Send a ping to the CoAP server by pressing the Ping
button:
Start discovering the available resources, press Discover
and the page will be populated in the left side
A well-known relative URI /.well-known/core
is defined as a default entry point for requesting the list of links about resources hosted by a server and thus performing CoRE Resource Discovery. This is quite useful as it makes easy to provision of a network by just "asking" the devices on the Border Router neighbor list information about their available resources, and the application-specific semantic type of a resource.
The SHT25
temperature and humidity sensor shown in previous sections can also be used, when connected it can provide readings in different formats, depending on the Accept
request field: text/plain
, application/xml
and application/json
.
The ADXL345
on-board the Z1 mote is also shown:
If you select the toggle
resource and use POST
you can see how the red LED of the CoAP server mote will toggle
To drive a specific LED (red, green or blue) on and off, in the URL type:
coap://[aaaa::c30c:0000:0000:0001]:5683/actuators/leds?color=g
Where g
corresponds to the green LED, b
and r
to the blue and red one respectively.
And then in the payload (the ongoing tab) write:
mode="on"
And press POST
or PUT
(hover with the mouse over the actuators/leds to see the description and methods allowed). Subsequently using off
will turn the LEDs off.
Tip
|
If click on the |
CLicking on the button
resource and pressing OBSERVE
will enable the client to receive notifications each time you press the user button
.
Launch the Sniffer and see the packets. A previously saved wireshark capture is available for study in the wireshark-coap-server-example.pcap
file.
MQTT (formerly MQ Telemetry Transport) is a publish-subscribe based messaging protocol on top of the TCP/IP protocol. It is designed for connections with remote locations where a "small code footprint" is required or where the network bandwidth is limited.
The publish-subscribe messaging pattern requires a message broker. The broker is responsible for distributing messages to interested clients based on the topic of a message. MQTT defines a set of Quality of Services (QoS) options.
Other features of MQTT are:
-
Keep-Alive message (PINGREQ, PINGRESP).
-
A broker can detect client disconnection, even without an explicit
DISCONNECT
message. -
“Last Will and Testament” message: specified in
CONNECT
message with topic, QoS and retain. On unexpected client disconnection, the “Last Will and Testament” message is sent to subscribed clients. -
“Retain” message: a
PUBLISH
message on a topic is kept on the broker which allows a new connected subscriber on the same topic to receive this message (last known good message). -
“Durable” subscription: on client disconnection, all subscriptions are kept on the broker and recovered on client reconnection.
MQTT reserves TCP/IP port 1883 and 8883, the later for using MQTT over SSL.
For more complete information visit the MQTT page.
A basic data exchange between clients (a publisher and a subscriber) over the MQTT broker is shown next. Whenever the Publisher (the source of the message) publishes data to a topic at the MQTT broker, all subscribers to the topic will receive the publication.
MQTT has defined three levels of Quality of Service (QoS):
-
QoS 0: The broker/client will deliver the message once, with no confirmation (fire and forget)
-
QoS 1: The broker/client will deliver the message at least once, with confirmation required.
-
QoS 3: The broker/client will deliver the message exactly once by using a four-step handshake.
A topic is a UTF-8
string case-sensitive, used by the broker to filter messages. A topic has one or more topic levels, separated by a forward slash (topic level separator). Topics starting with $
or $SYS
are treated specially as those are reserved by the MQTT broker internal statistics, and therefore not recommended for users to use to name their own topics. Publishing to MQTT broker reserved topics is forbidden.
MQTT is implemented in Contiki in apps/mqtt
. It makes use of the tcp-socket
library discussed in an earlier section.
The current MQTT version implemented in Contiki supports QoS levels 0 and 1.
The MQTT available functions are described next. A hands on example in the next section will help to clarify its use and suggest a state-machine approach to maintain the connection state and device operation.
This function initializes the MQTT engine and shall be called before any other MQTT function.
/**
* \brief Initializes the MQTT engine.
* \param conn A pointer to the MQTT connection.
* \param app_process A pointer to the application process handling the MQTT
* connection.
* \param client_id A pointer to the MQTT client ID.
* \param event_callback Callback function responsible for handling the
* callback from MQTT engine.
* \param max_segment_size The TCP segment size to use for this MQTT/TCP
* connection.
* \return MQTT_STATUS_OK or MQTT_STATUS_INVALID_ARGS_ERROR
*/
mqtt_status_t mqtt_register(struct mqtt_connection *conn,
struct process *app_process,
char *client_id,
mqtt_event_callback_t event_callback,
uint16_t max_segment_size);
This function connects to an MQTT broker.
/**
* \brief Connects to a MQTT broker.
* \param conn A pointer to the MQTT connection.
* \param host IP address of the broker to connect to.
* \param port Port of the broker to connect to, default is MQTT port is 1883.
* \param keep_alive Keep alive timer in seconds. Used by broker to handle
* client disc. Defines the maximum time interval between two messages
* from the client. Shall be min 1.5 x report interval.
* \return MQTT_STATUS_OK or an error status
*/
mqtt_status_t mqtt_connect(struct mqtt_connection *conn,
char *host,
uint16_t port,
uint16_t keep_alive);
This function disconnects from an MQTT broker.
/**
* \brief Disconnects from a MQTT broker.
* \param conn A pointer to the MQTT connection.
*/
void mqtt_disconnect(struct mqtt_connection *conn);
This function subscribes to a topic on an MQTT broker.
Warning
|
At the time of writting, the MQTT driver only supported subscribing to one topic. |
/**
* \brief Subscribes to a MQTT topic.
* \param conn A pointer to the MQTT connection.
* \param mid A pointer to message ID.
* \param topic A pointer to the topic to subscribe to.
* \param qos_level Quality Of Service level to use. Currently supports 0, 1.
* \return MQTT_STATUS_OK or some error status
*/
mqtt_status_t mqtt_subscribe(struct mqtt_connection *conn,
uint16_t *mid,
char *topic,
mqtt_qos_level_t qos_level);
This function unsubscribes from a topic on an MQTT broker.
/**
* \brief Unsubscribes from a MQTT topic.
* \param conn A pointer to the MQTT connection.
* \param mid A pointer to message ID.
* \param topic A pointer to the topic to unsubscribe from.
* \return MQTT_STATUS_OK or some error status
*/
mqtt_status_t mqtt_unsubscribe(struct mqtt_connection *conn,
uint16_t *mid,
char *topic);
This function publishes to a topic on an MQTT broker.
/**
* \brief Publish to a MQTT topic.
* \param conn A pointer to the MQTT connection.
* \param mid A pointer to message ID.
* \param topic A pointer to the topic to subscribe to.
* \param payload A pointer to the topic payload.
* \param payload_size Payload size.
* \param qos_level Quality Of Service level to use. Currently supports 0, 1.
* \param retain If the RETAIN flag is set to 1, in a PUBLISH Packet sent by a
* Client to a Server, the Server MUST store the Application Message
* and its QoS, so that it can be delivered to future subscribers whose
* subscriptions match its topic name
* \return MQTT_STATUS_OK or some error status
*/
mqtt_status_t mqtt_publish(struct mqtt_connection *conn,
uint16_t *mid,
char *topic,
uint8_t *payload,
uint32_t payload_size,
mqtt_qos_level_t qos_level,
mqtt_retain_t retain);
This function sets clients user name and password to use when connecting to an MQTT broker.
/**
* \brief Set the user name and password for a MQTT client.
* \param conn A pointer to the MQTT connection.
* \param username A pointer to the user name.
* \param password A pointer to the password.
*/
void mqtt_set_username_password(struct mqtt_connection *conn,
char *username,
char *password);
This function sets clients Last Will topic and message (payload). If the Will Flag is set to 1 (using the function) this indicates that, if the Connect request is accepted, a Will Message MUST be stored on the server and associated with the Network Connection. The Will Message MUST be published when the Network Connection is subsequently closed
This functionality can be used to get notified that a device has disconnected from the broker.
/**
* \brief Set the last will topic and message for a MQTT client.
* \param conn A pointer to the MQTT connection.
* \param topic A pointer to the Last Will topic.
* \param message A pointer to the Last Will message (payload).
* \param qos The desired QoS level.
*/
void mqtt_set_last_will(struct mqtt_connection *conn,
char *topic,
char *message,
mqtt_qos_level_t qos);
The following helper functions can be ussed to assert the MQTT connection status, to check if the mote is connected to the broker with mqtt_connected
, and with mqtt_ready
if the connection is estabished and there is space in the buffer to publish.
#define mqtt_connected(conn) \
((conn)->state == MQTT_CONN_STATE_CONNECTED_TO_BROKER ? 1 : 0)
#define mqtt_ready(conn) \
(!(conn)->out_queue_full && mqtt_connected((conn)))
The objective of the lessons are:
-
Familiarize with the MQTT API
-
Publish sensor data and node statistics to a MQTT broker
-
Control the on-board LEDs remotely by publishing to the MQTT broker and receiving the command notification
-
Implement a local MQTT broker (mosquitto), or publish to one on Internet
You will need a minimum of two Zolertia devices: a Border Router and a node acting as the MQTT client.
This example works for both Z1
nodes and zoul
platforms like the RE-Mote
, each one will publish its on-board sensors data.
Keep in mind the Z1
has lesser number of resources
enabled due to RAM constrains.
It is advisable to install mosquitto, on Ubuntu:
sudo apt-add-repository ppa:mosquitto-dev/mosquitto-ppa
sudo apt-get update
sudo apt-get install mosquitto mosquitto-clients
In case you don’t have an IPv6 connection, you can run a mosquitto MQTT broker in your host.
The 04-mqtt
folder contains the MQTT example.
The message content format is based on the IBM quickstart format. For more information about this check out the README
file, or read the IBM MQTT doc.
In the project-conf.h
file the IPv6 broker address is defined as follows:
/*
* If you have an IPv6 network or a NAT64-capable border-router:
* test.mosquitto.org
* IPv6 "2001:41d0:a:3a10::1"
* NAT64 address "::ffff:5577:53c2" (85.119.83.194)
*
* To test locally you can use a mosquitto local broker in your host and use
* i.e the fd00::1/64 address the Border router defaults to
*/
#define MQTT_DEMO_BROKER_IP_ADDR "fd00::1"
Notice there are three options:
-
Use the IPv6 address of the Mosquitto MQTT broker
-
Use the IPv4 address of the Mosquitto MQTT broker, if we have a NAT64-capable Border Router, or a NAT64 software like wrapsix (out of the scope of this book).
-
Use a mosquitto broker running in your host, in this case use the virtual interface address created with
tunslip6
, as shown in previous chapters.
/* Default configuration values */
#define DEFAULT_EVENT_TYPE_ID "status"
#define DEFAULT_SUBSCRIBE_CMD_TYPE "leds"
#define DEFAULT_BROKER_PORT 1883
#define DEFAULT_PUBLISH_INTERVAL (45 * CLOCK_SECOND)
#define DEFAULT_KEEP_ALIVE_TIMER 60
The DEFAULT_EVENT_TYPE_ID
allows to change the default zolertia/evt/status
last level. Likewise, the DEFAULT_SUBSCRIBE_CMD_TYPE
permits to change the default subscribe string topic.
The publication period (how often the sensor data is published) is given by DEFAULT_PUBLISH_INTERVAL
. Note the DEFAULT_KEEP_ALIVE_TIMER
should be always larger than DEFAULT_KEEP_ALIVE_TIMER
, at least 1.5 times.
The RE-Mote platform has a larger buffer to create topics and messages compared to the Z1, as the later has fewer RAM available and otherwise it would overflow when compiling.
/* Specific platform values */
#if CONTIKI_TARGET_ZOUL
#define BUFFER_SIZE 64
#define APP_BUFFER_SIZE 512
#else /* Default is Z1 */
#define BUFFER_SIZE 48
#define APP_BUFFER_SIZE 256
#define BOARD_STRING "Zolertia Z1 Node"
#undef NBR_TABLE_CONF_MAX_NEIGHBORS
#define NBR_TABLE_CONF_MAX_NEIGHBORS 3
#undef UIP_CONF_MAX_ROUTES
#define UIP_CONF_MAX_ROUTES 3
#endif
In the Makefile
the mqtt
driver is added:
APPS += mqtt
CONTIKI_WITH_IPV6 = 1
The data structure for the MQTT client configuration is declared as:
/**
* \brief Data structure declaration for the MQTT client configuration
*/
typedef struct mqtt_client_config {
char event_type_id[CONFIG_EVENT_TYPE_ID_LEN]; (1)
char broker_ip[CONFIG_IP_ADDR_STR_LEN]; (2)
char cmd_type[CONFIG_CMD_TYPE_LEN]; (3)
clock_time_t pub_interval; (4)
uint16_t broker_port; (5)
} mqtt_client_config_t;
-
Default event type
-
Broker IPv6 address
-
Default command type
-
Publish interval period
-
Broker default port, default is
1883
Later defined and populate as follows:
static mqtt_client_config_t conf;
static int
init_config()
{
/* Populate configuration with default values */
memset(&conf, 0, sizeof(mqtt_client_config_t));
memcpy(conf.event_type_id, DEFAULT_EVENT_TYPE_ID,
strlen(DEFAULT_EVENT_TYPE_ID));
memcpy(conf.broker_ip, broker_ip, strlen(broker_ip));
memcpy(conf.cmd_type, DEFAULT_SUBSCRIBE_CMD_TYPE, 4);
conf.broker_port = DEFAULT_BROKER_PORT;
conf.pub_interval = DEFAULT_PUBLISH_INTERVAL;
return 1;
}
The default values are set and now the topics and ID strings are going to be constructed by the update_config(…)
function. This function sequentially calls other helpers functions, such as construct_client_id(…)
and construct_pub_topic(…)
and create the corresponding strings, stored in the following buffers:
/*
* Buffers for Client ID and Topic.
* Make sure they are large enough to hold the entire respective string
*/
static char client_id[BUFFER_SIZE];
static char pub_topic[BUFFER_SIZE];
static char sub_topic[BUFFER_SIZE];
The mqtt_demo_process
starts as follows:
PROCESS_THREAD(mqtt_demo_process, ev, data)
{
PROCESS_BEGIN();
if(init_config() != 1) { (1)
PROCESS_EXIT();
}
update_config(); (2)
while(1) {
PROCESS_YIELD();
if((ev == PROCESS_EVENT_TIMER && data == &publish_periodic_timer) ||
ev == PROCESS_EVENT_POLL) { (3)
state_machine();
}
}
PROCESS_END();
}
-
Initial configuration values, as described earlier
-
Creates the client ID, publish and subscribe topics. The initial state
STATE_INIT
is set and thepublish_periodic_timer
event is scheduled -
Handles the
publish_periodic_timer
, this is where the application actually starts
The application example itself can be understood as a finite state machine, although it seems complicated it is actually very straightforward.
Basically the state_machine(…)
function sequentially handles the mqtt registration, configuration and connection to the Broker, then subscribe to the required topic. The code snippet below simplifies the state machine implementation to the basic taks done in each state:
static void
state_machine(void)
{
switch(state) {
case STATE_INIT: (1)
mqtt_register(&conn, &mqtt_demo_process, client_id, mqtt_event,
MAX_TCP_SEGMENT_SIZE);
state = STATE_REGISTERED;
case STATE_REGISTERED: (2)
if(uip_ds6_get_global(ADDR_PREFERRED) != NULL) {
/* Registered and with a public IP. Connect */
connect_to_broker();
}
etimer_set(&publish_periodic_timer, NET_CONNECT_PERIODIC);
return;
break;
case STATE_CONNECTING: (3)
break;
case STATE_CONNECTED: (4)
/* Notice there's no "break" here, it will continue to subscribe */
case STATE_PUBLISHING: (5)
if(mqtt_ready(&conn) && conn.out_buffer_sent) {
/* Connected. Publish */
if(state == STATE_CONNECTED) {
subscribe();
state = STATE_PUBLISHING;
} else {
publish();
}
etimer_set(&publish_periodic_timer, conf.pub_interval);
return;
}
break;
case STATE_DISCONNECTED: (6)
if(connect_attempt < RECONNECT_ATTEMPTS ||
RECONNECT_ATTEMPTS == RETRY_FOREVER) {
mqtt_disconnect(&conn);
etimer_set(&publish_periodic_timer, interval);
state = STATE_REGISTERED;
return;
}
break;
}
/* If we didn't return so far, reschedule ourselves */
etimer_set(&publish_periodic_timer, STATE_MACHINE_PERIODIC);
}
-
Entry point, register the mqtt connection and move to the
STATE_REGISTERED
event -
Attempts to connect to the broker. If the node has not joined the network (doesn’t have a valid IPv6 global address) it retries later. If the node has a valid address, then calls the
mqtt_connect
function and sets the state toSTATE_CONNECTING
, then sets thepublish_periodic_timer
with a faster pace -
This event just informs the user about the connection attempts. When the MQTT connection to the broker has been made, the
MQTT_EVENT_CONNECTED
is triggered at themqtt_event
callback handler -
As we are connected now, proceed and publish.
-
Checks if the MQTT connection is OK in
mqtt_ready
, then subscribe and publish -
Handles any disconnection event triggered from
MQTT_EVENT_DISCONNECTED
The mqtt_event
function is a callback handler in which we a notification from the mqtt library whenever an event happens. The mqtt_event
updates the status of the application so the state_machine
know what to do next.
static void
mqtt_event(struct mqtt_connection *m, mqtt_event_t event, void *data)
{
switch(event) {
case MQTT_EVENT_CONNECTED: { (1)
state = STATE_CONNECTED;
break;
}
case MQTT_EVENT_DISCONNECTED: { (2)
state = STATE_DISCONNECTED;
process_poll(&mqtt_demo_process);
break;
}
case MQTT_EVENT_PUBLISH: { (3)
pub_handler(msg_ptr->topic, strlen(msg_ptr->topic), msg_ptr->payload_chunk,
msg_ptr->payload_length);
break;
}
case MQTT_EVENT_SUBACK: { (4)
break;
}
case MQTT_EVENT_UNSUBACK: { (5)
break;
}
case MQTT_EVENT_PUBACK: { (6)
break;
}
}
-
Event notification of a successful connection to the Broker
-
Disconnection from the Broker event, immediately polls the
mqtt_demo_process
process and thestate_machine
function is invoked. -
A publication from a topic we are subscribed has been received
-
Successful subscription to a topic
-
Successful un-subscription to a topic
-
Sucessful publication to a topic
When receiving an event from a topic we are subscribed to, the MQTT_EVENT_PUBLISH
event is triggered and the pub_handler
is called. The example allows to turn the red LED on and off alternatively.
The default topic the example subscribe to is zolertia/cmd/leds
, specifically to change the LED status we would need to publish to the zolertia/cmd/leds
topic with value 1
to turn the LED on, and 0
otherwise.
static void
pub_handler(const char *topic, uint16_t topic_len, const uint8_t *chunk,
uint16_t chunk_len)
{
/* If we don't like the length, ignore */
if(topic_len != 17 || chunk_len != 1) {
printf("Incorrect topic or chunk len. Ignored\n");
return;
}
if(strncmp(&topic[13], "leds", 4) == 0) {
if(chunk[0] == '1') {
leds_on(LEDS_RED);
printf("Turning LED RED on!\n");
} else if(chunk[0] == '0') {
leds_off(LEDS_RED);
printf("Turning LED RED off!\n");
}
return;
}
}
Alternatively we could subscribe to a topic zolertia/cmd/#
and handle different types of commands, we would need to parse the topic
string and handle the topic and payload (chunk
string).
The publish
function create the string data to be published. Below is a snippet of the function highlighting only the most relevant parts.
static void
publish(void)
{
int len;
uint16_t aux;
int remaining = APP_BUFFER_SIZE; (1)
buf_ptr = app_buffer; (2)
len = snprintf(buf_ptr, remaining, (3)
"{"
"\"d\":{"
"\"myName\":\"%s\","
"\"Seq no\":%d,"
"\"Uptime (sec)\":%lu",
BOARD_STRING, seq_nr_value, clock_seconds());
if(len < 0 || len >= remaining) { (4)
printf("Buffer too short. Have %d, need %d + \\0\n", remaining, len);
return;
}
remaining -= len; (5)
buf_ptr += len; (6)
/* Put our Default route's string representation in a buffer */
char def_rt_str[64];
memset(def_rt_str, 0, sizeof(def_rt_str));
ipaddr_sprintf(def_rt_str, sizeof(def_rt_str), uip_ds6_defrt_choose());
len = snprintf(buf_ptr, remaining, ",\"Def Route\":\"%s\"",
def_rt_str); (7)
aux = cc2538_temp_sensor.value(CC2538_SENSORS_VALUE_TYPE_CONVERTED);
len = snprintf(buf_ptr, remaining, ",\"Core Temp\":\"%u.%02u\"",
aux / 1000, aux % 1000); (8)
aux = adc_zoul.value(ZOUL_SENSORS_ADC1);
len = snprintf(buf_ptr, remaining, ",\"ADC1\":\"%u\"", aux);
aux = adc_zoul.value(ZOUL_SENSORS_ADC3);
len = snprintf(buf_ptr, remaining, ",\"ADC3\":\"%u\"", aux);
len = snprintf(buf_ptr, remaining, "}}"); (9)
mqtt_publish(&conn, NULL, pub_topic, (uint8_t *)app_buffer,
strlen(app_buffer), MQTT_QOS_LEVEL_0, MQTT_RETAIN_OFF);
printf("APP - Publish to %s: %s\n", pub_topic, app_buffer);
}
-
We use the
remaining
variable to store the remaining bytes in the buffer in which we are storing the string to be published -
Use a pointer to the buffer address, so we move the pointer position each time we add a new sensor value thus appending a new string
-
The
snprintf
creates a formatted string at the position pointed by thebuf_otr
pointer. The JSON string follows IBMM quickstart format. -
If the return value of
snprintf
is negative, or exceeds the remaining bytes in our buffer, it means the buffer is full. If this is the case for you, either increase theAPPP_BUFFER_SIZE
value, or decrease the string content and lenght -
Update the available bytes counter
-
And move the
buf_ptr
pointer to the end of the newly created string, so the next string to be written starts at the end of the previous one. -
Store the IPv6 address of our next-hop neighbor. Steps 4-6 are omitted.
-
On-board sensors values are stored as string, only the
RE-Mote
sensors are shown but for theZ1
mote is also done in the application -
Close the JSON string
The mqtt_publish
updates the MQTT broker with the new values, publishing to the specified pub_topic
. The default topic is zolertia/evt/status
as done in the construct_pub_topic
function.
Now compile and upload the example:
make mqtt-example.upload
Compile and program a Border Router device as shown in the previous chapter. Verify the Border Router is online by making a ping6
request to it, then browse the Border Router’s web service and also ping6
the MQTT node.
Note
|
The next screenshoots shows images of both Z1 and RE-Mote MQTT nodes, images and snippets are shown indistinctively. |
The green LED will blink at a faster pace while the MQTT node is trying to connect to the MQTT broker, then when connected it will stop blinking.
Whenever the node publishes to the MQTT broker it will turn on the green LED, and off when it finishes publishing.
Rime started with address 193.12.0.0.0.0.19.208
MAC c1:0c:00:00:00:00:13:d0 Ref ID: 43981
Contiki-3.x-2577-gea0738b started. Node id is set to 5072.
CSMA nullrdc, channel check rate 128 Hz, radio channel 26
Tentative link-local IPv6 address fe80:0000:0000:0000:c30c:0000:0000:13d0
Starting 'MQTT Demo'
MQTT Demo Process
Subscription topic zolertia/cmd/leds
Init
Registered. Connect attempt 1
APP - MQTT Disconnect. Reason 3
Disconnected
Disconnected. Attempt 2 in 1024 ticks
Registered. Connect attempt 2
Connecting (2)
APP - Application has a MQTT connection
APP - Subscribing to zolertia/cmd/leds
APP - Application is subscribed to topic successfully
Publishing
APP - Publish to zolertia/evt/status: {"d":{"myName":"Zolertia Z1 Node","Seq no":1,"Uptime (sec)":63,"Def Route":"fe80::212:4b00:615:ab25","Temp":"2.768","X axis":"86"}}
There is a python script named mqtt-client.py
you could use to subscribe to the MQTT broker and topic, and receive notifications whenever the MQTT node publishes.
In case of a Z1 node publishing to mosquitto’s MQTT broker:
$ python mqtt-client.py
connecting to fd00::1
Connected with result code 0
Subscribed to zolertia/evt/status
Subscribed to zolertia/cmd/leds
zolertia/evt/status {"d":{"myName":"Zolertia Z1 Node","Seq no":1,"Uptime (sec)":63,"Def Route":"fe80::212:4b00:615:ab25","Temp":"27.68","X axis":"86"}}
For the RE-Mote:
$ python mqtt-client.py
connecting to fd00::1
Connected with result code 0
Subscribed to zolertia/evt/status
Subscribed to zolertia/cmd/leds
zolertia/evt/status {"d":{"myName":"Zolertia RE-Mote platform","Seq #":5,"Uptime (sec)":239,"Def Route":"fe80::212:4b00:615:ab25","Core Temp":"34.523","ADC1":"2280","ADC3":"1452"}}
As explained before, the MQTT node subscribes to the following topic:
zolertia/cmd/led
Sending as payload 1
will turn on the red LED, and a 0
off.
Execute this from the command line:
mosquitto_pub -h "test.mosquitto.org" -t "zolertia/cmd/led" -m "1" -q 0
The above command will publish to the cmd
topic, all nodes subscribed to it will turn its red LED on.
APP - Application received a publish on topic 'zolertia/cmd/leds'. Payload size is 1 bytes. Content:
Pub Handler: topic='zolertia/cmd/leds' (len=17), chunk_len=1
Turning LED RED on!
APP - Application received a publish on topic 'zolertia/cmd/leds'. Payload size is 1 bytes. Content:
Pub Handler: topic='zolertia/cmd/leds' (len=17), chunk_len=1
Turning LED RED off!
Tip
|
Remember in Chapter 4 a MQTT Android application was shown, try to configure and command your |
Ubidots is an IoT cloud platform that helps you create applications that capture real-world data and turn it into meaningful actions and insights.
The example will demonstrate the basic functionality of Contiki’s Ubidots library:
-
How to use the library to POST to a variable.
-
How to use the library to POST to a collection.
-
How to receive (parts of) the HTTP reply.
The original example and libraries were developed by George Oikonomou, available from George Oikonomou repository. At the time of writting it is not merged to Contiki, but available in the iot-workshop
branch we are currently using in the book.
The Ubidots example is located at:
examples/zolertia/tutorial/99-apps/ubidots-example
.
Ubidots application is implemented at apps/ubidots
.
You will need a minimum of two Zolertia devices: a Border Router and a node acting as the Ubidots client.
This example works for both Z1
nodes and zoul
platforms like the RE-Mote
, each one will publish data from an attached SHT21 temperature and humidity sensor, shown in previous sections.
Ubidots application uses TCP sockets to connect to the host things.ubidots.com
, which has the following IPv4 and IPv6 endpoints:
To check what’s going on enable the debug print statements in the ubidots.c
file, search for #define DEBUG DEBUG_NONE
and replace with:
#define DEBUG DEBUG_PRINT
First, you will have to register an account with Ubidots, create your data source and variables and request an authentication token.
-
Create an account with Ubidots and log in
-
Create a single data source
-
Under this data source, create two variables: One for the uptime and another for the sequence number.
-
Go to your account and request a short API token.
There are three things to configure in the example:
-
The choice between IP and IPv6. If you are planning to connect to Ubidots over NAT64, you will also want to configure the Ubidots server’s IP address in a NAT64 format.
-
The authentication token
-
The variable IDs
The example will build for IPv6 by default. If you have a NAT64 enabled Border Router or a similar software, open the example’s Makefile
. Change CONTIKI_WITH_IPV6=1
to WITH_IP64=1
.
In the project-conf.h
file configure the host address. If you don’t specify one, the Ubidots library will try to resolve the host name.
/* IPv6 address of things.ubidots.com is "2607:f0d0:2101:39::2", leave
* commented to resolve the host name. The NAT64 address is "::ffff:3217:7c44"
*/
#define UBIDOTS_CONF_REMOTE_HOST "2607:f0d0:2101:39::2"
Tip
|
If you don’t have a local IPv6 connection, services like gogo6 and hurricane electric provides IPv6 tunnels over IPv4 connections. Other options like wrapsix use NAT64 to translate IPv6/IPv4 addresses. |
Next get the API key from Ubidots:
This will be the value to be defined in UBIDOTS_CONF_AUTH_TOKEN
.
As we are to post temperature and humidity values to Ubidots, create the variables and copy their Variable ID
.
Next in the project-conf.h
file replace accordingly:
/* User configuration */
#define POST_PERIOD (CLOCK_SECOND * 40)
#define VARIABLE_BUF_LEN 16
#define UBIDOTS_CONF_AUTH_TOKEN ""
#define VARKEY_TEMPERATURE ""
#define VARKEY_HUMIDITY ""
#define UBIDOTS_CONF_IN_BUFFER_SIZE 64
Now compile and program:
make ubidots-client.upload
Compile and program a Border Router device as shown in the previous chapter. Verify the Border Router is online by making a ping6
request to it, then browse the Border Router’s web service and also ping6
the ubidots-client
node.
You should see the following output:
connecting to /dev/ttyUSB0 (115200) [OK]
Rime started with address 193.12.0.0.0.0.0.158
MAC c1:0c:00:00:00:00:00:9e Ref ID: 158
Contiki-d368451 started. Node id is set to 158.
nullmac nullrdc, channel check rate 128 Hz, radio channel 26
Tentative link-local IPv6 address fe80:0000:0000:0000:c30c:0000:0000:009e
Starting 'Ubidots demo process'
Ubidots client: STATE_ERROR_NO_NET
Ubidots client: STATE_ERROR_NO_NET
Ubidots client: STATE_ERROR_NO_NET
Ubidots client: STATE_STARTING
Ubidots client: Checking 64:ff9b::3217:7c44
Ubidots client: 'Host: [64:ff9b::3217:7c44]' (remaining 44)
Ubidots client: STATE_TCP_CONNECT (1)
Ubidots client: Connect 64:ff9b::3217:7c44 port 80
event_callback: connected
Ubidots client: STATE_TCP_CONNECTED
Ubidots client: Prepare POST: Buffer at 199
Ubidots client: Enqueue value: Buffer at 210
Ubidots client: POST: Buffer at 211, content-length 13 (2), at 143
Ubidots client: POST: Buffer at 208
Ubidots client: STATE_POSTING (176)
Ubidots client: STATE_POSTING (176)
Ubidots client: STATE_POSTING (144)
Ubidots client: STATE_POSTING (112)
Ubidots client: STATE_POSTING (80)
Ubidots client: STATE_POSTING (48)
Ubidots client: STATE_POSTING (16)
Ubidots client: STATE_POSTING (0)
Ubidots client: HTTP Reply 200
HTTP Status: 200
Ubidots client: New header: <Server: nginx>
Ubidots client: New header: <Date: Fri, 13 Mar 2015 09:35:08 GMT>
Ubidots client: New header: <Content-Type: application/json>
Ubidots client: New header: <Transfer-Encoding: chunked>
Ubidots client: New header: <Connection: keep-alive>
Ubidots client: New header: <Vary: Accept-Encoding>
Ubidots client: Client wants header 'Vary'
H: 'Vary: Accept-Encoding'
Ubidots client: New header: <Vary: Accept>
Ubidots client: Client wants header 'Vary'
H: 'Vary: Accept'
Ubidots client: New header: <Allow: GET, POST, HEAD, OPTIONS>
Ubidots client: Chunk, len 22: <[{"status_code": 201}]> (counter = 22)
Ubidots client: Chunk, len 0: <(End of Reply)> (Payload Length 22 bytes)
P: '[{"status_code": 201}]'
The above shows how the ubidots-client
node connects to the Ubidots server, and publishes data.
To visualize the data in a friendlier way, Ubidots provides ready to use dashboards with different visualization options (linear plot, scatter, tables, etc).