Showing posts with label Arduino. Show all posts
Showing posts with label Arduino. Show all posts

Friday, November 24, 2017

Publishing Sensor Data to MQTT

When publishing sensor data to MQTT, two question to ask are: how often is the data published?, and with what distribution?

Here are two examples:

  • Publish temperature, humidity, and barometric pressure from one sensor every second
  • Publish and verify RFID card swipes that employees use when entering a building.

In the first example, we have a good idea of the frequency of the data as well as how that data is distributed: data is sent once every second and it is uniformly distributed - in essence, it is time series data. In the second example, the frequency of data transmission depends on the time of day. Just before the business day starts, there will be a large number of RFID card swipes as the employees "clock in", then for the rest of the day the number of RFID events will be far lower.

The answers to those two questions will determine various aspects of the backend IoT architecture: the database that should be used to store and otherwise interact with this sensor data, the "analytics pipeline" (my term, ©2017, Patriot Geek) used to analyze and visualize that data, etc.

For this blog post, we will publish time series data using our old friends, the BME280 and the ESP8266! Please see this earlier blog post for wiring and importing the proper libraries:

http://patriot-geek.blogspot.com/2017/03/nodemcu-and-digital-sensors.html

Temperature, humidity, and barometric pressure will be published once every second to a MQTT broker. Compared to industrial IoT applications, this is not a lot of data! We can thus use a database like MongoDB or even MySQL for storage. The analytics pipeline will be discussed in a later post.

The next question to ask is: how should that data be formatted? One solution is to use a JSON format:

{
    "temperature": 71.55,
    "humidity": 41.46,
    "barometric-pressure": 29.73
}

An alternative is to use a CSV format like this:

71.55,41.46,29.73

The JSON format contains a human-readable description of those three values, whereas the CSV format contains considerably fewer characters. Since MQTT is all about short messages, we will use the CSV format. This will also serve us well should we want to use LoRa to send the same information. If for any reason we want to reformat the CSV into JSON, we can do so later.

Here is the code that will have our ESP8266 publish this data:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
/**
 * BME280-MQTT.js
 *
 * By: Mike Klepper
 * Date: 24 November 2017
 *
 * This program demonstrates how to publish to MQTT
 *
 * See blog post on patriot-geek.blogspot.com
 * for instructions.
 */


#include "ESP8266WiFi.h"
#include "PubSubClient.h"
#include "Wire.h"
#include "Adafruit_Sensor.h"
#include "Adafruit_BME280.h"

WiFiClient wifiClient;
PubSubClient mqttClient(wifiClient);
Adafruit_BME280 bme;

const char* SSID                  = "**********";
const char* PASSWORD              = "**********";

const char* MQTT_BROKER           = "192.168.1.11";
const int   MQTT_PORT             = 1883;
const char* MQTT_TOPIC            = "home/living-room";
const char* MQTT_CLIENT_NAME      = "ESP8266Client";

const int   BME280_DELAY          = 3000;
const int   RECHECK_INTERVAL      = 500;
const int   PUBLISH_INTERVAL      = 1000;

long        lastReconnectAttempt  = 0;
long        lastPublishAttempt    = 0;


void setup() 
{
  Serial.begin(115200);
  delay(10);

  setupWifi();
  setupBME280();
  setupMQTT();
}

void loop() 
{
  long now = millis();
  
  if(!mqttClient.connected()) 
  {
    if(now - lastReconnectAttempt > RECHECK_INTERVAL) 
    {
      lastReconnectAttempt = now;
      mqttClient.connect(MQTT_CLIENT_NAME);

      if(mqttClient.connected())
      {
        // Resubscribe to any topics, if necessary
        // This is also a good place to publish an error to a separate topic!
      }
    }
  }
  else
  {
    if(now - lastPublishAttempt > PUBLISH_INTERVAL)
    {
      lastPublishAttempt = now;
      readAndPublishData();
      mqttClient.loop();
    }
  }
}


void setupWifi()
{
  Serial.println("");
  Serial.print("Connecting to ");
  Serial.print(SSID);

  WiFi.begin(SSID, PASSWORD);

  while(WiFi.status() != WL_CONNECTED) 
  {
    delay(RECHECK_INTERVAL);
    Serial.print(".");
    yield();
  }

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

void setupBME280()
{
  Serial.println("");
  Serial.print("Setting up BME280");

  while(!bme.begin())
  {
    delay(RECHECK_INTERVAL);
    Serial.print(".");
    yield();
  }
  
  Serial.println("");
  Serial.println("BME280 found!");
  delay(BME280_DELAY);
}

void setupMQTT()
{
  mqttClient.setServer(MQTT_BROKER, MQTT_PORT);
  
  Serial.println("");
  Serial.print("Connecting to MQTT");
  
  while(!mqttClient.connected())
  {
    Serial.print(".");
    mqttClient.connect(MQTT_CLIENT_NAME);
    delay(RECHECK_INTERVAL);
  }
  
  Serial.println("");
  Serial.println("Connected!");
  Serial.println("");
}

void readAndPublishData()
{
  if(mqttClient.connected())
  {
    float tempC = bme.readTemperature();
    float tempF = 9.0/5.0 * tempC + 32.0;
    float humidity = bme.readHumidity();
    float pressurePascals = bme.readPressure();
    float pressureInchesOfMercury = 0.000295299830714 * pressurePascals;
  
    String msg = String(tempF) + "," + String(humidity) + "," + String(pressureInchesOfMercury);
    char msgAsCharAway[msg.length()];
    msg.toCharArray(msgAsCharAway, msg.length());
  
    Serial.print(msg);
    mqttClient.publish(MQTT_TOPIC, msgAsCharAway);
    Serial.println(" - sent");
  }
}

Here's a walkthrough of the code. The functions for initializing the wifi and the BME280 are the same as in previous posts; the new stuff is how we publish to MQTT, and how we time those publications.

14 - 18 The various libraries we'll be using
24 - 25 Name and password for wifi network
45 - 47 Connect to wifi, start the BME280, and connect to MQTT broker
120 Initialize the MQTT client, specifying the broker's IP address and port
125 While we're not connected...
128 ...attempt to connect
139 If the MQTT client is connected...
141 - 145 ...read data from the BME280 and convert to British units...
147 - 149 ...format the data as CSV, and prepare it for publication...
152 ...and publish it to the broker!
50 - 77 This loop function is based on the non-blocking reconnect example from the PubSubClient library
52 ...get the number of milliseconds since this sketch started running...
54 If we're not connected to the broker...
56 ...check to see how long it has been since we last tried reconnecting; if it has been more than a specified time...
58 ...reset the timer...
59 ...and try to reconnect
61 If we were successful...
63 - 64 ...perform any housekeeping activities, like reporting an error
68 Otherwise we're connected...
70 ...check to see how long it has been since we last published data; if sufficient time has passed...
72 ...reset the timer...
73 ...publish the data...
74 ...and process any incoming messages.

To test the application, first open a terminal window and start MQTT broker with the command:

mosquitto

Open another terminal window and start a MQTT client and subscribe to the topic home/living-room:

mosquitto_sub -h localhost -t home/living-room

Finally, watch the incoming data!

Some notes about this application:

  1. Data is transmitted in an insecure manner!
  2. A timestamp is not included in the data.
These two issues will be addressed in a later blog post.

Monday, April 17, 2017

Exposing Data as a REST API

How to expose the temperature, pressure, and humidity data over WiFi? There are two subquestions here:

  1. What network mechanism to use?
  2. What request and response formats to use?

For the network mechanism, we will create a small web server on the network that will listen for REST requests and return the data formatted as JSON. In other words, we will finally be using the ESP8266's WiFi capabilities! YAY!!

What should the request URIs for our REST API look like? Ask ten developers this question, and be prepared to get fifteen answers! The only thing they'll agree upon is that we will only be making GET requests, since we can only read data from a sensor.

We will structure the request URLs as follows:

/home/living-room/temperature
/home/living-room/humidity
/home/living-room/barometric-pressure
/home/living-room/
/home/living-room

This last two requests will return all data that is available from the BME280 sensor; both are included so as to demonstrate using one handler for both URIs. We will also handle 404 and 503 errors.

We will return the data as JSON, but how exactly should that JSON should be formatted? Put ten developers in a room, ask them that question, and expect only two developers to leave that room alive!

There are two extremes: return the minimum amount of requested data, or return all that data plus a ton of metadata, resource links, etc. This latter approach will not only require us to add a significant number of characters to the JSON, but will also require additional URIs. This is called HATEOAS.

We will take the minimal approach here. Further, we will always return data in British units. For example, in response to a GET request to home/living-room/temperature, we will return:


{
    "temperature": 86.56
}


The Code
The following code will connect to the local network, and create a web server listening on port 80:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
/**
 * BME280-WebServer-JSON
 * 
 * By: Mike Klepper
 * Date: 17 April 2017
 * 
 * This program exposes BME280 data as a REST API.
 * 
 * See blog post on patriot-geek.blogspot.com
 * for instructions.
 */

#include "ESP8266WiFi.h"
#include "WiFiClient.h"
#include "ESP8266WebServer.h"
#include "Adafruit_Sensor.h"
#include "Adafruit_BME280.h"

const char* SSID = "**********";
const char* PASSWORD = "**********";

const int DELAY = 3000;
const int STARTUP_DELAY = 500;

const char* MESSAGE_404 = "404 Not Found";
const char* MESSAGE_503 = "503 Service Unavailable";

boolean sensorAvailable;

ESP8266WebServer server(80);
Adafruit_BME280 bme;

void setup(void)
{
  Serial.begin(115200);

  // Start the BME280 sensor
  if(!bme.begin())
  {
    Serial.println("Could not find a valid BME280 sensor, check wiring!");
    sensorAvailable = false;
  }
  else
  {
    sensorAvailable = true;
    delay(STARTUP_DELAY);
  }

  WiFi.begin(SSID, PASSWORD);

  // Wait for the connection
  while(WiFi.status() != WL_CONNECTED) 
  {
    delay(STARTUP_DELAY);
    Serial.print(".");
  }
  
  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(SSID);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());

  server.on("/home/living-room/temperature", returnTemperature);
  server.on("/home/living-room/humidity", returnHumidity);
  server.on("/home/living-room/barometric-pressure", returnPressure);
  server.on("/home/living-room/", returnAll);
  server.on("/home/living-room", returnAll);
  
  server.onNotFound(return404Error);

  server.begin();
  Serial.println("HTTP server started");
}

void loop(void)
{
  server.handleClient();
  yield();
}

void return404Error()
{
  server.send(404, "text/plain", MESSAGE_404);
}

void return503Error()
{
  server.send(500, "text/plain", MESSAGE_503);
}

void returnTemperature() 
{
  if(sensorAvailable)
  {
    // Get temperature and format it in degree Fahrenheit
    float tempC = bme.readTemperature();
    float tempF = 9.0/5.0 * tempC + 32.0;
  
    // Build JSON response
    String responseJson = "{\n\"temperature\":" + String(tempF) + "}";
  
    // Return JSON with correct MIME type
    server.send(200, "application/json", responseJson);
  }
  else
  {
    return503Error();
  }
}

void returnHumidity() 
{
  if(sensorAvailable)
  {
    // Get humidity
    float humidity = bme.readHumidity();
  
    // Build JSON response
    String responseJson = "{\n\"humidity\":" + String(humidity) + "}";
  
    // Return JSON with correct MIME type
    server.send(200, "application/json", responseJson);
  }
  else
  {
    return503Error();
  }
}

void returnPressure() 
{
  if(sensorAvailable)
  {
    // Get pressure and change units to inHg
    float pressurePascals = bme.readPressure();
    float pressureInchesOfMercury = 0.000295299830714 * pressurePascals;
  
    // Build JSON response
    String responseJson = "{\n\"barometric-pressure\":" + String(pressureInchesOfMercury) + "}";
  
    // Return JSON with correct MIME type
    server.send(200, "application/json", responseJson);
  }
  else
  {
    return503Error();
  }
}

void returnAll()
{
  if(sensorAvailable)
  {
    // Get values
    float tempC = bme.readTemperature();
    float humidity = bme.readHumidity();
    float pressurePascals = bme.readPressure();
  
    // Convert to British units
    float tempF = 9.0/5.0 * tempC + 32.0;
    float pressureInchesOfMercury = 0.000295299830714 * pressurePascals;
  
    // Build JSON response
    String responseJson = "";
    responseJson += "{";
    responseJson +=     "\"temperature\":" + String(tempF) + ",";
    responseJson +=     "\"humidity\":" + String(humidity) + ",";
    responseJson +=     "\"barometric-pressure\":" + String(pressureInchesOfMercury);
    responseJson += "}";
  
    // Return JSON with correct MIME type
    server.send(200, "application/json", responseJson);
  }
  else
  {
    return503Error();
  }
}

Running the Code
When this code is ran, the IP address that the network assigns to the ESP8266 is reported in the serial monitor. For example, my IP address is (currently) 10.200.206.75. To test this code, then, start a browser or other HTTP client and go to the URL made of that IP address together with one of the paths. For example when I visit http://10.200.206.75/home/living-room, the result is:

What happened to the quotes? Some browser extensions, like JSONView for Chrome, suppresses the quotes and adds tabs. If this extension is disabled, the result looks like this:


Code Explanation

13 - 15Include libraries for working with WiFi as well as creating a web server on an ESP8266
16 - 17Include the libraries for using the BME280 sensor
19 - 20Specify the network name (SSID) and password for the network we're connecting to. Everybody's password is "**********", no?
25 - 26Messages for 404 and 503 errors
38 - 47Initialize the BME280 sensor; set sensorAvailable to the appropriate value
49Connect to the network
52 - 56Print dots to the serial monitor until we're connected
58 - 62Print connection info to the serial monitor
64 - 66Specify the paths used to request single sensor readings (temperature only, for example), and associate a handler for each
67 - 68Associate the same handler - returnAll() - for two different paths
70Have web server call return404Error() whenever a URI handler is not specified
72Start the web server!
76 - 80Handle incoming requests, repeatedly
82 - 85Whenever a path hasn't been found, send HTTP error code 404, using MIME type "text/plain"
87 - 90For internal errors (like when the BME280 isn't available), send HTTP error code 503, using MIME type "text/plain"
92 - 110Callback for handling /home/living-room/temperature requests
94If the sensor is available at startup...
96 - 98Read the temperature from the BME280 and convert to Fahrenheit
101Wrap the temperature inside a JSON object
104Return that JSON using MIME type "application/json" and response code 200
106 - 109If the sensor is NOT available, return 503.
112 - 129Callback for handling /home/living-room/humidity requests
131 - 149Callback for handling /home/living-room/barometric-pressure requests
151 - 179Callback for handling /home/living-room/ and /home/living-room requests

Notes:

  1. The problem with setting sensorAvailable at the start is: what happens if the BME280 becomes disconnected later? This can be handled by performing the check for bme.begin() in each of the response handlers.
  2. To connect to a wifi network that doesn't use a password, change line 50 to read:
    WiFi.begin(SSID);
  3. Change lines 26 and 27 to give more interesting error messages!

Friday, March 31, 2017

ESP8266, BME280 and OLED Displays

Now that we've successfully read temperature, humidity, and air pressure from a BME280, we turn to the problem of making this data available without having to use a serial monitor. Our first solution is to use a tiny OLED display.

In this tutorial we will:

  1. Install the Correct Library
  2. Add the Display to the Breadboard
  3. Test the Display
  4. Show Sensor Data in the Display


Install the Correct Library
A wide variety of tiny OLED displays are available, and for this tutorial we'll be using a 0.96 inch, monochrome, 128 x 64 pixel screen that is driven by the SSD1306, and that has an I2C interface. As it goes, this device has I2C address 0x3c. There are several libraries for the SSD1306, and we will use the one entitled "ESP8266 and ESP32 Oled Driver for SSD1306 display by Daniel Eichhorn, Fabrice Weinberg". To install this library:

  1. In the Arduino IDE, choose the menu Sketch | Include Library | Manage Libraries...
  2. Enter SSD1306, click the right one, and install the latest version of that library (currently version 3.2.7).


Add the Display to the Breadboard
Along the top of the display there will be four pins that read something like

  • GND, VCC, SCL, and SDA
  • or GND, VDD, SCK, and SDA

First disconnect the BME280, then wire the display as follows:

  • ESP8266 <--> OLED
  • 3V3 <--> VCC
  • GND <--> GND
  • SCL <--> D5
  • SDA <--> D6

If there isn't room on the breadboard to add the display, either use a full-size breadboard or use a second half-sized breadboard!


Test the Display
Ignoring the BME280 for a minute, the following sketch tests the features we will be using when we display data from the BME280.

The library works as follows:

  1. We initialize it in the setup() function, and we flip the screen vertically. The end result is to create a memory buffer
  2. Drawing commands will be written to this buffer - they will NOT be immediately visible
  3. When we are ready, write the buffer to the display using the command:
    display.display();

We will be using the following commands in the final sketch:

  • display.setFont(fontName);
  • display.drawString(x, y, message);
  • display.display();
  • display.clear();
There are many other commands in this library - check out the sample code!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/*
 * Simple-OLED-Test
 * 
 * By: Mike Klepper
 * Date: 29 March 2017
 * 
 * This program demonstrates a VERY few simple commands from the
 * "ESP8266 Oled driver library for SSD1306 display" library 
 * by Daniel Eichhorn, Fabrice Weinberg.
 * 
 * Connections:
 * Display VCC --> NodeMCU 3V3
 * Display GND --> NodeMCU GND
 * Display SCL --> NodeMCU D5
 * Display SDA --> NodeMCU D6
 */

#include "SSD1306.h"

SSD1306 display(0x3c, D6, D5);

void setup() 
{
  display.init();
  display.flipScreenVertically();
}

void loop() 
{
  display.clear();
  
  display.drawRect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT);
  
  display.setFont(ArialMT_Plain_16);
  display.drawString(20, 7, "Hello, world!");
  
  display.setFont(ArialMT_Plain_10);
  display.drawString(8, 30, "DISPLAY_WIDTH = " + String(DISPLAY_WIDTH));
  display.drawString(8, 45, "DISPLAY_HEIGHT = " + String(DISPLAY_HEIGHT));
  
  display.display();

  yield();
  delay(2000);
}

18Include the library we downloaded earlier
20Create a display with I2C address 0x3c, the SDA connected to D6, and the SCL connected to D5
24Initialize the display (this creates the buffer)
25Change the orientation of the display
30Clear the buffer
32Draw a border around the screen - notice that the library makes two constants available to us: DISPLAY_WIDTH and DISPLAY_HEIGHT
34Set the font we'll be using.
35Print "Hello, world!" at x = 20 and y = 7
37Change font to ArialMT_Plain_10
38 - 39Print the screen width - note that this value is available in the constants DISPLAY_WIDTH and DISPLAY_HEIGHT
41Copy the buffer to the physical display
43-44Wait for a bit before doing it all again

The library includes three fonts: ArialMT_Plain_10, ArialMT_Plain_16, and ArialMT_Plain_24. Additional fonts can be created using the tools found at:


Show Sensor Data on the Display
Now we will show the temperature, humidity and barometric pressure on the display. To make this a little more interesting, we will alternate showing British units and metric units.

You would think that pulling this off would be simply a matter combining the above code with the sketch from the last tutorial - after all, there is no pin overlap, right? As it goes, restoring the jumper wires as follows WILL NOT WORK!

The ESP8266 has exactly one I2C bus, so the two devices (the BME280 and the OLED display) must be on that one I2C bus! Here's what the connections will look like on a full-sized breadboard.

Once the connections are correct, the code is very easy!

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
/**
 * BME280-OLED
 * 
 * By: Mike Klepper
 * Date: 31 March 2017
 * 
 * This program reads data from the BMP280 and shows it on a 
 * SSD1306 OLED display. It will alternte between British and 
 * metric units.
 * 
 * See blog post on patriot-geek.blogspot.com 
 * for connections.
 */

#include "Wire.h"
#include "Adafruit_Sensor.h"
#include "Adafruit_BME280.h"
#include "SSD1306.h"

const float SEA_LEVEL_PRESSURE_HPA = 1013.25;
const int DELAY = 3000;
const int STARTUP_DELAY = 500;


Adafruit_BME280 bme;

SSD1306 display(0x3c, D6, D5);

void setup() 
{
  Serial.begin(115200);
  
  if(!bme.begin())
  {
    Serial.println("Could not find a valid BME280 sensor, check wiring!");
    while (1)
    {
        yield();
        delay(DELAY);
    }
  }
  delay(STARTUP_DELAY);

  display.init();
  display.flipScreenVertically();

}

void loop() 
{
  float tempC = bme.readTemperature();
  float humidity = bme.readHumidity();
  float pressurePascals = bme.readPressure();

  // Print to serial monitor
  printToSerial(tempC, humidity, pressurePascals);

  // Display data on screen in British units
  drawWithBritishUnits(tempC, humidity, pressurePascals);
  yield();
  delay(DELAY);

  // Display data on screen in metric units
  drawWithMetricUnits(tempC, humidity, pressurePascals);
  yield();
  delay(DELAY);
}


void drawWithBritishUnits(float tempC, float humidity, float pressurePascals)
{
  float tempF = 9.0/5.0 * tempC + 32.0;
  float pressureInchesOfMercury = 0.000295299830714 * pressurePascals;
  
  display.clear();
  
  display.drawRect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT);
  
  display.setFont(ArialMT_Plain_16);
  display.drawString(35, 3, "BME280");
  
  display.setFont(ArialMT_Plain_10);
  display.drawString(5, 22, "Temperature = " + String(tempF) + " *F");
  display.drawString(5, 35, "Humidity = " + String(humidity) + "%");
  display.drawString(5, 48, "Pressure = " + String(pressureInchesOfMercury) + " inHg");
  
  display.display();
}

void drawWithMetricUnits(float tempC, float humidity, float pressurePascals)
{
  float pressureHectoPascals = pressurePascals / 100.0;
  
  display.clear();
  
  display.drawRect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT);
  
  display.setFont(ArialMT_Plain_16);
  display.drawString(35, 3, "BME280");
  
  display.setFont(ArialMT_Plain_10);
  display.drawString(5, 22, "Temperature = " + String(tempC) + " *C");
  display.drawString(5, 35, "Humidity = " + String(humidity) + "%");
  display.drawString(5, 48, "Pressure = " + String(pressureHectoPascals) + " h,Pa");
  
  display.display();
}

void printToSerial(float tempC, float humidity, float pressurePascals)
{
    // Temperature
    float tempF = 9.0/5.0 * tempC + 32.0;

    Serial.println("Temperature:");
    printValueAndUnits(tempC, "*C");
    printValueAndUnits(tempF, "*F");
    //printValueAndUnits(tempC, "°C");
    //printValueAndUnits(tempF, "°F");
    Serial.println("");

    // Barometric pressure
    float pressureHectoPascals = pressurePascals / 100.0;
    float pressureInchesOfMercury = 0.000295299830714 * pressurePascals;

    Serial.println("Pressure:");
    printValueAndUnits(pressurePascals, "Pa");
    printValueAndUnits(pressureHectoPascals, "hPa");
    printValueAndUnits(pressureInchesOfMercury, "inHg");
    Serial.println("");

    // Humidity
    Serial.println("Humidity:");
    printValueAndUnits(humidity, "%");
    Serial.println("");

    // Approximate altitude
    float altitudeMeters = bme.readAltitude(SEA_LEVEL_PRESSURE_HPA);
    float altitudeFeet = 3.28 * altitudeMeters;
    
    Serial.println("Approx. Altitude:");
    printValueAndUnits(altitudeMeters, "m");
    printValueAndUnits(altitudeFeet, "ft");
    Serial.println();
}

void printValueAndUnits(float value, String units)
{
    Serial.print("     ");
    Serial.print(value);
    Serial.print(" ");
    Serial.println(units);
}

This completes our attempt to display sensor data using hardware. Although we were successful, we did not use the WiFi capabilities of the ESP8266! That's what the next tutorial will be about!

Tuesday, March 28, 2017

NodeMCU and Digital Sensors

The goal for the next few tutorials is to read data from a digital sensor, then output that data in various fashions. We will finish this series by discussing the shortcomings common to all of these approaches, and this will set the direction for future work.

The sensor we'll be using is called a BME280, which returns temperature, humidity, and barometric pressure. Using sea level air pressure, the sensor also returns an approximate altitude, too. Adafruit has developed an Arduino library for this sensor, and we'll be using that library.

Instructions for installing the Arduino IDE and the baseline ESP8266 board can be found in the earlier "Getting Started with the NodeMCU ESP8266 Board" tutorial.


Installing the Libraries
First, we install the BME280 library into the Arduino IDE. Well, actually, there are two libraries to install:

  • Adafruit Unified Sensor Driver
  • Adafruit BME280

The Adafruit Unified Sensor Driver library is an abstraction layer that provides a unified interface for numerous types of sensors. One of the ways this interface "unifies" these sensors is that sensor values will always be returned in metric (SI) units. A list of those sensors and an explanation of why an abstraction layer is a good thing can be found at https://github.com/adafruit/Adafruit_Sensor.

The Adafruit BME280 library implements the methods in the first library for the BME280.

To install these libraries, open the Arduino IDE, and choose the menu Sketch | Include Library | Manage Libraries..., and the Library Manager window will open:

Enter "Adafruit Unified Sensor" into the search box, and scroll through the results until you find the row titled "Adafruit Unified Sensor" library. Click the row, and a version drop-down list and an "Install" button will be displayed. Choose the latest version (currently 1.0.5) and click the "Install" button.

Note: once the Adafruit Unified Sensor Driver library is installed, we need not install it again when we use other sensors that depend on it.

Now install the BME280 library itself: in the search box, enter "Adafruit BME280", select the latest version (1.0.5 as of this writing) and click "Install".

Done and done!


Connecting the Sensor Using I2C
The BME280 comes mounted on a number of different breakout boards from different vendors. We will use the (overpriced) version from SparkFun Electronics - other vendors sell far less expensive versions. The SparkFun breakout board includes connection points for both I2C and SPI interfaces, and we will be using the I2C interface, which has the following pins: GND, 3.3V, SDA, and SCL. Connect the sensor to the ESP8266 as follows:

Notice that we used NodeMCU's D2 pin for SDA and NodeMCU's D1 pin for SCL. Those are the default I2C pins for the NodeMCU.


Read From the Sensor
Enter the following sketch:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
/*
 * BME280
 * 
 * By: Mike Klepper
 * Date: 28 March 2017
 * 
 * The BME280 is a humidity + temperature + barometric pressure sensor.
 * This program reads those values from that sensor, converts the values into various units,
 * then displays the results in the Serial Monitor. It is based upon code found 
 * at learn.adafruit.com.
 *
 * Connections using the SparkFun breakout board:
 * BME280 GND --> NodeMCU GND
 * BME280 3.3V --> NodeMCU 3V3
 * BME280 SDA --> NodeMCU D2
 * BME280 SCL --> NodeMCU D1
 */

#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>

const float SEA_LEVEL_PRESSURE_HPA = 1013.25;
const int DELAY = 2000;
const int STARTUP_DELAY = 500;

Adafruit_BME280 bme;

void setup() 
{
  Serial.begin(115200);
  Serial.println("BME280 Test");

  if(!bme.begin())
  {
    Serial.println("Could not find a valid BME280 sensor, check wiring!");
    while (1)
    {
        yield();
        delay(DELAY);
    }
  }
  delay(STARTUP_DELAY);
}

void loop() 
{
    // Temperature    
    float tempC = bme.readTemperature();
    float tempF = 9.0/5.0 * tempC + 32.0;

    Serial.println("Temperature:");
    printValueAndUnits(tempC, "*C");
    printValueAndUnits(tempF, "*F");
    Serial.println("");

    // Barometric pressure
    float pressurePascals = bme.readPressure();
    float pressureHectoPascals = pressurePascals / 100.0;
    float pressureInchesOfMercury = 0.000295299830714 * pressurePascals;

    Serial.println("Pressure:");
    printValueAndUnits(pressurePascals, "Pa");
    printValueAndUnits(pressureHectoPascals, "hPa");
    printValueAndUnits(pressureInchesOfMercury, "inHg");
    Serial.println("");

    // Humidity
    float humidity = bme.readHumidity();
    
    Serial.println("Humidity:");
    printValueAndUnits(humidity, "%");
    Serial.println("");

    // Approximate altitude
    float altitudeMeters = bme.readAltitude(SEA_LEVEL_PRESSURE_HPA);
    float altitudeFeet = 3.28 * altitudeMeters;
    
    Serial.println("Approx. Altitude:");
    printValueAndUnits(altitudeMeters, "m");
    printValueAndUnits(altitudeFeet, "ft");
    Serial.println();

    Serial.println();
    yield();
    delay(DELAY);
}

void printValueAndUnits(float value, String units)
{
    Serial.print("     ");
    Serial.print(value);
    Serial.print(" ");
    Serial.println(units);
}

Again, D2 and D1 are NodeMCU's default pins for SDA and SCL, respectively. Using a different ESP8266 board will entail specifying those pins in the code, as explained on the Adafruit site.

The code is self explanatory, except for the following:

Line 19 - Wire.h is Arduino's default library for communicating with I2C devices, like the BME280.
Line 23 - The value chosen for SEA_LEVEL_PRESSURE_HPA is the average air pressure at sea-level, measured in hectoPascals.

Save it, then choose "NodeMCU 1.0 (ESP-12E Module)" under Tools | Board and choose the right port under Tools | Port. Flash the code to the NodeMCU, and open the serial monitor once it has done uploading:

These results don't exactly match the results returned by the local weatherman, but they were taken while the sensor is in a (rather warm) public library.

Thursday, February 23, 2017

Scanning for WiFi Networks

The next few blog posts will be based on some of the code samples included with the Arduino IDE for ESP8266. The specific examples are chosen for their particular relevance to IoT applications. The examples will NOT be exactly like the examples included in the Arduino IDE, but will be extended to demonstrate additional features, to illustrate coding techniques, etc.

The ESP8266's wifi can be in one of four modes:

# Name Description
0 WIFI_OFF The wifi is... off!
1 WIFI_STA Station mode - the ESP is working as a wifi client
2 WIFI_AP Access point mode - other wifi clients will be able to connect to the ESP
3 WIFI_AP_STA  AP + Station mode - The ESP8266 is acting both as a station and as an access point

The WiFi class is fundamental to most of these examples, so here is a quick overview of some of its properties and methods, as well as associated constants. Since this example is only about scanning wifi networks, and not connecting to them, the methods for actually connecting to an access point will be covered in a later tutorial.

WiFi.mode(modeName) - set the ESP8266's wifi mode to be one of the four values listed above

WiFi.disconnect() - disconnect from a station

WiFi.scanNetworks() - returns the number of available networks as well as some information about each

  • SSID - Service set identifier, essentially the network name
  • BSSID - MAC address of the network access point
  • RSSI - Received Signal Strength Indicator; access points with RSSIs closer to zero have stronger signals
  • isHidden
  • channel
  • encryptionType - see below code for the encryption types recognized by the ESP8266

For the wifi scanner, we will be using station mode, but again we will not be connecting to any of the access points we find. The code will list each access point, it's name, encryption type, etc., in the Serial Monitor window.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
/*
 * WiFiScanDetails
 * 
 * By: Mike Klepper
 * Date: 22 Feb 2017
 * 
 * This program checks for available networks and displays details about each in the Serial Monitor. 
 * It is based upon the WiFiScan example in the Arduino IDE for the ESP8266
 * 
 */
 
#include "ESP8266WiFi.h"

void setup() 
{
  Serial.begin(115200);
  
  WiFi.mode(WIFI_STA);
  WiFi.disconnect();
  
  delay(100);

  Serial.println("Setup done");
}

void loop() 
{
  Serial.println("Scan start");

  int numNetworks = WiFi.scanNetworks();
  Serial.println("Scan done");
  
  if(numNetworks == 0)
  {
    Serial.println("No networks found");
  }
  else
  {
    Serial.print(numNetworks);
    Serial.println(" networks found");
    
    for(int i = 0; i < numNetworks; i++)
    {
      Serial.print(i + 1);
      Serial.print(": ");
      Serial.println(WiFi.SSID(i));

      Serial.print("     BSSID = ");
      Serial.println(WiFi.BSSIDstr(i));

      Serial.print("     RSSI = ");
      Serial.println(WiFi.RSSI(i));

      Serial.print("     isHidden = ");
      Serial.println(WiFi.isHidden(i));

      Serial.print("     channel = ");
      Serial.println(WiFi.channel(i));

      Serial.print("     encryptionType = ");
      Serial.print(readableEncryptionType(WiFi.encryptionType(i)));
      Serial.print(" (");
      Serial.print(WiFi.encryptionType(i));
      Serial.println(")");
      
      delay(10);
      yield();
    }
  }
  
  Serial.println("");

  delay(5000);
  yield();
}

String readableEncryptionType(uint8_t encType)
{
  String encTypeAsString;
  
  switch(encType)
  {
    case ENC_TYPE_TKIP:
    {
      encTypeAsString = "WPA";
      break;
    }
    case ENC_TYPE_WEP:
    {
      encTypeAsString = "WEP";
      break;
    }
    case ENC_TYPE_CCMP:
    {
      encTypeAsString = "WPA2";
      break;
    }
    case ENC_TYPE_NONE:
    {
      encTypeAsString = "None";
      break;
    }
    case ENC_TYPE_AUTO:
    {
      encTypeAsString = "Auto";
      break;
    }
    default:
    {
      encTypeAsString = "Other";
      break;
    }
  }

  return encTypeAsString;
}

Lines Explanation
1-10 This is a multi-line comment. Arduino sketches support C/C++/Java/JavaScript's style of single-line and multi-line comments
12 This makes the WiFi class available for use in the code!
14 The setup() function is ran exactly once when the ESP8266 is powered-on or reset
16 Enables serial communication over the USB port between the board and the computer to which it is attached. The baud rate is set to 115200 here
18 Set the ESP8266 into station mode
19 Disconnect from any previous network connection
26 The loop() function is ran after the setup() function completes
30 This line accomplishes two things: it returns the number of available wifi networks, and it allows us to get properties of those networks, as we'll see below
33-37 If the number of networks found is zero, say so, otherwise...
42 ... loop over the available wifi networks
46 Print the current network's SSID
49 Print the current network's BSSID as a string
52 Print the current network's RSSI
55 Print whether the current network is hidden
58 Print the current network's channel
61 Print the current network's encryption type in a human-readable manner (see line 81 for the readableEncryptionType function)
63 Print the current network's encryption type as an integer
73-74 Wait for five seconds, yield so that background processes can be handled, then do it again!
77 This function converts the encryption type into a human-readable string
81 We do a switch-case statement to choose the right descriptive string for the corresponding encryption type
115 Return the descriptive string to whatever called this function

After flashing this code, open the Serial Monitor to see the output.

Monday, February 20, 2017

Getting Started with the NodeMCU ESP8266 Board

In this tutorial we will use the Arduino IDE to compile and flash our code to the NodeMCU ESP8266. When we "flash" code to an embedded device, it will be written so that the device will retain that code and run it on reset or next power-up.

These instructions have been tested on Mac OS X 10.11.6.


Part 1: Connect Board and Install Drivers

By default, Mac OS X will not recognize the NodeMCU ESP8266 when it is plugged in! So we will install a driver so that ESP8266, when connected, appears as a serial port.

  1. Connect the ESP8266 dev board to your computer using a USB cable. Note: not all USB cables will work for this! In particular, cables designed for portable phone rechargers will usually not work. USB cables that are used for synching Android phones should be OK.
  2. On Mac, open a terminal window and type:
    ls /dev/cu.*
    You will find Bluetooth ports, but nothing we really want!
  3. Download and install the Silicon Labs VCP Driver. The URL is: https://www.silabs.com/products/development-tools/software/usb-to-uart-bridge-vcp-drivers
  4. Now we need to determine the serial port the ESP8266 is using, as the Arduino IDE may or may not detect it automatically. So, open a terminal window and type:
    ls -l /dev/cu.*
    The desired port should be something like /dev/cu.SLAB_USBtoUART
  5. If that port does not show, try using a different USB cable. Also, you may need to restart your computer at this point.

NOTE: In Windows, use the Device Manager to determine the NodeMCU's USB port.


Part 2: Setup Arduino IDE for ESP8266 Development

  1. Download IDE from Arduino.cc - current version is 1.8.1. URL is: https://www.arduino.cc/en/Main/Software
  2. On Mac, this will download arduino-1.8.1-macosx.zip to the Downloads folder. Double-click to install.
  3. Start the Arduino IDE
  4. Open Preferences window by choosing Arduino | Preferences… menu
  5. Add the following URL to the “Additional Boards Manager URLs” field: http://arduino.esp8266.com/stable/package_esp8266com_index.json
  6. While we’re here, click the “Display line numbers” checkbox
  7. Press “OK” button to save these changes
  8. Open the Boards Manager by using “Tools | Board | Boards Manager…” menu
  9. In search box at top right of that window, enter “esp8266”
  10. Click on the “esp8266 by ESP8266 Community” item, and an “Install” button appears. Click it, and this will start the installation process.
  11. Once installation is complete, click the close button.
As a result of doing this:
  1. You can now target your code for the ESP8266
  2. A number of example programs have been installed, available from “File | Examples” menu

Part 3: Connect Board and Configure the IDE

  1. Connect the ESP8266 dev board to your computer using a USB cable.
  2. Start Arduino IDE if it isn't already running.
  3. Under “Tools | Board” menu, choose NodeMCU 1.0 (ESP-12E Module)
  4. In Arduino IDE, choose “Tools | Port | /dev/cu.SLAB_USBtoUART”
  5. If all goes well, the board type and port will be shown at bottom of Arduino IDE window.

Part 4: First Program

The default program in the IDE window is as follows:

1
2
3
4
5
6
7
8
9
void setup() {
  // put your setup code here, to run once:

}

void loop() {
  // put your main code here, to run repeatedly:

}

For our first program, we will make the NodeMCU's onboard LED blink. Modify the code to read as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/*
 * BlinkLED
 * 
 * By: Mike Klepper
 * Date: 19 Feb 2017
 * 
 * This program blinks the LED on the NodeMCU board
 * 
 * LED_BUILTIN is used to find the pin for the onboard LED; 
 * the delay constants at the top are in milliseconds
 */

const int LED_ON_DELAY = 500;
const int LED_OFF_DELAY = 2000;

void setup() 
{
  pinMode(LED_BUILTIN, OUTPUT); 
  Serial.begin(115200);
}

void loop() 
{
  Serial.print("LED pin is: ");
  Serial.println(LED_BUILTIN);
  
  digitalWrite(LED_BUILTIN, LOW);
  Serial.println("LED is on");
  delay(LED_ON_DELAY); 
  
  digitalWrite(LED_BUILTIN, HIGH);
  Serial.println("LED is off");
  delay(LED_OFF_DELAY); 
  
  yield();
  Serial.println("");
}

Lines Explanation
1-11 This is a multi-line comment. Arduino sketches support C/C++/Java/JavaScript's style of single-line and multi-line comments
13-14 Constants determining the length of time the LED is lit and unlit, measured in milliseconds
16 The setup() function is ran exactly once when the ESP8266 is powered-on or reset
18 LED_BUILTIN determines the pin number for controlling the user-controllable LED. This line sets that pin for output
19 Enables serial communication over the USB port between the board and the computer to which it is attached. The baud rate is set to 115200 here
16 The loop() function is ran after the setup() function completes
24-25 Write values to the serial port
27 Set the voltage sent to the pin to be LOW (on)
29 Causes control to wait for the specified number of milliseconds
31 Set the voltage sent to the pin to be HIGH (off)
35 Allows the ESP8266 to handle background processes, like WiFi connections

We now compile and flash this program:

  1. Click the arrow button at the top-left of the IDE window. The program will be compiled and then will be flashed to the ESP8266 board. When flashing, a blue LED on the NodeMCU board will flicker. Once the program starts, the red LED will turn-on for 0.5 seconds, and then turn-off for 2 seconds.
  2. The ArduinoIDE has a built-in serial monitor. To open it, click the magnifier glass icon at top right corner of the IDE.
  3. Set the speed to be 115200 baud to match the rate that the above program is transmitting. The serial monitor will display the results of the Serial.println() and Serial.print() statements. As you can see, LED_BUILTIN resolves to pin 16.

If you've completed this tutorial, then you now know how to use the Arduino IDE to write code for the ESP8266! Ooh RAH!