Showing posts with label Temperature. Show all posts
Showing posts with label Temperature. Show all posts

Friday, May 22, 2020

ATOM Matrix: Temperature Sensor

The temperature sensor included in the MPU6886 is not terribly useful for measuring ambient temperature. It is better used for detecting hardware overheating.

To read the temperature in Celsius, first declare a float (called tempC in the following application), and then pass-in the address of that float into the getTempData method like this:

M5.IMU.getTempData(&tempC)

For the sake of completeness, though, here is a simple application using the sensor.


Change Display Color Based on Temperature

 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
/*
 * TemperatureSensor01.ino
 * 
 * By: Mike Klepper
 * Date: 26 April 2020
 * 
 * Changes display based on internal temperature
 */

#include "M5Atom.h"

int GRB_COLOR_WHITE = 0xffffff;
int GRB_COLOR_BLACK = 0x000000;
int GRB_COLOR_RED = 0x00ff00;
int GRB_COLOR_ORANGE = 0xa5ff00;
int GRB_COLOR_YELLOW = 0xffff00;
int GRB_COLOR_GREEN = 0xff0000;
int GRB_COLOR_BLUE = 0x0000ff;
int GRB_COLOR_PURPLE = 0x008080;

float lowTempF = 103;
float highTempF = 105;

float tempC = 0;
bool IMU6886Flag = false;

void setup() 
{
    M5.begin(true, false, true);
    delay(20);
    
    IMU6886Flag = M5.IMU.Init() == 0;

    if(!IMU6886Flag)
    {
        Serial.println("Error initializing the IMU! :-(");
    }
}

void loop() 
{
    if(IMU6886Flag)
    {
        M5.IMU.getTempData(&tempC);
        Serial.printf(" Temp : %.2f C \r\n", tempC);
        float tempF = tempC * 9 / 5 + 32;
        Serial.printf(" Temp : %.2f F \r\n", tempF);
    
        if(tempF < lowTempF)
        {
            fillDisplay(GRB_COLOR_BLUE);
        }
        else if(tempF > highTempF)
        {
            fillDisplay(GRB_COLOR_RED);
        }
        else
        {
            fillDisplay(GRB_COLOR_GREEN);
        }
        
        delay(500);
        M5.update();
    }
}

void fillDisplay(int fillColor)
{
    for(int i = 0; i < 25; i++)
    {
        M5.dis.drawpix(i, fillColor);
    }
}

The delay(20) found on line 30 is necessary, as the MPU6886 is sometimes not found without it.

Click here to go to the table of contents for this series.

Sunday, December 3, 2017

Node-RED: Dashboard Notifications and Rate Limiting

This post builds upon the dashboard we built in the last post. In particular we will:

  1. Simplify the dashboard flow using a Function node
  2. Alert the user when specific conditions are met
  3. Examine the source code for the final dashboard


Simplify Using Function Nodes
The previous version of the dashboard included three Change nodes...

The purpose of those nodes was to extract the temperature, humidity, and barometric pressure from the incoming JSON message. The reason we did that was because the Chart and Gauge nodes required that the payload hold only a single value, the value that will be charted.

The Function node will allow us to extract those three values in one step!

Start by adding a new Function node to the "MQTT Dashboard" workspace, name it "Extract Values", set the number of outputs to 3, and enter the following code for the Function node's umm... Function field:

return [
    {payload: msg.payload.temperature}, 
    {payload: msg.payload.humidity},
    {payload: msg.payload["barometric-pressure"]}
];

What this code does is to return an array of objects, each one of which contains a single property, "payload", which has been set to the temperature, humidity, and pressure found in the incoming message.

By giving the Function node three output ports, we are sending each value in that array out into those three ports. Which output goes to each port? The output ports will be arranged top to bottom on the right side of the node, with temperature going to the topmost port, humidity to the middle port, and pressure to the bottom. To make this easier to see, we can even give labels to the output ports, as shown:

Now, remove the three Change nodes, drag the new Function node into place, and wire everything up like this:

Deploy the dashboard, view it at http://127.0.0.1:1880/ui, and it should be working fine!


Dashboard Notifications
Next, let us add some notifications within the dashboard. We want to alert the user when the temperature climbs above 80 °F. In particular, we will show a "toast" popup, and even use text-to-speech to announce that the temperature is above that value.

Now sensor readings are coming in once every second. So if we just did this, the user would be notified once every second - not a good user experience! What we must do is limit the rate that these notifications are sent, without changing the rate at which data is arriving. Node-RED has this functionality ready for our use, in Delay node, which is found in the "function" section of the palette.

Drag into the workspace one of each of the following nodes: Switch, Delay, Notification, Change, and Audio Out. The Notification and Audio Out nodes are found in the "dashboard" section. Set their properties as follows:

Node Type Property Value
Switch
Name: Check Temperature
Property: msg.payload
Condition: > 80
Delay
Action: Rate Limit
Rate: 1 msg(s) per
Interval: 1 Minute
Drop Intermediate Messages: Checked
Notification
Layout: Top Right
Timeout (S): 3
Topic: Temperature is Above 80 °F
Change
Action: Set
Object: msg.payload
To: Temperature is above 80 degrees!
Audio Out
Group: Temperature [Living Room Data]
TTS Voice: Alex (en-US)

Note that for the Notification node's topic, it is necessary to enter the degree (°) symbol directly - using the HTML escape sequence will not work!

Finally, wire up components as follows:

An easy way to test this is to (believe it or not) breathe on the BME280. Once the temperature gets sufficiently high, the "toast" popup will be displayed in the top-right corner, and the audio message will be played!


Dashboard Source Code
Here's the source code for the dashboard flow. There are no changes to the MQTT Subscriber, and that code can be found in the previous post.

  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
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
[
    {
        "id": "492fdf69.9cb38",
        "type": "link in",
        "z": "49d20fae.01266",
        "name": "From Subscriber",
        "links": [
            "3a476f0b.d6bea"
        ],
        "x": 195,
        "y": 220,
        "wires": [
            [
                "14f7f237.a4b53e",
                "cf06c435.a31528"
            ]
        ]
    },
    {
        "id": "14f7f237.a4b53e",
        "type": "debug",
        "z": "49d20fae.01266",
        "name": "",
        "active": false,
        "console": "false",
        "complete": "true",
        "x": 330,
        "y": 160,
        "wires": []
    },
    {
        "id": "6b80f3f4.1fdb4c",
        "type": "ui_gauge",
        "z": "49d20fae.01266",
        "name": "Humidity Gauge",
        "group": "f468b186.a264b",
        "order": 0,
        "width": 0,
        "height": 0,
        "gtype": "gage",
        "title": "",
        "label": "%",
        "format": "{{value}}",
        "min": 0,
        "max": "100",
        "colors": [
            "#00b500",
            "#e6e600",
            "#ca3838"
        ],
        "seg1": "",
        "seg2": "",
        "x": 620,
        "y": 260,
        "wires": []
    },
    {
        "id": "62dd2ada.5449a4",
        "type": "ui_chart",
        "z": "49d20fae.01266",
        "name": "Temperature Chart",
        "group": "637855dc.2229bc",
        "order": 0,
        "width": 0,
        "height": 0,
        "label": "",
        "chartType": "line",
        "legend": "false",
        "xformat": "HH:mm:ss",
        "interpolate": "linear",
        "nodata": "",
        "dot": false,
        "ymin": "",
        "ymax": "",
        "removeOlder": "5",
        "removeOlderPoints": "",
        "removeOlderUnit": "60",
        "cutout": 0,
        "useOneColor": false,
        "colors": [
            "#1f77b4",
            "#aec7e8",
            "#ff7f0e",
            "#2ca02c",
            "#98df8a",
            "#d62728",
            "#ff9896",
            "#9467bd",
            "#c5b0d5"
        ],
        "useOldStyle": false,
        "x": 630,
        "y": 140,
        "wires": [
            [],
            []
        ]
    },
    {
        "id": "4075ff77.21db8",
        "type": "ui_chart",
        "z": "49d20fae.01266",
        "name": "Pressure Chart",
        "group": "9728196c.705ff8",
        "order": 0,
        "width": 0,
        "height": 0,
        "label": "",
        "chartType": "line",
        "legend": "false",
        "xformat": "HH:mm:ss",
        "interpolate": "linear",
        "nodata": "",
        "dot": false,
        "ymin": "",
        "ymax": "",
        "removeOlder": "5",
        "removeOlderPoints": "",
        "removeOlderUnit": "60",
        "cutout": 0,
        "useOneColor": false,
        "colors": [
            "#1f77b4",
            "#aec7e8",
            "#ff7f0e",
            "#2ca02c",
            "#98df8a",
            "#d62728",
            "#ff9896",
            "#9467bd",
            "#c5b0d5"
        ],
        "useOldStyle": false,
        "x": 620,
        "y": 380,
        "wires": [
            [],
            []
        ]
    },
    {
        "id": "df80eb36.2bf648",
        "type": "ui_text",
        "z": "49d20fae.01266",
        "group": "637855dc.2229bc",
        "order": 0,
        "width": 0,
        "height": 0,
        "name": "Temperature Text",
        "label": "Temperature: ",
        "format": "{{msg.payload}} &deg;F",
        "layout": "row-left",
        "x": 630,
        "y": 200,
        "wires": []
    },
    {
        "id": "51d9d859.dc84c8",
        "type": "ui_text",
        "z": "49d20fae.01266",
        "group": "f468b186.a264b",
        "order": 0,
        "width": 0,
        "height": 0,
        "name": "Humidity Text",
        "label": "Humidity: ",
        "format": "{{msg.payload}} %",
        "layout": "row-left",
        "x": 620,
        "y": 320,
        "wires": []
    },
    {
        "id": "919809c4.84ef18",
        "type": "ui_text",
        "z": "49d20fae.01266",
        "group": "9728196c.705ff8",
        "order": 0,
        "width": 0,
        "height": 0,
        "name": "Pressure Text",
        "label": "Barometric Pressure:",
        "format": "{{msg.payload}} InHg",
        "layout": "row-left",
        "x": 620,
        "y": 440,
        "wires": []
    },
    {
        "id": "d29db2b5.f8ddb",
        "type": "delay",
        "z": "49d20fae.01266",
        "name": "",
        "pauseType": "rate",
        "timeout": "5",
        "timeoutUnits": "seconds",
        "rate": "1",
        "nbRateUnits": "1",
        "rateUnits": "minute",
        "randomFirst": "1",
        "randomLast": "5",
        "randomUnits": "seconds",
        "drop": true,
        "x": 840,
        "y": 60,
        "wires": [
            [
                "6e7140a1.07b05",
                "1e27bbe.febf544"
            ]
        ]
    },
    {
        "id": "6e7140a1.07b05",
        "type": "ui_toast",
        "z": "49d20fae.01266",
        "position": "top right",
        "displayTime": "3",
        "highlight": "",
        "outputs": 0,
        "ok": "OK",
        "cancel": "",
        "topic": "Temperature is Above 80 °F",
        "name": "",
        "x": 1070,
        "y": 40,
        "wires": []
    },
    {
        "id": "bb4a13de.4faaa",
        "type": "switch",
        "z": "49d20fae.01266",
        "name": "Check Temperature",
        "property": "payload",
        "propertyType": "msg",
        "rules": [
            {
                "t": "gt",
                "v": "80",
                "vt": "num"
            }
        ],
        "checkall": "true",
        "outputs": 1,
        "x": 630,
        "y": 60,
        "wires": [
            [
                "d29db2b5.f8ddb"
            ]
        ]
    },
    {
        "id": "3f251d1e.167872",
        "type": "ui_audio",
        "z": "49d20fae.01266",
        "name": "",
        "group": "637855dc.2229bc",
        "voice": "en-US",
        "always": false,
        "x": 1040,
        "y": 180,
        "wires": []
    },
    {
        "id": "1e27bbe.febf544",
        "type": "change",
        "z": "49d20fae.01266",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "Temperature is above 80 degrees!",
                "tot": "str"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 1060,
        "y": 80,
        "wires": [
            [
                "3f251d1e.167872"
            ]
        ]
    },
    {
        "id": "cf06c435.a31528",
        "type": "function",
        "z": "49d20fae.01266",
        "name": "Extract Values",
        "func": "return [\n    {payload:msg.payload.temperature}, \n    {payload:msg.payload.humidity},\n    {payload:msg.payload[\"barometric-pressure\"]}\n];",
        "outputs": "3",
        "noerr": 0,
        "x": 360,
        "y": 280,
        "wires": [
            [
                "bb4a13de.4faaa",
                "62dd2ada.5449a4",
                "df80eb36.2bf648"
            ],
            [
                "6b80f3f4.1fdb4c",
                "51d9d859.dc84c8"
            ],
            [
                "4075ff77.21db8",
                "919809c4.84ef18"
            ]
        ],
        "outputLabels": [
            "Temperature",
            "Humidity",
            "Barometric Pressure"
        ]
    },
    {
        "id": "f468b186.a264b",
        "type": "ui_group",
        "z": "",
        "name": "Humidity",
        "tab": "d376d602.4e3538",
        "order": 2,
        "disp": true,
        "width": "6"
    },
    {
        "id": "637855dc.2229bc",
        "type": "ui_group",
        "z": "",
        "name": "Temperature",
        "tab": "d376d602.4e3538",
        "order": 1,
        "disp": true,
        "width": "6"
    },
    {
        "id": "9728196c.705ff8",
        "type": "ui_group",
        "z": "",
        "name": "Barometric Pressure",
        "tab": "d376d602.4e3538",
        "order": 3,
        "disp": true,
        "width": "6"
    },
    {
        "id": "d376d602.4e3538",
        "type": "ui_tab",
        "z": "",
        "name": "Living Room Data",
        "icon": "dashboard",
        "order": 2
    }
]

Saturday, November 25, 2017

Node-RED: Subscribing to a MQTT Topic

For our next Node-RED flow, we will build a MQTT client that will subscribe to BME280 sensor data published by an ESP8266. The hardeare and code for publishing that data is described in an earlier post.

The messages published to the MQTT topic home/living-room are CSV strings containing temperature, humidity, and barometric pressure in British units. Here's an example of such a message:

71.55,41.46,29.73

Our goal for this post is to build a Node-RED flow that subscribes to that topic, and parses that data into JSON like this:

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

For this flow, we'll be using the following three types of nodes:

MQTT In
Connects to a MQTT server and subscribes to a specified topic
CSV
Converts CSV data into JSON
Debug
Displays messages in the debug tab

These nodes will be connected as follows:

Double-click the MQTT In node and set the following properties:

Server: 192.168.1.11:1883
Topic: home/living-room
QoS: 0

Next, double-click the CSV node and set the following properties:

Columns: temperature,humidity,barometric-pressure
Separator: Comma
Name: Parse CSV into JSON

Finally, double-click each of the Debug nodes and set the following properties:

Output: complete msg object
to: debug tab

The CSV node uses the column names for the field names, and assigns values to those fields in the order of the values in the incoming CSV. The end result is set to be the message's payload. This is how the messages look in the debug tab:

Pretty-printing the top entry in the debug list shows that the msg object has the following structure:

{
  "topic": "home/living-room",
  "payload":
  {
    "temperature": 73.8,
    "humidity": 37.12,
    "barometric-pressure": 29.6
  },
  "qos": 0,
  "retain": false,
  "_msgid": "1c091ae3.456e45"
}

Great, we've subscribed to a MQTT topic and formatted the incoming CSV data as a JSON object!

Wouldn't it be nice if there were timestamps in these objects? As explained in the post where we built and coded the sensor, our little ESP8266 doesn't have a clock running on battery, nor are we getting time from a network time service. Instead, we will add the timestamp to the data once it arrives in our Node-RED flow.

To accomplish this, we will use a Function node, which will run a block of JS. Wire this Function node into the rest of the flow as follows:

Double-click on the Function node, and set the name to be Add Time Info. In the "Function" textarea, add the following code:

1
2
3
4
var now = new Date();
msg.payload["timestamp"] = now.getTime();
msg.payload["fomratted-time"] = now.toUTCString();
return msg;

All that this code does is to add two additional properties to msg.payload. The JavaScript .getDate() method returns the number of milliseconds since January 1, 1970, and the .toUTCString() formats that data into a human-readable form in the GMT timezone.

Clear the debug tab, deploy the edited flow, and examine the incoming data. Here's an example of what the final result will be, once we pretty-print it:

{
  "topic": "home/living-room",
  "payload": 
  {
    "temperature": 73.76,
    "humidity": 37.35,
    "barometric-pressure": 29.6,
    "timestamp": 1511588927869,
    "formatted-time": "Sat, 25 Nov 2017 05:48:47 GMT"
  },
  "qos": 0,
  "retain": false,
  "_msgid":"3867cbee.a2cfa4"
}

Here's the source code for this last flow:

 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
[
    {
        "id": "ee409dc6.f511b",
        "type": "mqtt in",
        "z": "e260f053.ca405",
        "name": "",
        "topic": "home/living-room",
        "qos": "0",
        "broker": "a481358c.031c18",
        "x": 120,
        "y": 160,
        "wires": [
            [
                "f90ccd18.b0f84",
                "b7c62375.17de5"
            ]
        ]
    },
    {
        "id": "f90ccd18.b0f84",
        "type": "debug",
        "z": "e260f053.ca405",
        "name": "",
        "active": false,
        "console": "false",
        "complete": "true",
        "x": 770,
        "y": 160,
        "wires": []
    },
    {
        "id": "b7c62375.17de5",
        "type": "csv",
        "z": "e260f053.ca405",
        "name": "Parse CSV into JSON",
        "sep": ",",
        "hdrin": "",
        "hdrout": "",
        "multi": "one",
        "ret": "\\n",
        "temp": "temperature,humidity,barometric-pressure",
        "x": 380,
        "y": 220,
        "wires": [
            [
                "5097ac3c.7d4b04"
            ]
        ]
    },
    {
        "id": "adf30d5c.857be",
        "type": "debug",
        "z": "e260f053.ca405",
        "name": "",
        "active": true,
        "console": "false",
        "complete": "true",
        "x": 770,
        "y": 220,
        "wires": []
    },
    {
        "id": "5097ac3c.7d4b04",
        "type": "function",
        "z": "e260f053.ca405",
        "name": "Add Time Info",
        "func": "var now = new Date();\nmsg.payload[\"timestamp\"] = now.getTime();\nmsg.payload[\"fomratted-time\"] = now.toUTCString();\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "x": 600,
        "y": 220,
        "wires": [
            [
                "adf30d5c.857be"
            ]
        ]
    },
    {
        "id": "a481358c.031c18",
        "type": "mqtt-broker",
        "z": "",
        "broker": "192.168.1.11",
        "port": "1883",
        "clientid": "",
        "usetls": false,
        "compatmode": true,
        "keepalive": "60",
        "cleansession": true,
        "willTopic": "",
        "willQos": "0",
        "willPayload": "",
        "birthTopic": "",
        "birthQos": "0",
        "birthPayload": ""
    }
]

What are we going to do with all this data flowing in? That's the topic of the next post!

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.

Wednesday, March 8, 2017

NodeMCU and Analog Sensors: Hardware Diagnostics

This post was originally going to be devoted to how to use an analog temperature sensor with the NodeMCU ESP8266. We will indeed accomplish that goal, but we will first have to take a detour through basic hardware diagnostics!

There are a wide variety of temperature sensors we can use with the NodeMCU; here's three of them:

  • TMP36
  • LM60
  • BME280

The TMP36 and the LM60 are analog sensors, which means they are sensors that, from the point of view of the ESP8266, return data in the form of a continuous value, a voltage.

Of the three, the BME280 returned the most accurate temperature (accuracy determined by my apartment's thermostat). In addition, it also returns barometric pressure and humidity. The BME280 is not an analog sensor, however.

The TMP36 returned fluctuating temperatures, and those temperatures were low.

The LM60's values were far more steady than those returned by the TMP36, but the values were again too low.

We will be using the LM60 for this tutorial. This sensor is made by Texas Instruments, and is repackaged by various vendors, for example MCM Electronics, where it is given a different name: MC16-0362. The part has three pins, VDD for positive power supply, GND for ground, and OUT which is our signal pin.

The ESP8266 has only one analog input pin, called A0. This means that we must connect the signal pin to A0, and the other pins are connected as shown in this diagram:

After reading Texas Instrument's documentation on the LM60, we learn the following:

  1. The LM60 is rated for a temperature range of -40°C to +125°C
  2. It accepts voltage in the range 2.7 V to 10 V
  3. The output voltage is 174 mV at -40°C and 1205 mV at 125°C
  4. The output voltage is linearly proportional to the Celsius temperature
  5. This proportionality is 6.25 mV per degree Celsius - that's the slope of this line
  6. There is an offset of 424 mV - that's the y-intercept

Thus, the LM60 can be powered from the NodeMCU's 3V3 pin, and the return voltage is less than maximum analog-in value of 3.3 V - great!

The documentation also includes the following diagram, which illustrates the linear relationship and values listed above:

The equation describing this line is right on that diagram:

V = 6.25 * T + 424

where:

V is measured in millivolts
T is measured in degrees Celsius

Solving this equation for T gives us:

T = (V - 424)/6.25

Now we need to read the number of millivolts being sent to A0. The A0 pin takes a voltage between 0 and 3.3 V and converts it to the range 0 to 1024, which we'll call the "raw value". A0 is said to have a 10-bit resolution, and 2^10 = 1024. We will assume this is a linear relationship. Thus, to get the value in volts, we multiply by 3.3 and divide by 1024. So:

volts = rawValue * 3.3 / 1024

We combine all this into the following 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
/*
 * LM60TemperatureSensor
 * 
 * By: Mike Klepper
 * Date: 8 March 2017
 * 
 * This program measures ambient temperature using the LM60 analog sensor;
 * it displays the results in the Serial Monitor.
 * 
 * As we'll see in this tutorial, it returns incorrect temperatures!
 */

const int ADC_PIN = A0;
const float ANALOG_TO_VOLTAGE_FACTOR = 3.3/1024.0; //0.00322265625

int rawValue;
float voltage;
float milliVolts;
float tempC;
float tempF;

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

void loop() 
{
  rawValue = analogRead(ADC_PIN);
  voltage = rawValue * ANALOG_TO_VOLTAGE_FACTOR;
  milliVolts = 1000.0 * voltage;
  tempC = (milliVolts - 424.0) / 6.25;
  tempF = 9.0 * tempC / 5.0  + 32.0;
  
  Serial.print("raw value: ");
  Serial.print(rawValue);
  Serial.print("  voltage: ");
  Serial.print(voltage);
  Serial.print("  deg C: ");
  Serial.print(tempC);
  Serial.print("  deg F: ");
  Serial.println(tempF);
  
  delay(2000);
  
  yield();
}

Here's the output in the serial monitor:

According to this, the temperature is 57.46°F. According to my thermostat, the temperature is 68°F. What is the source of discrepancy? Here are some theories:

  1. The temperature of the LM60 component itself was changed by an external source
  2. The thermostat in my apartment is wrong
  3. This particular LM60 is defective
  4. The temperature returned by the LM60 varies based on input voltage
  5. There is a problem with the NodeMCU's ADC pin
  6. There is a calibration error in the LM60

Anything that we would normally do to the LM60 component would RAISE the returned temperature - for example, we had to touch it to attach it to the breadboard. To avoid the effects of our own body heat on the sensor, we avoid touching it for a few minutes and wait for the value to stabilize.

I have tried another thermometer, and it is returning 68.0°F. So the problem is not with the thermostat.

Switching to a different LM60 gives the same temperature reading.

According to TI's documentation, the value returned by the LM60 is not determined by input voltage, so long as the voltage is in the range of 2.7 V to 10 V.

Let's investigate the theory that there is a problem with the NodeMCU's ADC pin. To to this, we have to take out a multimeter - in other words, shit is gettin' real!

Connecting the multimeter between the GND and 3V3 pin, there is indeed 3.3 V going across those pins. When we measure voltage between the GND and A0 pins, we see that it is 0.53 V even though serial output us showing 0.51 V - a discrepancy!

To investigate further, disconnect the LM60 and connect a 10 kΩ potentiometer as follows:

We use the same program as above and focus on the raw value and the voltage that is reported. Turning the potentiometer's knob all the way to the left, we get a raw value of 0 and a voltage of 0.00. Connecting the multimeter to A0 and GND gives us the same voltage. Turning the knob all the way to the right gives us a raw value between 998 and 1001 and voltages of 3.22 V and 3.23 V, respectively. The multimeter shows 3.29 V.

This means that the A0 pin doesn't work as expected - the conversion from the raw value to the voltage is incorrect! As it goes, this is a known bug in the NodeMCU Arduino software.

While the potentiometer is attached, we take additional readings to ensure that the relationship between actual voltage and raw value is linear. Plotting these using an Excel spreadsheet shows that the relationship is indeed linear:

To compensate for this bug, we need only change the value of ANALOG_TO_VOLTAGE_FACTOR from 3.3 / 1024 to 3.29 / 1001. After making this change, the reported temperature is now approximately 59.45°F. Closer to the expected value, but still low.

Finally, let us check for a calibration error in the LM60. An easy way to do this is to put some ice cubes in a plastic bag and hold them against the LM60. The reported temperature is 25.37°F or 24.32°F instead of the expected 32°F.

The results of these experiments are as follows:

Actual Voltage V Actual Temperature °F Actual Temperature °C
0.52 68 20
0.40 32 0

So, assume that the voltage returned by the LM60 is indeed in linear proportion to the temperature, those two data points can be used to calculate the equation of that line.

The slope, m, is (20 - 0)/(0.52 - 0.40) = 20/0.12 = 166.67 °C / volt

Using the point-slope form of the line:

y - y1 = m(x - x1)
where (x1, y1) is (0.52, 20) we get
y - 20 = 166.7*(x - 0.52)
y = 166.7*(x - 0.52) + 20
In terms of the variables in our program, that equation becomes:
tempC = 166.7*(voltage - 0.52) + 20;
The final version of our program is 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
38
39
40
41
42
43
44
45
46
/*
 * LM60TemperatureSensor
 * 
 * By: Mike Klepper
 * Date: 8 March 2017
 * 
 * This program measures ambient temperature using the LM60 analog sensor;
 * it displays the results in the Serial Monitor.
 * 
 * The program includes corrections for a problem with the NodeMCU's AO pin
 * as well as a calibration problem with the LM60 itself.
 */

const int ADC_PIN = A0;
const float ANALOG_TO_VOLTAGE_FACTOR = 3.29/1001.0; //0.00322265625

int rawValue;
float voltage;
float tempC;
float tempF;

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

void loop() 
{
  rawValue = analogRead(ADC_PIN);
  voltage = rawValue * ANALOG_TO_VOLTAGE_FACTOR;
  tempC = 166.7 * (voltage - 0.52) + 20;
  tempF = 9.0 * tempC/5.0  + 32.0;

  Serial.print("raw value: ");
  Serial.print(rawValue);
  Serial.print("  voltage: ");
  Serial.print(voltage);
  Serial.print("  deg C: ");
  Serial.print(tempC);
  Serial.print("  deg F: ");
  Serial.println(tempF);

  delay(2000);

  yield();
}

Here's the output shown in the serial monitor:

The reported temperature is now 67.79°F - not exactly 68.0°F like the thermometer reports, but certainly much closer!

What accounts for this final discrepancy?

The problem is that it is not possible for this program to return exactly 68.0°F! Why is this? Well, the A0 pin returns integer values. If it reports 158, our program calculates the temperature to be 67.79°F. If A0 reports 159, the program calculates the temperature as 68.78°F. The A0 pin is incapable of returning a value between 158 and 159!

This concludes our first foray into using analog sensors with the NodeMCU ESP8266. We encountered problems with the NodeMCU's analog input pin as well as with the sensor's calibration, and we were able to solve both problems using high school algebra.