GPS-equipped portable atmospheric recorder, with LCD and map display

NOTE – all code you see here is in better, complete form at my GitHub page.

As I have returned from Madrid after a week-long seminar with nine students, I thought I’d go thru some of the features of the IoT device we presented to and built with the other students (Swiss and Spanish).

The Seminar is now in its 14th implementation, and it has only been suspended twice, once in 2013 and now 2020 and 2021 for obvious reasons. We started this yearly Seminar in 2006 at Haaga-Helia. The other schools are Copenhagen North who couldn’t make it this year, HES-SO Switzerland, and Universidad Europea. Usually the students get to ideate what we want to make and present at the Seminar, and form a consensus. This year, due to the global shortage of electronic parts, I had to act dictatorial and go shopping before the team was even selected. For example, I wanted to buy long range WLAN (LORAWAN) ESP32s for building this device, but they just weren’t available right now.

I found other parts on Amazon.de and a local electronics shop, Partco, to build a portable device that has

  • a GPS device
  • a sensor with temperature and atmospheric pressure
  • an LCD display,
  • an ESP32 to drive it
  • the capability to send the measured latitude, longitude, altitude, and number of seen satellites as well as the air variables to a file on the Web, and
  • a PHP file that can take the readings from the files and show them on an online map.

This list I presented as a fait accompli to the newly selected team, and let them form five subteams.

The idea of the seminar is to have each country present something to the other students, who have been mixed into multinational teams. This time, the Finnish students will walk the others through installing a display, then adding an atmospheric sensor, then the GPS sensor, making the ESP32 connect to the Web, send the data, and finally, write the PHP.

The LCD display

The LCD is the same LCD 1604 as I have shown you in the servo rotation speed counter, with a little difference. The ESP32 is using the I2C protocol to send the data to the display, and in the rotation speed example, it is an Arduino. The difference is that Arduino uses analog pins A4 and A5 as the SDA (data) and SCL (clock) pins, but in the ESP32, the preferred pins for I2C are 21 and 22. Anyway, here’s the Random Nerd Tutorial example file for connecting the LCD:

#include <LiquidCrystal_I2C.h>

// set the LCD number of columns and rows
int lcdColumns = 20;
int lcdRows = 4;

// set LCD address, number of columns and rows
// if you don't know your display address, run an I2C scanner sketch
LiquidCrystal_I2C lcd(0x27, lcdColumns, lcdRows);  

void setup(){
  // initialize LCD
  lcd.init();
  // turn on LCD backlight                      
  lcd.backlight();
}

void loop(){
  // set cursor to first column, first row
  lcd.setCursor(0, 0);
  // print message
  lcd.print("Hello, World!");
  delay(1000);
  // clears the display to print new message
  lcd.clear();
  // set cursor to first column, second row
  lcd.setCursor(0,1);
  lcd.print("Hello, World!");
  delay(1000);
  lcd.clear(); 
} 

The BMP280 sensor

Next, we connect the atmospheric sensor BMP280. There are two different BM* 280 versions, of which BMP delivers air pressure and temperature, while BME280 measures air humidity and temperature. The libraries for each can be installed into the Arduino IDE via the Manage Libraries / Search system. Make sure you get the proper one for your device, they are not interchangeable.

BMP280 with pins. You can see this is a tiny sensor.

Since the BMP280 is another I2C device, you can use the same pins 21 and 22 on ESP32 to drive this sensor too. The address, which is similar to the one set up above – LiquidCrystal_I2C lcd(0x27, lcdColumns, lcdRows) – is probably 0x56 for the BMP280, but if you have issues, you may want to scan for the I2C addresses. The address can be set up in the command that starts the device.

By the way, the easiest way to deal with two devices wanting to share two pins on the ESP32 is to lead the pins into holes 21 and 22 on a breadboard, then leading the wires to each device from the breadboard.

This is the sample code that is available after you install the library for this device:

#include <Wire.h>
#include <SPI.h>
#include <Adafruit_BMP280.h>

Adafruit_BMP280 bmp; // I2C

void setup() {
  Serial.begin(9600);
  while ( !Serial ) delay(100);   // wait for native usb
  Serial.println(F("BMP280 test"));
  unsigned status;
  //status = bmp.begin(BMP280_ADDRESS_ALT, BMP280_CHIPID);
  status = bmp.begin();
  if (!status) {
    Serial.println(F("Could not find a valid BMP280 sensor, check wiring or "
                      "try a different address!"));
    Serial.print("SensorID was: 0x"); 
    Serial.println(bmp.sensorID(),16);
    Serial.print("        ID of 0xFF probably means a bad address, a BMP 180 or BMP 085\n");
    Serial.print("        ID of 0x56-0x58 represents a BMP 280,\n");
    Serial.print("        ID of 0x60 represents a BME 280.\n");
    Serial.print("        ID of 0x61 represents a BME 680.\n");
    while (1) delay(10);
  }

  /* Default settings from datasheet. */
  bmp.setSampling(Adafruit_BMP280::MODE_NORMAL,     /* Operating Mode. */
                  Adafruit_BMP280::SAMPLING_X2,     /* Temp. oversampling */
                  Adafruit_BMP280::SAMPLING_X16,    /* Pressure oversampling */
                  Adafruit_BMP280::FILTER_X16,      /* Filtering. */
                  Adafruit_BMP280::STANDBY_MS_500); /* Standby time. */
}

void loop() {
    Serial.print(F("Temperature = "));
    Serial.print(bmp.readTemperature());
    Serial.println(" *C");

    Serial.print(F("Pressure = "));
    Serial.print(bmp.readPressure());
    Serial.println(" Pa");

    Serial.print(F("Approx altitude = "));
    Serial.print(bmp.readAltitude(1013.25)); /* Adjusted to local forecast! */
    Serial.println(" m");

    Serial.println();
    delay(2000);
}

Note the local forecast pressure to be adjusted in the last few lines. Check with the local meteorological institute for the current pressure at ground level to make this device function properly.

The GPS sensor

The GPS sensor we want to use is the UART GPS NEO-6M, which is very neat and surprisingly easy to take into use It uses two other pins on the ESP32, ie. 12 and 13 for its receiving and transmitting, so it doesn’t want to crash the party going on at pins 21 and 22.

GPS receiver with antenna. Do not start the sensor without the antenna.

The sentences that GPS receivers deliver are not the easiest of things to understand. Luckily there is the library TinyGPSPlus-ESP32 which takes in the data and renders it into variables, which you can then access. Here’s the code you need to set it up.

#include <TinyGPSPlus.h>
/* 
   This sample sketch should be the first you try out when you are testing a TinyGPSPlus
   (TinyGPSPlus) installation.  In normal use, you feed TinyGPSPlus objects characters from
   a serial NMEA GPS device, but this example uses static strings for simplicity.
*/

// A sample NMEA stream.
const char *gpsStream =
  "$GPRMC,045103.000,A,3014.1984,N,09749.2872,W,0.67,161.46,030913,,,A*7C\r\n"
  "$GPGGA,045104.000,3014.1985,N,09749.2873,W,1,09,1.2,211.6,M,-22.5,M,,0000*62\r\n"
  "$GPRMC,045200.000,A,3014.3820,N,09748.9514,W,36.88,65.02,030913,,,A*77\r\n"
  "$GPGGA,045201.000,3014.3864,N,09748.9411,W,1,10,1.2,200.8,M,-22.5,M,,0000*6C\r\n"
  "$GPRMC,045251.000,A,3014.4275,N,09749.0626,W,0.51,217.94,030913,,,A*7D\r\n"
  "$GPGGA,045252.000,3014.4273,N,09749.0628,W,1,09,1.3,206.9,M,-22.5,M,,0000*6F\r\n";

// The TinyGPSPlus object
TinyGPSPlus gps;

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

  Serial.println(F("BasicExample.ino"));
  Serial.println(F("Basic demonstration of TinyGPSPlus (no device needed)"));
  Serial.print(F("Testing TinyGPSPlus library v. ")); Serial.println(TinyGPSPlus::libraryVersion());
  Serial.println(F("by Mikal Hart"));
  Serial.println();

  while (*gpsStream)
    if (gps.encode(*gpsStream++))
      displayInfo();

  Serial.println();
  Serial.println(F("Done."));
}

void loop()
{
}

void displayInfo()
{
  Serial.print(F("Location: ")); 
  if (gps.location.isValid())
  {
    Serial.print(gps.location.lat(), 6);
    Serial.print(F(","));
    Serial.print(gps.location.lng(), 6);
  }
  else
  {
    Serial.print(F("INVALID"));
  }

  Serial.print(F("  Date/Time: "));
  if (gps.date.isValid())
  {
    Serial.print(gps.date.month());
    Serial.print(F("/"));
    Serial.print(gps.date.day());
    Serial.print(F("/"));
    Serial.print(gps.date.year());
  }
  else
  {
    Serial.print(F("INVALID"));
  }

  Serial.print(F(" "));
  if (gps.time.isValid())
  {
    if (gps.time.hour() < 10) Serial.print(F("0"));
    Serial.print(gps.time.hour());
    Serial.print(F(":"));
    if (gps.time.minute() < 10) Serial.print(F("0"));
    Serial.print(gps.time.minute());
    Serial.print(F(":"));
    if (gps.time.second() < 10) Serial.print(F("0"));
    Serial.print(gps.time.second());
    Serial.print(F("."));
    if (gps.time.centisecond() < 10) Serial.print(F("0"));
    Serial.print(gps.time.centisecond());
  }
  else
  {
    Serial.print(F("INVALID"));
  }

  Serial.println();
}

This is only a partial set of the data that is available from the GPS sensor. In the delightfully named example, KitchenSink.ino, you can see every bit of information that can be gleaned from the GPS system.

ESP32 data output to the Web

This is a simple thing. Make your ESP32 aware of a local access point – we will use mobile phone hotspots for this. All you need to do is to install the Wifi and HTTPClient libraries, then use something like this code. The stuff below has the relevant parts, but won’t work as such, you need to edit it to suit your needs.

First I’ll show the PHP file that will receive your sent data. It merely listens to the server’s input channel, and whatever it gets, it writes into a text file:

<?php
    $_POST = file_get_contents('php://input');
    $myYear = date("d.m.Y");
    $myHour = date("H:i");
    $myFile = "my_iot.txt";
    $fh = fopen($myFile, 'a') or die("can't open file");
    fwrite($fh, $myYear . "," .  $myHour);
    fwrite($fh, ",");
    fwrite($fh, $_POST);
    fwrite($fh, "\n");    
    fclose($fh);
?>

The thing you must note is the filename my_iot.txt. This script will just append the data it receives to the file, and if it doesn’t exist for some reason, it’ll create a new one. It is comma separated, so there are commas after date and time. The data is comma separated as well, so it’s easy to parse into variables.

#include <WiFi.h>   //wifi library
#include <HTTPClient.h> //http client
// network settings
const char* ssid       = "YOUR_SSID";
const char* password   = "YOUR_PASSWORD";
const char* serverName = "YOUR_WEB_INTERFACE"; // for example http://www.yourserver.com/gps.php
String WebMessage
void setup() {

  Serial.begin(115200);
  while (WiFi.status() != WL_CONNECTED) {
    delay(200);
    Serial.print(".");

    WiFi.begin(ssid, password);
    Serial.println("Connecting");
    while (WiFi.status() != WL_CONNECTED) {
      delay(500);
      Serial.print(".");
    }
    Serial.println("");
    Serial.print("Connected to WiFi network with IP Address: ");
    Serial.println(WiFi.localIP());
    UpdateActivity("Booted");
  }
}

void loop() { //this is just a sample, won't work as such
              //note the comma separated values

  WebMessage = strLat + ", "
               + strLon + ", "
               + strPressure + ", "
               + strTemperature + ", "
               + strLat + ", " +
               + strNumSats + ", ";
  UpdateActivity(webMessage);
  delay (10000);
}

void UpdateActivity(String myActivity) {
  //sending data to the server:

  Serial.println("add data to web");
  Serial.printf("Connecting to %s ", ssid);
  if (WiFi.status() == WL_CONNECTED) {
    WiFiClient client;
    HTTPClient http;

    // Your Domain name with URL path or IP address with path
    http.begin(serverName);

    // If you need an HTTP request with a content type: text/plain
    http.addHeader("Content-Type", "text/plain");
    String httpRequestData = myActivity
    // Send HTTP POST request
    int httpResponseCode = http.POST(httpRequestData);

    Serial.println("HTTP Response code: ");
    Serial.println(httpRequestData);

    // Free resources
    http.end();
  }
  else {
    Serial.println("WiFi Disconnected");
  }
}

This will send the string myActivity to the web page, and it looks like this on the server:

31.03.2022,17:55,60.201082,24.933874,24.65,1008.67,65.20,7

This is then easy to parse into relevant variables for your map to digest and show.

Reading the web data and showing a marker on the map

For this, I will rely on code developed by one of my students, Vincent Le, who was really quick on learning PHP and putting together two versions of the map code – many thanks for allowing me to share the files! He was also instrumental in editing the map end until the very end of our day at the Seminar. His site is really interesting and you should pay it a visit.

The code for this part is on my GitHub page, in the folders /gps-singlemap and /gps-multimap. Have a look over there to see the code while you read this.

Here is a sample of the data that gets collected into the iotwrite.php file:

27.04.2022 12:48	60.201167,24.933687,23.65,1021.37,42.00,6
27.04.2022 12:48	60.201167,24.933687,23.65,1021.37,42.00,6
27.04.2022 12:48	60.201595,24.934066,23.65,1021.35,42.00,6
27.04.2022 12:49	60.201595,24.934066,23.65,1021.36,42.00,6
27.04.2022 12:49	60.201595,24.934066,23.65,1021.35,42.00,6
27.04.2022 12:49	60.201649,24.934002,23.66,1021.35,42.00,6

To show the data, we needed two versions of the map code. One is to show the location of each box, and the other is to get all ten boxes’ data and show all of them on the same map. In multimap view, clicking on the map marker will show you the data associated with the box, in single map view the data is shown all the time.

The Fetch Data button is used in Vincent’s system to update the location, but there is nothing to keep you from updating the page with Refresh at any interval you choose.

We had nine boxes for the teams, hence the folder structure was arranged so that we had subfolders like /madrid/iot01 —/madrid/iot09. In the subfolder /madrid/iot00 we had the multimap view. Every folder had an index.php file, which read the data and showed it on the map. There is also another file there for extracting the data from the file for the index.php file. The difference of the two extraction files is that in the multimap version, the files are in an array of nine items, whereas in the single map file, the array has only one item.

The difference of any single map index file and the multimap file is that the multimap uses a loop to pass through the files listed in the array, while the single map just passes through the one coordinate and atmospheric data file. When you go to Github, you can take into use whichever you please. In both files you need to edit in your server data.

We started our day with a presentation on IoT, followed by the installation of Arduino IDE with ESP32 support. We used the WifiScan example file to make sure the integrated development environment works. After that, it took us from maybe ten o’clock to 15.30 to build the devices, first the LCD, then the BMP20, followed by the GPS. When each bit worked to a sufficient degree, we took out the master file that has all the functionality, uploaded it to the numbered boxes, then went out to see that satellite acquisition worked,

Our students each helped one Spanish and one Swiss student to get the hang of first Arduino IDE, then understand the peculiarities of the ESP32 and then showing them wire by wire how to install the parts and how they correspond to the code. At least one team experimented further using the LCD display and created alternate datasets to show at one second intervals. That is the true spirit of the IT Seminar, going above and beyond what you are shown.

What I didn’t tell the students was that I had Box #11. So, I slipped away at the time when all teams were too excited while testing their box and went down through the buildings B and A, to the corner of the tennis courts to hide. Then I sent my students this picture:

Faculty member location data

Since they didn’t know that the path /iot11 existed and had the map in it, they had to resort to moving around with their box and see every ten seconds if the numbers moved in the right direction in relation to my numbers. I was watching the boxes move on /iot00:

Watching the teams approach me.

I was sitting on a bench next to the tennis courts, under the text “edificio A” on the map, but this map doesn’t show my mystery box. Following the teams’ movement was fun in the extreme, especially seeing two teams embarking on a wild goose chase to the northwest (near Calle Tajo in the image). Finally two teams saw me at the same time and ran for the bag of candy, and it was won by the team whose point man crashed through a hedge, tumbled on the asphalt skinning his knee, and finally stopped against the tennis court wire fence.

True sporting spirit there, don’t you think?

I had another bag of candy for everyone else, so we retired to the seminar classroom to wrap up the day’s learnings. In essence we showed the students that to get going with IoT is yet again easy but takes perseverance and a good nose for finding examples that actually work.

Demo of the device booting and running

Leave a Reply

Your email address will not be published.

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.