Showing posts with label Digital Sensors. Show all posts
Showing posts with label Digital Sensors. Show all posts

Saturday, September 23, 2017

Exposing Data in a Captive Portal

So far, BME-280 sensor data has been exposed as a JSON API and as a web page. There are two problems with the second approach:

  • There must be a wifi router to which the ESP8266 can connect
  • The IP address the router assigns must be somehow displayed before we visit the page

In an earlier blog post, the IP address was displayed in the Arduino IDE's serial monitor. Of course, we could connect a display and output the IP address there. Wouldn't it be nice if our browser were automatically redirected to that page, sort of like what happens when you connect to a public network in your favorite coffee shop?

That's exactly what we'll do.

We establish our own access point such that whenever the user connects to that network, his browser will be automatically directed to a specific landing page. This landing page is called a "captive portal", and is typically used for user authentication, gather information, plant tracking cookies, etc.

Captive portals are not just for coffee shops any more - ours will display temperature, humidity, and air pressure from the BME-280 sensor!

Here's the 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
 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
/**
 * BME280-Captive-Portal
 * 
 * By: Mike Klepper
 * Date: 23 September 2017
 * 
 * This program exposes BME-280 data as a captive portal.
 * 
 * See patriot-geek.blogspot.com
 * for explanation.
 */

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

const int STARTUP_DELAY = 500;

const char* MESSAGE_503 = "503 Service Unavailable";

const char* AP_NAME = "BME-280 Data";
const byte DNS_PORT = 53;
const IPAddress SUBNET_MASK(255, 255, 255, 0);
const IPAddress AP_IP(192, 168, 1, 1);
const byte WEB_SERVER_PORT = 80;

boolean sensorAvailable;

DNSServer dnsServer;
ESP8266WebServer webServer(WEB_SERVER_PORT);

Adafruit_BME280 bme;

void setup(void)
{
  // Start the BME280 sensor
  if(!bme.begin())
  {
    sensorAvailable = false;
  }
  else
  {
    sensorAvailable = true;
    delay(STARTUP_DELAY);
  }

  WiFi.mode(WIFI_AP);
  WiFi.softAPConfig(AP_IP, AP_IP, SUBNET_MASK);
  WiFi.softAP(AP_NAME);

  dnsServer.start(DNS_PORT, "*", AP_IP);

  webServer.onNotFound([]() 
  {
    returnHtml();
  });
  
  webServer.begin();
}

void loop(void)
{
  dnsServer.processNextRequest();
  webServer.handleClient();
  yield();
}

void returnHtml()
{
  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 HTML response
    String responseHtml = "";
    responseHtml += "<!DOCTYPE html>";
    responseHtml += "<html>";
    responseHtml += "    <head>";
    responseHtml += "        <meta charset=\"UTF-8\">";
    responseHtml += "        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">";
    responseHtml += "        <title>BME-280 Sensor Data</title>";
    responseHtml += "        <style>";
    responseHtml += "            body {font-family: sans-serif}";
    responseHtml += "            h1 {font-size: 1.0cm}";
    responseHtml += "            p {font-size: 0.50cm}";
    responseHtml += "            button {font-size: 1.0cm}";
    responseHtml += "        </style>";
    responseHtml += "        <script type=\"text/javascript\">";
    responseHtml += "        function refreshPage()";
    responseHtml += "        {";
    responseHtml += "            location.reload(true);";
    responseHtml += "        }";
    responseHtml += "        </script>";
    responseHtml += "    </head>";
    responseHtml += "    <body>";
    responseHtml += "        <h1>BME-280 Sensor Data</h1>";
    responseHtml += "        <p>Temperature: " + String(tempF) + "&deg; F</p>";
    responseHtml += "        <p>Humidity: " + String(humidity) + "%</p>";
    responseHtml += "        <p>Pressure: " + String(pressureInchesOfMercury) + " inHg</p>";
    responseHtml += "        <button type=\"button\" onclick=\"refreshPage()\">Refresh</button>";
    responseHtml += "    </body>";
    responseHtml += "</html>";
  
    // Return HTML with correct MIME type
    webServer.send(200, "text/html; charset=utf-8", responseHtml);
  }
  else
  {
    webServer.send(500, "text/plain", MESSAGE_503);
  }
}

Once it is running, you will see a new wireless network, called "BME-280 Data".

Connect to it, and you're immediately presented with the sensor data!

The only new part of the code are on lines 49 through 58:

49 Set the ESP8266 into access point mode
50 Set the AP's local IP address, gateway IP, and subnet mask
51 Specify the AP's name
53 Start a domain name server
55-58 Return the same HTML regardless of the address the user tries to visit

This technique - creating a captive portal - has many applications beyond displaying sensor data. In particular, it can be used to view and edit configuration values. This will be explored in a future post.

Why bother having the user connect to that AP - why not just make the AP name to be the sensor data? One problem with this is that the AP name will usually be truncated by the network manager of the computer or smart phone. Further, most computers or smart phones cache the names of the networks it finds.

Saturday, September 2, 2017

Exposing Data in a Web Page

In a previous post, BME-280 data was exposed as JSON, and that data was made available using a simple API. Once the web server was created and API URL paths were specified, exposing sensor data as JSON became just a problem in string manipulation - we just constructed a string that contained the data, and that string was in valid JSON format.

This same trick can be applied to exposing data in an HTML page - we'll construct a string that includes the sensor data, and that string will be a valid HTML5 document. We return that string with mime type text/html, and it will be rendered in a browser!

Why stop with HTML? We can also use string manipulation to include CSS and JavaScript!

Our goal is to have the ESP8266 serve a page that:

  • Presents temperature, humidity, and barometric pressure from the BME-280 sensor
  • Includes CSS that makes the page somewhat attractive on both notebook computers and iPhones
  • Has button that when clicked will call a JavaScript function that refreshes the page

We could add this to the program in that previous post, but to keep things simple, this functionality will be put in a separate program.

Here's the program:

  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
/**
 * BME280-WebServer-HTML
 * 
 * By: Mike Klepper
 * Date: 2 September 2017
 * 
 * This program exposes BME-280 data in a web page.
 * 
 * See patriot-geek.blogspot.com
 * for explanation.
 */

#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("/", returnHtml);
  server.on("", returnHtml);
  
  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 returnHtml()
{
  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 HTML response
    String responseHtml = "";
    responseHtml += "<!DOCTYPE html>";
    responseHtml += "<html>";
    responseHtml += "    <head>";
    responseHtml += "        <meta charset=\"UTF-8\">";
    responseHtml += "        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">";
    responseHtml += "        <title>BME-280 Sensor Data</title>";
    responseHtml += "        <style>";
    responseHtml += "            body {font-family: sans-serif}";
    responseHtml += "            h1 {font-size: 1.0cm}";
    responseHtml += "            p {font-size: 0.50cm}";
    responseHtml += "            button {font-size: 1.0cm}";
    responseHtml += "        </style>";
    responseHtml += "        <script type=\"text/javascript\">";
    responseHtml += "        function refreshPage()";
    responseHtml += "        {";
    responseHtml += "            location.reload(true);";
    responseHtml += "        }";
    responseHtml += "        </script>";
    responseHtml += "    </head>";
    responseHtml += "    <body>";
    responseHtml += "        <h1>BME-280 Sensor Data</h1>";
    responseHtml += "        <p>Temperature: " + String(tempF) + "&deg; F</p>";
    responseHtml += "        <p>Humidity: " + String(humidity) + "%</p>";
    responseHtml += "        <p>Pressure: " + String(pressureInchesOfMercury) + " inHg</p>";
    responseHtml += "        <button type=\"button\" onclick=\"refreshPage()\">Refresh</button>";
    responseHtml += "    </body>";
    responseHtml += "</html>";
  
    // Return HTML with correct MIME type
    server.send(200, "text/html; charset=utf-8", responseHtml);
  }
  else
  {
    return503Error();
  }
}

Once the program is running, the IP address that the network assigns to the ESP8266 is displayed in the serial monitor. For example, my IP address is (currently) 192.168.1.8. Open a browser and go to http://192.168.1.8, and the following will be displayed:

On the iPhone, the result is:

Click or tap the "Refresh" button and the page does indeed refresh.

This program functions very similar to the one in the previous post, so I won't do a code walk-through.

Most any web developer will complain about the way I mixed HTML, CSS, and JavaScript in one file. But that is the attitude of one who has the luxury of great bandwidth and fast servers, and we are not in that position.

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.