ESP-32 Flight Datalogger – Turning Code into Blocks
Developing firmware for hardware typically is not a ground up method. Many AVR and ESP projects that exist have been developed using pre-developed code. This can be the main code base or libraries that provide support. Libraries are easier to adopt into a project because they can be defined and called when needed. Code base reuse doesn’t offer this. Much of it is specific for the application it was developed for. As a result, the code base will need to be edited. Turning the code base into blocks of routines can provide the developer reuse code that can be adopted for other purposes.
Determining which code base to develop as the foundation depends on what function all others will revolve around. Since the process used on the ESP32-Cam module project will be a super loop and linear in nature, the primary purpose of logging GPS data will be that foundation. So this can can be called the primary function. The code base will be example code demonstrating the GPS module, so the TinyGPS++ example was used as the code base. When code is run, there will be the standard setup function followed by the main loop function. Instead of placing all of the code in either of these functions, sub functions will be created that serve a specific purpose.
Here is an example, see code “GPS_SoftwareSerial_TinyGPS++_Ver9.ino”
/** GPS_SoftwareSerial_TinyGPS++_Ver9.ino Base Results from Altitude Readings July 1st, 2021 - 22:07 **/ // Libraries #include <TinyGPS++.h> #include <SoftwareSerial.h> // Variables and Constants TinyGPSPlus gps; // The TinyGPS++ object SoftwareSerial SoftSerial(8, 7); // The serial connection to the GPS device unsigned long last = 0UL; // For stats that happen every 5 seconds static const double SixtyAcres_LAT = 47.702875, SixtyAcres_LON = -122.137535; int PadSeconds; int PadMinutes; int PadDays; int PadMonths; // Routines and Subroutines // Hardware Initialization Routine void HardwareInit() { Serial.begin(115200); SoftSerial.begin(9600); } // GPS Reading Fault Routine void GPSReadingFault() { if (gps.charsProcessed() < 10) Serial.println(F("WARNING: No GPS data. Check wiring.")); } // Printout Header Routine void PrintHeader() { Serial.println(F("Lat,Long,Date,Time,MPH,Course,Altitude,Home,Bearing,Cardinal")); } // Printout Location Routine void PrintOutLocation() { Serial.print(gps.location.lat(), 6); Serial.print(F(",")); Serial.print(gps.location.lng(), 6); Serial.print(F(",")); } // Printout Date Routine void PrintOutDate() { PadMonths = gps.date.month(); if (PadMonths < 10) {Serial.print(F("0"));} Serial.print(gps.date.month()); Serial.print(F("/")); PadDays = gps.date.day(); if (PadDays < 10) {Serial.print(F("0"));} Serial.print(gps.date.day()); Serial.print(F("/")); Serial.print(gps.date.year()); Serial.print(F(",")); } // Printout Time Routine void PrintOutTime() { Serial.print(gps.time.hour()); Serial.print(F(":")); PadMinutes = gps.time.minute(); if (PadMinutes < 10) {Serial.print(F("0"));} Serial.print(gps.time.minute()); Serial.print(F(":")); PadSeconds = gps.time.second(); if (PadSeconds < 10) {Serial.print(F("0"));} Serial.print(gps.time.second()); Serial.print(F(",")); } // Printout Speed Routine void PrintOutSpeed() { Serial.print(gps.speed.mph()); Serial.print(F(",")); } // Printout Course Routine void PrintOutCourse() { Serial.print(gps.course.deg()); Serial.print(F(",")); } // Printout Altitude Routine void PrintOutAltitude() { Serial.print(gps.altitude.feet()); Serial.print(F(",")); } // 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); Serial.print(distanceToSixtyAcres/1609, 6); Serial.print(F(",")); Serial.print(courseToSixtyAcres, 6); Serial.print(F(",")); Serial.print(TinyGPSPlus::cardinal(courseToSixtyAcres)); } // Inbound Serial Data Processing void InboundSerialData() { while (SoftSerial.available() > 0) gps.encode(SoftSerial.read()); if (gps.altitude.isUpdated()) { PrintOutLocation(); PrintOutDate(); PrintOutTime(); PrintOutSpeed(); PrintOutCourse(); PrintOutAltitude(); BearingReference(); Serial.println(); } else if (millis() - last > 5000) { if (gps.location.isValid()) GPSReadingFault(); last = millis(); } } void setup() { HardwareInit(); PrintHeader(); } void loop() { InboundSerialData(); }
The code starts with a title comment section followed by Libraries then Variables and Constants. Underneath that are the Routines and Subroutines. The routines follow this hierarchy. Setup routine calls the Hardware Initialization routine, this is only run once. Then the PrintHeader routine outputs the row containing the descriptions of each column. The super loop routine calls the InboundSerialData routine which then outputs the readings from the GPS module. It loops through this every time there is a data stream from the GPS module.
If the objective were to store the results on microSD media, then a PrintResults routine would need to be created. The rest of the code base can be reused.
Here is another example, see code “Adafruit INA219 Current Sensor_Ver6.ino”
/** Adafruit INA219 Current Sensor_Ver6.ino Completed Blocking of Code June 30th, 2021 - 07:52 **/ // Libraries #include <Wire.h> #include <Adafruit_INA219.h> // Variables and Constants 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; // Routines and Subroutines // 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); } // Printout Header Routine void PrintHeader() { Serial.println("Bus Voltage,Shunt Voltage,Load Voltage,Current,Watts"); } // Printout Results Routine void PrintOut() { Serial.print(busvoltage); Serial.print(","); Serial.print(shuntvoltage); Serial.print(","); Serial.print(loadvoltage); Serial.print(","); Serial.print(current_mA); Serial.print(","); Serial.print(power_mW); Serial.println(""); } // Timer Delay Routine void TimerDelay() { delay(1000); } // Hardware Initialization Routine void HardwareInit() { pinMode(LED_BUILTIN, OUTPUT); Serial.begin(115200); delay(10); } // Fault Detetection and Alerting Routine void FaultDetect() { if (! ina219.begin()) { digitalWrite(LED_BUILTIN, HIGH); while (1) { delay(10); } } } // Sensor Calibraion Scaling Routine void SensorCalibration() { ina219.setCalibration_16V_400mA(); } // Clear Fault Alert Routine void ClearFaultAlert() { digitalWrite(LED_BUILTIN, LOW); } // Setup Routine void setup() { HardwareInit(); FaultDetect(); SensorCalibration(); ClearFaultAlert(); PrintHeader(); } // Looping Routine void loop() { GetVoltSensorValues(); PrintOut(); TimerDelay(); }
Each routine is called either once or in a loop. It’s easier to know what the program is going to do. There is little need for excessive comments as the functions are self explanatory. Again, if serial print commands were to be replaced with microSD memory write commands, the 2 functions PrintHeader and PrintOut would only need to be edited.
When editing base code from a third party, it is important to test the code as is to get a baseline. From that point moving forward, each edit should be detailed in the title comment section and a sequential version saved. No matter how simple the edit may seem, the code revision should be retested to validate it. Further edits should follow this same pattern. A good rule of thumb for editing is this process. Create and test a baseline. Create a title comments version. Next create a libraries version, followed by a variable and constants section. Now the editing can start to focus on routine revisions. Eventually the code base will be organized and each of the sections can be modified, reused, or removed with less impact on the entire code base.
It may seem a painstaking process. However, it is far less cumbersome than scouring through several copies of code base for a peppered edit that broke the project. There have been many abandonded projects due to poor version controls.
One other benefit to code development that is object orientated is any late stage changes that may be introduced can be easily adopted. This gives a level of agility that would not exist otherwise. The next section will cover how additional sensors were added to the ESP32-Cam project midway through its firmware development stage.