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.

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!