Building Low-Cost IoT with NodeMCU and Vertx MQTT

By Gerald Mücke | July 30, 2017

Building Low-Cost IoT with NodeMCU and Vertx MQTT

On this year’s JCrete conference I learned during the hackday about the NodeMCU board, which is an impressive low-cost alternative to an Arduino. It uses the 80 MHz ESP8266 chip that includes WiFi support out of the box. In this article I’d like to describe how to make a simple temperature and pressure sensor device that publishes data via MQTT to a handler implemented using the reactive Vert.x framework. The hardware for the solution costs no more than $15.

The ESP8266 can be programmed with LUA with the stock firmware or same as an Arduino using C and the Arduino IDE.

Solution Outline

The examples described in this article shows how to build a temperature and pressure sensor that pushes sensor data to a MQTT broker. The board is programmed with C++ using the Arduino IDE. The example broker is implemented in Java using Vert.x MQTT support and does only printout incoming messages, which should just serve as example.

The NodeMCU will be connected with a BMP280, which is a temperature and pressure sensor build by Bosch. In this example I use the Adafruit BMP280 board.

The board will connect via WiFi to the MQTT broker and publishes in regular intervals the sensor information using MQTT. The broker will listen to MQTT messages and prints them out.

Solution Outline

Pricing

The ESP8266 chip itself costs around $2, the NodeMCU development board can be bought for less than $5 node mcu price.

The Adafruit BMP280 costs around $10. Lower cost alternatives using the same sensor can be found as well, reducing the overall cost of the sensor hardware 5-10$.

Wiring

The NodeMCU’s 3V3 has to be connected with the VIN of the sensor, same for the GND pins. For using the default settings of both the NodeMCU and the BMP280 sensor, I connect the D1 pin with the SDI (which may named SDA on other sensor boards or specifications) and the D2 pin with the clock pin SCK (aka SCL).

NodeMCUBMP280
3V3VIN
GNDGND
D1SDI
D2SCK

breadboard layout.

Sensor Program

The sensor – built of the NodeMCU and the BMP280 – requires a program to collect measurements and publish them via MQTT. The following code adds the required libraries and sets parameters used during setup and execution. For your local environment you have to set the SSID of your Wireless LAN and the WPA/WPA2 password. In order to send data to the MQTT broker, you have to set the hostname or the IP address of the broker, too. In case your broker requires authentication, add these as well, although the example broker I describe later won’t require these. Finally, the MQTT topic defines where the NodeMCU publishes the sensor readings and potential subscribers can receive those.

 /**********************************************
 * DevCon5 GmbH, info@devcon5.ch 
 * Read temperature and pressure from BMP280
 * and publish via MQTT
 **********************************************/

#include <Wire.h>
#include <SPI.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BMP280.h>
#include <ESP8266WiFi.h>
#include <PubSubClient.h>

#define BMP_SCK 13
#define BMP_MISO 12
#define BMP_MOSI 11 
#define BMP_CS 10

/* Use just the following line if I2C wiring*/
Adafruit_BMP280 bme; // I2C

const char* ssid = "<wifi SSID here>";
const char* pass = "<wifi password here>";

// MQTT server params: server, port, user, password, and unique clientid
const char* mqtt_server = "<hostname or ip of mqtt broker here>";
const int   mqtt_port = 1883; 
const char* mqtt_user = "bmp280";
const char* mqtt_pass = "bmp280";
const char *mqtt_topic = "sensordata";
String mqtt_clientid = "";

WiFiClient espClient;
PubSubClient client(mqtt_server, mqtt_port, espClient);

During the setup we initialize the Serial port so that we can write debugging information on the serial connection. Use the Arduino IDE Serial Monitor to print out the messages.

Using the Wire library we configure which pins of the NodeMCU are used to communicate with the I2C bus of the BMP280 module. The first parameter is the pin for the data (D1 from SDI). The second parameter is the pin for the clock signal (D2 from SCK). After the BMP280 (as bme) is initialized, we setup the WiFi connection. The example code uses WPA/WPA2 with ssid and password as set in the header.

/**************************  
 *   S E T U P
 **************************/
void setup() {

  Serial.begin(9600);

  //set the input pins for SDA and SCL (I2C)
  Wire.begin(D1,D2);
  if (!bme.begin()) {  
    Serial.println("Could not find a valid BMP280 sensor, check wiring!");
    end();
  }
  setup_wifi();

  mqtt_clientid = String(ESP.getChipId());
}

// Connect to WiFi network
void setup_wifi() {
  delay(10);

  //check for presence of WiFi shield
  if(WiFi.status() == WL_NO_SHIELD){
     Serial.println("WiFi shield not present");
     end();
  }

  while(WiFi.status() != WL_CONNECTED ){
    Serial.print("Attempting to connect to SSID: ");
    Serial.println(ssid);
    //connect to via WPA/WPA2
    WiFi.begin(ssid,pass);
    //wait 10 seconds, then retry
    delay(10000); 
  }
  
  Serial.println();
  Serial.println("WiFi connected");  
  Serial.print("IP address: ");
  Serial.print(WiFi.localIP());
  Serial.print(" signal strength (RSSI):");
  Serial.print(WiFi.RSSI());
  Serial.println(" dBm");
}
void end(){
  while(1);
}

The loop is rather simple. First, the client’s connection state is checked and reconnected if needed. Afterwards, the payload is created using the sensor readings from the BMP280. The altitude reading I use here is based on a pressure at sea-level of 1013.25 hPa. This however may vary on actual weather conditions of your location. So the readings might not be exact. After payload has been created, it is published using the PubSubClient.

/**************************  
 *  L O O P
 **************************/
void loop() {

  if (!client.connected()) {
    reconnect();
  }
  client.loop();
  
  String payload = "{";
  
  payload += "\"temp\":";
  payload += bme.readTemperature();
  payload += ",\"tempUnit\":\"C\"";
  
  payload += ",\"pres\":";
  payload += bme.readPressure();
  payload += ",\"presUnit\":\"Pa\"";
  
  payload += ",\"alt\":";
  payload += bme.readAltitude(1013.25);
  payload += ",\"altUnit\":\"m\"";
  payload += ",\"altRefPres\":";
  payload += 1013.25;
  
  payload += "}";

  Serial.println(payload);
  if (client.publish(mqtt_topic, (char*) payload.c_str())) {
    Serial.println("Publish ok");
  } else {
    Serial.println("Publish failed");
  }
 
  // wait 5 seconds
  delay(5000); 
}

// Connect to MQTT broker
void reconnect() {
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    Serial.println(mqtt_clientid);
    
    if (client.connect(mqtt_clientid.c_str(), mqtt_user, mqtt_pass)) {
      Serial.println("MQTT connected");
    } 
    else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      // Wait 5 seconds before retrying
      delay(5000);
    }
  }
  delay(500);
}

MQTT Broker

For the broker I use the Vert.x framework which provides a low-footprint, event-driven non-blocking I/O implementation for MQTT servers and clients. So it may run adequately for a number of sensors on any custom hardware, including the Raspberry PI.

Add the vertx-mqtt Maven dependency to your pom.xml

<dependency>
    <groupId>io.vertx</groupId>
    <artifactId>vertx-mqtt-server</artifactId>
    <version>3.4.2</version>
</dependency>

The initialization of the Verticle consists of defining an endpoint handler, that is executed when a client connects and a call to the listen method which puts the MQTT server into operation accepting client connections.

The endpoint handler of this example simply prints out any incoming message. A full broker would handle subscription a message delivery of published messages.

/**********************************************
 * DevCon5 GmbH, info@devcon5.ch 
 * Vertx MQTT Broker
 **********************************************/
package ch.devcon5.mqtt;

import io.netty.handler.codec.mqtt.MqttQoS;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Future;
import io.vertx.core.Vertx;
import io.vertx.core.json.JsonObject;
import io.vertx.mqtt.MqttServer;

public class VertxMQTTServer extends AbstractVerticle {

    public static void main(String... args) {

        final Vertx vertx = Vertx.vertx();
        vertx.deployVerticle(new VertxMQTTServer());
    }

    @Override
    public void start(final Future<Void> startFuture) throws Exception {

        final MqttServer mqttServer = MqttServer.create(vertx);
        mqttServer.endpointHandler(endpoint -> {
            System.out.printf("MQTT client [%s] request to connect, clean session = %s %n",
                              endpoint.clientIdentifier(),
                              endpoint.isCleanSession());

            endpoint.publishHandler(message -> {
                final JsonObject msg = message.payload().toJsonObject();

                System.out.printf("Received message [%s] with QoS [%s]%n", msg, message.qosLevel());

                //TODO react to the message

                if (message.qosLevel() == MqttQoS.AT_LEAST_ONCE) {
                    endpoint.publishAcknowledge(message.messageId());
                } else if (message.qosLevel() == MqttQoS.EXACTLY_ONCE) {
                    endpoint.publishRelease(message.messageId());
                }
            }).publishReleaseHandler(endpoint::publishComplete);
            endpoint.accept(false);

        }).listen(ar -> {
            if (ar.succeeded()) {
                System.out.println("MQTT server is listening on port " + ar.result().actualPort());
                startFuture.complete();
            } else {
                System.out.println("Error on starting the server");
                startFuture.fail(ar.cause());
            }
        });
    }
}

The verticle can be deployed using the Vert.x commandline or executed directly as Java application.

comments powered by Disqus