ESP-32 Flight Datalogger – Field Validation
With all of the final code validated on the workbench, it was time to test it in the field. The project deadline was 3 weeks away and the field testing was a formality to ensure nothing was missing before assembly. With all of the bench testing done, it seemed that this would take a single day to complete and the next phase would begin.
Field testing involves taking the hardware outside and testing it in a state that closely matches its final form. The hardware is rather raw and not complete, so care must be taken to not disturb or mishandle it. This was the case with this project, the hardware and wiring were fastened down and secure. The process for activating and controlling the hardware was well rehearsed. It behaved with high reliability on the bench, so it was the best it would be for field testing.
When the tests began, it became apparent early on that something wasn’t working. The GPS module was failing to get a reliable lock or it was taking a longer than expected time to get a lock. The first thing to suspect were loose wiring. But all of these checked fine. The field testing took a sour turn, the unreliable GPS lock issue had to be identified before clearing the firmware for the hardware build phase.
Out came the test equipment. The GPS was connected to a serial monitor, however it operated without issue. The GPS unit by itself acquired a lock quickly and consistently. The GPS mockup was tested again and the system operated as expected. However, the GPS lock issue continued to plague the project when the GPS was connected to the ESP32-Cam module.
This went on for a couple of days, the pressure was on to get a fix, pun intended. Identifying the problem was the first hurdle, the next would be correcting it, while the last would be validated field testing. This meant going back to the firmware development stage and bench hardware. Each interface of the ESP32-Cam module was scrutinized. The project had originally used hardware UART pins as serial debug for development. The GPS serial was received on a software serial pin that was assigned to another GPIO pin. The mockup worked without issue in this configuration. However, the GPS module had issues with that setup. The GPIO pin assigned as a software serial input exhibited a noticeable voltage drop, changing the logic recognized by the ESP32-Cam module. No voltage drop occurred when the GPS was connected to the hardware serial pin of the ESP32-Cam module. This meant that the firmware would need to be reworked to accommodate this requirement.
See notes “Supported Interfaces_Ver5.txt”
ESP-32 Cam GPIO0 CSI_MCLK, I2C SLC, this can't be grounded at startup, it's the program pin GPIO1 U0TXD, WRITE INT, used for DEBUG console during DEV GPIO3 U0RXD, GPS RXD GPIO16 U2RXD, I2C SDA GPS GP-20U7 UART Transmit - This will connect to ESP-32 GPIO3 (U0RXD) Volt/Amp Sensor - INA219 This uses I2C, a bit of a challenge, requires 2 data pins SLC - This will connect to ESP-32 GPIO0 SDA - This will connect to ESP-32 GPIO16 I also need a file write interupt to prevent data corruption at power down. GPIO1 - Set Pin HIGH for data write, LOW to stop data write Set this option when all debuging is done Notes Field tests were spotty using software serial or U2RXD (GPIO16) for input from the GPS. Only U0RXD (GPIO3) reliably recieved and processed GPS data promptly and consistantly. Earlier dev noted that the GPS logic levels dropped from 3.3v to 1.5v when connected to any GPIO other than GPIO3. Also noted that the bootup message on DEBUG had system memory errors, but cleared on subsequent reboots. Write interupt works fine using the U0TXD pin if the following are followed. When hardware is initialized, Start both GPIO1 and GPIO3 as hardware serial ports Then set GPIO1 pin as an input, followed by defining its initial state
Back to the bench, the code changes were made. The revision involved a global change to all of the GPIOs used on the ESP32-Cam module to all of the sensors. Fortunately, the code was organized in a way that the time involved for a new version was minimal. The I2C data bus, write interrupt, GPS input, and serial debug were tested one by one and passed. The hardware was once again ready for field testing.
See code set “FlightData_Final_Ver9.ino”
/** FlightData_Final_Ver9.ino Pressure and Temperature Sensor Merge July 22nd, 2021 - 04:21 **/ // Libraries #include "FS.h" #include "SD_MMC.h" #include <TinyGPS++.h> #include <Wire.h> #include <Adafruit_INA219.h> #include "WiFi.h" #include <Adafruit_BMP085.h> // Variables and Constants // Set these based on your needs // GPS Home, https://www.google.com/maps static const double SixtyAcres_LAT = 47.702875, SixtyAcres_LON = -122.137535; // Current baromentric pressure, https://tides4fishing.com/ uint32_t inHg = 30.03; TinyGPSPlus gps; // The TinyGPS++ object unsigned long last = 0UL; // For stats that happen every 5 seconds int PadSeconds; int PadMinutes; int PadDays; int PadMonths; Adafruit_INA219 ina219(0x40); // Initializes I2C communication with the Adafruit_INA219 device address 0x40 uint32_t currentFrequency; float shuntvoltage = 0; float busvoltage = 0; float current_mA = 0; float loadvoltage = 0; float power_mW = 0; int WifiNetworks = 0; int NetworkCount = 0; Adafruit_BMP085 bmp; // can this be addressed? uint32_t mbar = inHg / .02953; // convert inHg to mbar uint32_t Pa = mbar / .01; // convert mbar to Pa int InteruptState = 1; int FaultState = 0; int WarmUpState = 1; const char* FileName = "/FLTDATA.CSV"; #define FlashLED 4 #define WriteInt 1 #define StautsLED 33 // REMEMBER - The ESP32-CAM has Reverse logic, HIGH is off, LOW is on. #define SCL 0 #define SDA 16 // Routines and Subroutines // Hardware Initialization Routine void HardwareInit() { Serial.begin(9600); Wire.begin(SDA, SCL); WiFi.mode(WIFI_STA); WiFi.disconnect(); pinMode(StautsLED, OUTPUT); pinMode(FlashLED, OUTPUT); pinMode(WriteInt, INPUT); digitalWrite(FlashLED, LOW); digitalWrite(StautsLED, HIGH); // The ESP32-Cam Module uses reverse logic, so HIGH = off and LOW = on delay(5000); // give GPS time to settle } void StartupMicrSD() { if(!SD_MMC.begin()){ SystemHalt(); return; } uint8_t cardType = SD_MMC.cardType(); if(cardType == CARD_NONE){ SystemHalt(); return; } } //Append to the end of file in SD card void appendFile(fs::FS &fs, const char * path, const char * message){ File file = fs.open(path, FILE_APPEND); if(!file){ return; } if(file.print(message)){ } else { } } //Sample Data Write for Main Loop void WriteInterupt() { InteruptState = digitalRead(WriteInt); if (InteruptState == 0) { while (InteruptState == 0){ InteruptState = digitalRead(WriteInt); digitalWrite(StautsLED, LOW); delay(50); digitalWrite(StautsLED, HIGH); delay(950); } } } //Sample Data Write for Main Loop void SystemHalt() { FaultState = 1; while (FaultState == 1){ digitalWrite(StautsLED, LOW); delay(50); digitalWrite(StautsLED, HIGH); delay(50); } } // Fault Detetection and Alerting Routine void FaultDetect() { if (! bmp.begin ()) { digitalWrite(StautsLED, LOW); while (1) { delay(10); } } if (! ina219.begin()) { digitalWrite(StautsLED, LOW); while (1) { delay(10); } } } // Clear Fault Alert Routine void ClearFaultAlert() { digitalWrite(StautsLED, HIGH); } // Sensor Calibraion Scaling Routine void SensorCalibration() { ina219.setCalibration_16V_400mA(); } // Get Voltage Sensor Value Routine void GetVoltSensorValues() { shuntvoltage = ina219.getShuntVoltage_mV(); busvoltage = ina219.getBusVoltage_V(); current_mA = ina219.getCurrent_mA(); power_mW = ina219.getPower_mW(); loadvoltage = busvoltage + (shuntvoltage / 1000); } // WiFi Scanning Routine void WifiScanning() { // WiFi.scanNetworks will return the number of networks found WifiNetworks = WiFi.scanNetworks(); if (WifiNetworks == 0) { } else { String StringWifiNetworks = String(WifiNetworks); appendFile(SD_MMC, FileName, StringWifiNetworks.c_str()); appendFile(SD_MMC, FileName, ","); for (NetworkCount = 0; NetworkCount < 3; ++NetworkCount) { String StringSSID = String(WiFi.SSID(NetworkCount)); appendFile(SD_MMC, FileName, StringSSID.c_str()); appendFile(SD_MMC, FileName, ","); String StringRSSI = String(WiFi.RSSI(NetworkCount)); appendFile(SD_MMC, FileName, StringRSSI.c_str()); appendFile(SD_MMC, FileName, ","); String StringEncryptionType = String((WiFi.encryptionType(NetworkCount) == WIFI_AUTH_OPEN)?" ":"*"); appendFile(SD_MMC, FileName, StringEncryptionType.c_str()); appendFile(SD_MMC, FileName, ","); } } } // Printout Header Routine void PrintHeader() { appendFile(SD_MMC, FileName, "Lat,Long,Date,Time,MPH,Course,Altitude,Home,Bearing,Cardinal,Bus V,Shunt mV,Load V,mA,mW,Temp C,Temp F,Def Alt M,Cal Alt M, Cal Alt F,Networks,SSID-1,RSSID-1,Enc-1,SSID-2,RSSID-2,Enc-2,SSID-3,RSSID-3,Enc-3\n"); } // Printout Location Routine void PrintOutLocation() { String StringLat = String(gps.location.lat(), 6); String StringLong = String(gps.location.lng(), 6); String Location = String(StringLat + "," + StringLong + ","); appendFile(SD_MMC, FileName, Location.c_str()); } // Printout Date Routine void PrintOutDate() { PadMonths = gps.date.month(); if (PadMonths < 10) { appendFile(SD_MMC, FileName, "0"); } String StringMonth = String(gps.date.month()); appendFile(SD_MMC, FileName, StringMonth.c_str()); appendFile(SD_MMC, FileName, "/"); PadDays = gps.date.day(); if (PadDays < 10) { appendFile(SD_MMC, FileName, "0"); } String StringDay = String(gps.date.day()); appendFile(SD_MMC, FileName, StringDay.c_str()); appendFile(SD_MMC, FileName, "/"); String StringYear = String(gps.date.year()); appendFile(SD_MMC, FileName, StringYear.c_str()); appendFile(SD_MMC, FileName, ","); } // Printout Time Routine void PrintOutTime() { String StringHour = String(gps.time.hour()); appendFile(SD_MMC, FileName, StringHour.c_str()); appendFile(SD_MMC, FileName, ":"); PadMinutes = gps.time.minute(); if (PadMinutes < 10) { appendFile(SD_MMC, FileName, "0"); } String StringMinutes = String(gps.time.minute()); appendFile(SD_MMC, FileName, StringMinutes.c_str()); appendFile(SD_MMC, FileName, ":"); PadSeconds = gps.time.second(); if (PadSeconds < 10) { appendFile(SD_MMC, FileName, "0"); } String StringSeconds = String(gps.time.second()); appendFile(SD_MMC, FileName, StringSeconds.c_str()); appendFile(SD_MMC, FileName, ","); } // Printout Speed Routine void PrintOutSpeed() { String StringMph = String(gps.speed.mph()); appendFile(SD_MMC, FileName, StringMph.c_str()); appendFile(SD_MMC, FileName, ","); } // Printout Course Routine void PrintOutCourse() { String StringCourse = String(gps.course.deg()); appendFile(SD_MMC, FileName, StringCourse.c_str()); appendFile(SD_MMC, FileName, ","); } // Printout Altitude Routine void PrintOutAltitude() { String StringAltitude = String(gps.altitude.feet()); appendFile(SD_MMC, FileName, StringAltitude.c_str()); appendFile(SD_MMC, FileName, ","); } // Reference Bearing Routine void BearingReference() { double distanceToSixtyAcres = TinyGPSPlus::distanceBetween( gps.location.lat(), gps.location.lng(), SixtyAcres_LAT, SixtyAcres_LON); double courseToSixtyAcres = TinyGPSPlus::courseTo( gps.location.lat(), gps.location.lng(), SixtyAcres_LAT, SixtyAcres_LON); String StringDistToHome = String(distanceToSixtyAcres/1609, 6); appendFile(SD_MMC, FileName, StringDistToHome.c_str()); appendFile(SD_MMC, FileName, ","); String StringCourseToHome = String(courseToSixtyAcres, 6); appendFile(SD_MMC, FileName, StringCourseToHome.c_str()); appendFile(SD_MMC, FileName, ","); String StringCardinal = String(TinyGPSPlus::cardinal(courseToSixtyAcres)); appendFile(SD_MMC, FileName, StringCardinal.c_str()); appendFile(SD_MMC, FileName, ","); } // Printout Results Routine void PrintOutINA219Readings() { String StringBusVoltage = String(busvoltage); appendFile(SD_MMC, FileName, StringBusVoltage.c_str()); appendFile(SD_MMC, FileName, ","); String StringShuntVoltage = String(shuntvoltage); appendFile(SD_MMC, FileName, StringShuntVoltage.c_str()); appendFile(SD_MMC, FileName, ","); String StringLoadVoltage = String(loadvoltage); appendFile(SD_MMC, FileName, StringLoadVoltage.c_str()); appendFile(SD_MMC, FileName, ","); String StringCurrentmA = String(current_mA); appendFile(SD_MMC, FileName, StringCurrentmA.c_str()); appendFile(SD_MMC, FileName, ","); String StringPowermA = String(power_mW); appendFile(SD_MMC, FileName, StringPowermA.c_str()); appendFile(SD_MMC, FileName, ","); } // Printout Results Routine void PrintOutGY65Readings() { String StringTemperatureC = String(bmp.readTemperature ()); appendFile(SD_MMC, FileName, StringTemperatureC.c_str()); appendFile(SD_MMC, FileName, ","); String StringTemperatureF = String(bmp.readTemperature () * 9 / 5 + 32); appendFile(SD_MMC, FileName, StringTemperatureF.c_str()); appendFile(SD_MMC, FileName, ","); String StringDefAltitudeM = String(bmp.readAltitude ()); appendFile(SD_MMC, FileName, StringDefAltitudeM.c_str()); appendFile(SD_MMC, FileName, ","); String StringCalAltitudeM = String(bmp.readAltitude (Pa)); appendFile(SD_MMC, FileName, StringCalAltitudeM.c_str()); appendFile(SD_MMC, FileName, ","); String StringCalAltitudeF = String(bmp.readAltitude (Pa) * 3.2808); appendFile(SD_MMC, FileName, StringCalAltitudeF.c_str()); appendFile(SD_MMC, FileName, ","); } // Inbound Serial Data Processing void InboundSerialData() { while (Serial.available() > 0) gps.encode(Serial.read()); if (gps.altitude.isUpdated()) { PrintOutGlobalReadings(); } else if (millis() - last > 5000) { if (gps.location.isValid()) last = millis(); } } // Printout Global Results Routine void PrintOutGlobalReadings() { PrintOutLocation(); PrintOutDate(); PrintOutTime(); PrintOutSpeed(); PrintOutCourse(); PrintOutAltitude(); BearingReference(); GetVoltSensorValues(); PrintOutINA219Readings(); PrintOutGY65Readings(); WifiScanning(); appendFile(SD_MMC, FileName, "\n"); } void setup() { HardwareInit(); StartupMicrSD(); FaultDetect(); SensorCalibration(); ClearFaultAlert(); PrintHeader(); } void loop() { WriteInterupt(); InboundSerialData(); }
Now the deadline was 2 weeks away, so the pressure for a passing result was at its highest. The first test had the GPS lock take within 30 seconds. The following day, three additional tests were done in varying manners with all passing results. The firmware was ready for the hardware build phase.
Now with the project ready for building, hardware used in the development would need to be sourced for the final builds. The next section will cover a supply chain challenge and late phase changes to firmware.