How to expose the temperature, pressure, and humidity data over WiFi? There are two subquestions here:
- What network mechanism to use?
- 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 - 15 | Include libraries for working with WiFi as well as creating a web server on an ESP8266 |
16 - 17 | Include the libraries for using the BME280 sensor |
19 - 20 | Specify the network name (SSID) and password for the network we're connecting to. Everybody's password is "**********", no? |
25 - 26 | Messages for 404 and 503 errors |
38 - 47 | Initialize the BME280 sensor; set sensorAvailable to the appropriate value |
49 | Connect to the network |
52 - 56 | Print dots to the serial monitor until we're connected |
58 - 62 | Print connection info to the serial monitor |
64 - 66 | Specify the paths used to request single sensor readings (temperature only, for example), and associate a handler for each |
67 - 68 | Associate the same handler - returnAll() - for two different paths |
70 | Have web server call return404Error() whenever a URI handler is not specified |
72 | Start the web server! |
76 - 80 | Handle incoming requests, repeatedly |
82 - 85 | Whenever a path hasn't been found, send HTTP error code 404, using MIME type "text/plain" |
87 - 90 | For internal errors (like when the BME280 isn't available), send HTTP error code 503, using MIME type "text/plain" |
92 - 110 | Callback for handling /home/living-room/temperature requests |
94 | If the sensor is available at startup... |
96 - 98 | Read the temperature from the BME280 and convert to Fahrenheit |
101 | Wrap the temperature inside a JSON object |
104 | Return that JSON using MIME type "application/json" and response code 200 |
106 - 109 | If the sensor is NOT available, return 503. |
112 - 129 | Callback for handling /home/living-room/humidity requests |
131 - 149 | Callback for handling /home/living-room/barometric-pressure requests |
151 - 179 | Callback for handling /home/living-room/ and /home/living-room requests |
Notes:
- 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 forbme.begin()
in each of the response handlers. - To connect to a wifi network that doesn't use a password, change line 50 to read:
WiFi.begin(SSID);
- Change lines 26 and 27 to give more interesting error messages!
Hello and thanks for the sketch.
ReplyDeleteI get the error
Request header field X-Requested-With is not allowed by Access-Control-Allow-Headers in preflight response.
Can you assist in adding the headers for CORS
So, to clarify. I am using javascript and html to return the json formatted values but get the error Request header field X-Requested-With is not allowed by Access-Control-Allow-Headers in preflight response. It works fine with the url in the browser address bar. Its the json call in javascript that gets this error in the browser. Whats even weirder is in the Chrome developer console under Network --> Preview the correctly formatted values are there.
ReplyDeleteLOL...Sorry to pester you. I figured it out. Since I am only requesting the "All" (returnAll) I placed the header in void returnAll() and right above server.send(200, "application/json", responseJson);
ReplyDeleteThe working headers for my application are
server.sendHeader("Access-Control-Allow-Headers", "Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers");
Glad you got it working!
Delete