ESP-32 Flight Datalogger – Turning Code into Blocks

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.

Comments are closed.