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.

No comments:

Post a Comment