IoT Temperature and Humidity Sensor

This is a walkthrough for setting up a NodeMCU with a Si7021 temperature and humidity sensor as an IOT device.

It will collect sensor information, and continuosly report them to Grafana and MQTT.

The hardware

The specific hardware used in this case:

  • a NodeMCU Lolin v3 ESP8266 amazon
  • a Si7021 on an Adafruit Breakout board adafruit

Si7021 breakout

Si7021 breakout

NodeMCU (CC BY-SA 4.0 - Make: Magazin)

NodeMCU (CC BY-SA 4.0 - Make: Magazin)

Wiring

Connect

  • NodeMCU D1 to Si7021 SCL
  • NodeMCU D2 to Si7021 SDA
  • NodeMCU 3V to Si7021 VIN
  • NodeMCU GND to Si7021 GND
schematic

schematic

Get the sensor working

There are a dozen of libraries already available for this sensor (including one made by myself, but for the particle platform). I picked the Adafruit one, since the official lib to the board should be doing fine.

My code is divided into the main .ino file and an auxiliary config.h file to define user-specific configuration. This config.h file is thus not included in git, and needs to be set up manually (like in config.h.example)

The first steps to test out thing are basically:

  • define the PUBLISH_INTERVAL in config.h
  • include the sensor lib
  • initialize the sensor with .begin() in the setup() function (on this occasion, the lib could return false, indicating no sensor has been found)
  • after each PUBLISH_INTERVAL milli-seconds, the program now asks the sensor for current data points, and reports them via the serial port

main .ino

#include "config.h"
#include "Adafruit_Si7021.h"


Adafruit_Si7021 sensor = Adafruit_Si7021();
bool isSensorAvailable = true;
unsigned long lastPublish = 0;
float humidity, temperature;


void setup() {
  Serial.begin(115200);
  Serial.println("\n\nSi7021 test!");
  
  if (!sensor.begin()) {
    Serial.println("Did not find Si7021 sensor!");
    isSensorAvailable = false;
    return;
  }
}

void loop() {
  unsigned long ms = millis();
  
  if (!isSensorAvailable) {
    delay(1000);
    return;
  }

  if (ms - lastPublish < PUBLISH_INTERVAL) {
    return;
  }

  update();
  
  Serial.print("Humidity:    ");
  Serial.print(humidity, 2);
  Serial.print("\tTemperature: ");
  Serial.println(temperature, 2);
  delay(1000);
}


void update() {
  humidity = sensor.readHumidity();
  temperature = sensor.readTemperature();

  lastPublish = millis();
}

config.h

#ifndef _CONFIG_H_
#define _CONFIG_H_

#define PUBLISH_INTERVAL   60000

#endif

Flash this, and you should get continuos updates pushed via the Serial connection over the USB port:

serial output

serial output

Side Notes

The Si7021 would also support reading temperature and humidity in one measurement, as internally a humidity measurement always triggers a temperature measurement.

However, the Adafruit library does not support this functionality. In case you are interested in this, have a look at the specific example or the implementation in my own library.

Add OTA Update support

Always plugging, and unplugging that NodeMCU is quite annoying… After all this is a WiFi enabled chip, and it should be able to do OTA updates!

OTA is already available as a builtin functionality of the ESP8266 arduino Core (see https://arduino-esp8266.readthedocs.io/en/stable/ota_updates/readme.html)

So, all that is required is:

Include the required libraries

// for a normal WiFi connection
#include <WiFiClient.h>

// a generic webserver
#include <ESP8266WebServer.h>

// the OTA http update service
#include <ESP8266HTTPUpdateServer.h>

// a mDNS service for announcing the device on the network
#include <ESP8266mDNS.h>

Instantiate the required classes

// 80 is the port number - the default port for HTTP
ESP8266WebServer httpServer(80);
ESP8266HTTPUpdateServer httpUpdater;

Setup WiFi and OTA server

void setup() {
  setupOTA();

  // ...
}

void setupOTA() {
  // set WiFi mode to station mode
  WiFi.mode(WIFI_AP_STA);

  // set a device hostname
  // this is not required, but helps in recognizing the device on the network
  // MY_HOSTNAME is defined in config.h
  WiFi.hostname(MY_HOSTNAME);

  // actually connect to wifi
  // again, WIFI_SSID and WIFI_PASSWORD are defined in config.h
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

  while (WiFi.waitForConnectResult() != WL_CONNECTED) {
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  }

  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());

  MDNS.begin(MY_HOSTNAME);

  // initialize the web server & the OTA updater
  httpUpdater.setup(&httpServer, OTA_USERNAME, OTA_PASSWORD);
  httpServer.begin();

  // tell the devices on our network that we have a HTTP server here
  MDNS.addService("http", "tcp", 80);
}

// add necessary update function for OTA
void loop() {
  unsigned long ms = millis();

  httpServer.handleClient();
  MDNS.update();

  if (!isSensorAvailable) {
    delay(1000);
    return;
  }

  // ...
}
Side Notes

mDNS should not be required and could be removed from the code. It only aids in finding the device on the network.

The httpUpdater can also be set up without credentials (httpUpdater.setup(&httpServer)). This is insecure though and not recommended. Everyone having access to that network could basically change the firmware.

Using OTA

Open a web-browser and navigate to the IP address or the hostname of the device. Append /update to the URL (eg: http://192.168.2.20/update)

You should now see a simple form with file upload functionality. On this form, you can upload a compiled sketch binary (Arduino IDE -> Sketch -> Export compiled binary). On successful upload, the OTA functionality will flash the device and initiate a reset.

Software updates without USB connection!

An indicator for Wifi connection

For convenience, the onboard LED is used to indicate WiFi connection state. This can be done easily with the Ticker class (reference). The onboard LED is wired to D4

The following changes will make the LED blink while connecting to Wifi:

#include <Ticker.h>

// ...

void setup() {
  Ticker blink;

  // setup builtin LED GPIO
  pinMode(D4, OUTPUT);
  // turn LED ON
  digitalWrite(D4, HIGH);

  Serial.begin(115200);
  Serial.println("\n\nSi7021 test!");

  // rapid blinking while connecting and getting ready for OTA
  blink.attach(0.1, ledBlink);
  setupOTA();
  blink.detach();
  // turn LED OFF
  digitalWrite(D4, LOW);

  if (!sensor.begin()) {
    Serial.println("Did not find Si7021 sensor!");
    isSensorAvailable = false;
    return;
  }
}

Integrate to grafana with StatsD

There is no IoT yet in this whole thing..

I connected it to my grafana for visualizing. It visualizes data stored in InfluxDB. The data is processed by Telegraf, and sent there via the StatsD line protocol.

// STATSD_IP is defined in config.h
// it should be comma separated numbers like this
//  #define STATSD_IP 192, 168, 13, 37
IPAddress statsdIP(STATSD_IP);

void update() {
  WiFiUDP udp;
  char buffer[128];

  humidity = sensor.readHumidity();
  temperature = sensor.readTemperature();

  lastPublish = millis();

  // build a communication packet according to the Statsd protocol
  snprintf(
    buffer,
    sizeof(buffer),
    "home.temperature,room=livingroom:%.2f|g\nhome.humidity,room=livingroom:%.2f|g\n",
    temperature,
    humidity
   );

   // build the actual UDP packet - and send it to specific IP:port
   udp.beginPacket(statsdIP, STATSD_PORT);
   udp.write(buffer);
   udp.endPacket();
}

Given a working grafana - influxdb - telegraf setup, the data can now be easily graphed:

grafana visualization

grafana visualization

MQTT publishing

In addition to publishing data to StatsD, publishing to a MQTT server is helping in device-to-device communication and reacting to the sensor’s data.

For this, I’ll be using the PubSubClient from https://pubsubclient.knolleary.net/ It establishes the connection to a MQTT server, and periodically send messages in the update() method.

// add a new include for the PubSubClient
#include <PubSubClient.h>

WiFiClient espClient;
PubSubClient client(espClient);

void setup() {
  // ...

  // set MQTT server
  client.setServer(MQTT_HOST, 1883);
}

void loop() {

  // ...

  if (client.connected()) {
    // if client is still connected, call the loop() method to keep connection
    // and possibly receive messages
    client.loop();
  } else {
    // otherwise just try to connect again
    Serial.print("MQTT disconnected, re-connecting...");
    client.connect(MQTT_CLIENT_ID, MQTT_USER, MQTT_PASS);
  }

  if (ms - lastPublish < PUBLISH_INTERVAL) {
    return;
  }
  
  // ...
}

When subscribing to this specific MQTT topic (named home/air/livingroom in my case), there are now updates being pushed to all subscribed clients.

I formatted the messages in JSON format, since it is easily readable, but also parsers exists in virtually any language. The format can be completely arbitrary though. For example, you could just send the raw temperature value on a temperature topic.

MQTT messages

MQTT messages

NTP time sync

You may have noticed the time field in the MQTT messages above. It is supposed to be a way of knowing when the last measurement was done.

It is still 0 above, since the NodeMCU has no builtin way for getting time. The next step will therefore be adding a NTP client to fetch and keep time synced. The library used is here: https://github.com/arduino-libraries/NTPClient

https://www.ntppool.org/ provides a lot of NTP server pools to choose from.

#include <NTPClient.h>

WiFiUDP ntpUDP;

// NTP_SERVER is the chosen pool
// NTP_OFFSET is a timezone offset
// NTP_INTERVAL is the update interval of the client
NTPClient timeClient(ntpUDP, NTP_SERVER, NTP_OFFSET, NTP_INTERVAL);

void setup() {

  // ...

  // setup NTP client
  timeClient.begin();
}

void loop() {

  // ...

  // keep NTP time up-to-date - this may block for some seconds 
  // in case it does an update
  timeClient.update();

  // ...
}

void update() {

  // ...

  // publish MQTT message
  snprintf(
    buffer,
    sizeof(buffer),
    "{\"temperature\": %f, \"humidity\": %f, \"time\": %u}",
    temperature,
    humidity,
    timeClient.getEpochTime()
  );
  client.publish(MQTT_PUBLISH_TOPIC, buffer);
}

That’s all to get network synced time!

Side Notes

NTP time is delivered in UTC. In case local time is required, the offset parameter of the NTPClient constructor can be set to the timezone offset.

Source code

The complete source code is published on my github here

comments powered by Disqus