ESP32-Cam Data Retention

ESP32-Cam Data Retention

This post will show how to use the ESP32-Cam module microSD media as non volatile storage for variables. Flash write cycles have a limitation which can limit the life of the module if overused. MicroSD media can be easily replaced as needed and offers a more suitable alternative when the module is repeatedly power cycled.

The module will be used to capture images of the interior of a vehicle when in operation. Each time the vehicle is operated, a new folder is created with a number variable referenced from the microSD media. Images are then stored to that new folder. With each successive trip, new folders are created and images for each trip are stored in its respective folder.

Along with image capture, the ESP32-Cam module WiFi library will be used to scan for available networks it can detect. Those results will also be stored for each trip when the vehicle is operated. Additional modules were added to expand the logging to support GPS, Barometric (Altitude and Temperature), Triple Axis Magnetometer (Compass readings along 3 planes), and Triple Axis Gyroscope readings. These added modules are handled by an Arduino Pro Mini due to the IO limits of the ESP32-Cam module. The following diagram shows how each are interconnected with a breadboard, the final build mirrors this.

Here is a list of the parts for the build.

  • ESP32-Cam Module
  • Arduino Pro Mini 8Mhz 3.3V
  • NEO06MV2 GPS Module (Warning below)
  • GY-521 3-Axis Gyroscope
  • GY-271 3-Axis Compass
  • GY-68 Barometer

The GPS data is a serial stream at 9600 baud and is sent to Pin 2 of the Arduino Pro Mini. The Pro Mini sends its data to the ESP32-Cam module as a serial stream at 9600 baud. The remaining sensors use the I2C protocol and interface with the Pro Mini using its standard data and clock pins.

Power is supplied by the vehicle through a 12v to 5v USB adapter rated at 2 amps. The ESP32-Cam module includes a programming breakout board with a USB port. This then splits out and supplies power to the GPS, Pro Mini, and ESP32-Cam modules. The Pro Mini provides the 3.3 volt supply for the I2C sensors.

The ESP32-Cam module can be removed from its breakout board to be serviced, updated, or replaced as needed. The Pro Mini has header pins to allow it to be updated with new firmware if needed. All of the items are contained in a housing that is mounted overhead on the front cabin light console, with the exception of the GPS antenna, which is mounted forward facing.

Here is the code used for the ESP32-Cam module.

// Libraries
#include "FS.h"                // SD Card ESP32
#include "SD_MMC.h"            // SD Card ESP32
#include "WiFi.h"
#include "esp_camera.h"

// Variables and Constants

String GPSReadings = "/GPS_I2C_Readings.csv";
String WifiScanReadings = "/Wifi_Readings.csv";
String BootCounter = "/BootCounter.txt";
String StringBootReference = "";
String RetentionDirectory = "Data_";
String InboundSerialDataString = "";

String MillisString;

//boolean stringComplete = false;  // whether the string is complete

unsigned long ExecuteInterval = 2000;
unsigned long LastInterval = 0;
unsigned long CurrentInterval = 0;
int HoldInterval = 0;

unsigned long last = 0UL;    // For stats that happen every 5 seconds
float OnboardSeconds;

// Pin definition for CAMERA_MODEL_AI_THINKER
#define PWDN_GPIO_NUM     32
#define RESET_GPIO_NUM    -1
#define XCLK_GPIO_NUM      0
#define SIOD_GPIO_NUM     26
#define SIOC_GPIO_NUM     27
#define Y9_GPIO_NUM       35
#define Y8_GPIO_NUM       34
#define Y7_GPIO_NUM       39
#define Y6_GPIO_NUM       36
#define Y5_GPIO_NUM       21
#define Y4_GPIO_NUM       19
#define Y3_GPIO_NUM       18
#define Y2_GPIO_NUM        5
#define VSYNC_GPIO_NUM    25
#define HREF_GPIO_NUM     23
#define PCLK_GPIO_NUM     22


// Routines and Subroutines

// Hardware Initialization Routine
void HardwareInit()
{  
  //Default baud of NEO-6M is 9600
  Serial.begin(9600);
  delay(100);
  StartupCamera();
  delay(100);
  StartupMicroSD();
  delay(100);
} 

void StartupCamera() {
  
  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = Y2_GPIO_NUM;
  config.pin_d1 = Y3_GPIO_NUM;
  config.pin_d2 = Y4_GPIO_NUM;
  config.pin_d3 = Y5_GPIO_NUM;
  config.pin_d4 = Y6_GPIO_NUM;
  config.pin_d5 = Y7_GPIO_NUM;
  config.pin_d6 = Y8_GPIO_NUM;
  config.pin_d7 = Y9_GPIO_NUM;
  config.pin_xclk = XCLK_GPIO_NUM;
  config.pin_pclk = PCLK_GPIO_NUM;
  config.pin_vsync = VSYNC_GPIO_NUM;
  config.pin_href = HREF_GPIO_NUM;
  config.pin_sscb_sda = SIOD_GPIO_NUM;
  config.pin_sscb_scl = SIOC_GPIO_NUM;
  config.pin_pwdn = PWDN_GPIO_NUM;
  config.pin_reset = RESET_GPIO_NUM;
  config.xclk_freq_hz = 20000000;
  config.pixel_format = PIXFORMAT_JPEG;  //YUV422,GRAYSCALE,RGB565,JPEG
  config.frame_size = FRAMESIZE_XGA; // FRAMESIZE_ + QVGA (320 x 240) | CIF (352 x 288) |VGA (640 x 480) | SVGA (800 x 600) |XGA (1024 x 768) |SXGA (1280 x 1024) |UXGA (1600 x 1200)
  config.jpeg_quality = 10; // 10-63 lower number means higher quality
  config.fb_count = 1;
  
  // Init Camera
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    return;
  }

  sensor_t * s = esp_camera_sensor_get();
  s->set_whitebal(s, 0);       // 0 = disable , 1 = enable
  s->set_awb_gain(s, 0);       // 0 = disable , 1 = enable
 
}

void StartupMicroSD() {
  
  if(!SD_MMC.begin()){
    return;
  }
  
  uint8_t cardType = SD_MMC.cardType();
  if(cardType == CARD_NONE){
    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 {
    }
}


//Read variable from file in SD card
int readBootReference(fs::FS &fs, const char *path) {
  File file = fs.open(path);
  if (!file) {
    return -1;
  }

  int value = file.parseInt();
  file.close();
  return value;
}


//Write variable to file in SD card
void writeBootReference(fs::FS &fs, const char *path, int value) {
  File file = fs.open(path, FILE_WRITE);
  if (!file) {
    return;
  }
  file.print(value);
  file.close();
}


//Create directory in SD card
void createDir(fs::FS &fs, const char * path){
    if(fs.mkdir(path)){
    } else {
    }
}


void BootCounterCheck() {
  
    int BootReference = readBootReference(SD_MMC, BootCounter.c_str());
    if (BootReference == -1) {
      BootReference = 1;
      writeBootReference(SD_MMC, BootCounter.c_str(), BootReference);
      StringBootReference = String(BootReference);
    } else {
      BootReference++;
      writeBootReference(SD_MMC, BootCounter.c_str(), BootReference);
      StringBootReference = String(BootReference);
    }

}


void PrintHeader()
{

   RetentionDirectory = "/Data_" + StringBootReference;
   createDir(SD_MMC, RetentionDirectory.c_str());

   GPSReadings = RetentionDirectory + "/" + StringBootReference + "_GPS_I2C_Readings.csv";
   WifiScanReadings = RetentionDirectory + "/" + StringBootReference + "_Wifi_Readings.csv";
  
   appendFile(SD_MMC, GPSReadings.c_str(), "Arduino_ProMini_GPS_I2C_ver4");
   appendFile(SD_MMC, GPSReadings.c_str(), "\n");
   appendFile(SD_MMC, GPSReadings.c_str(), "OnboardSeconds,Lat,Long,Date,Time,MPH,Course,Altitude,Home,Bearing,Cardinal,Temperature,Pressure,Ralated Atmosphere,Altitude,Compass-X,Compass-Y,Compass-Z,Pitch,Roll,Yaw,SampleCount");
   appendFile(SD_MMC, GPSReadings.c_str(), "\n");

   appendFile(SD_MMC, WifiScanReadings.c_str(), "ESP32-Cam_Car-Interior-Camera-Sensor_ver4");
   appendFile(SD_MMC, WifiScanReadings.c_str(), "\n");
   appendFile(SD_MMC, WifiScanReadings.c_str(), "OnboardSeconds,Networks Found,SSID,dBm,AP Mac Address,Channel,Security");
   appendFile(SD_MMC, WifiScanReadings.c_str(), "\n");

  
}


// https://mischianti.org/esp32-practical-power-saving-manage-wifi-and-cpu-1/
void disableWiFi(){
  
  // Switch WiFi off
  WiFi.mode(WIFI_OFF);    // Switch WiFi off
  delay(100);
}


void enableWiFi(){

  // Set WiFi to station mode and disconnect from an AP if it was previously connected.
  WiFi.mode(WIFI_STA);
  WiFi.disconnect();
  delay(100);
}


void ScanWifiNetworks()
{

      // WiFi.scanNetworks will return the number of networks found.
    int IntWifiNetworks = WiFi.scanNetworks(/*async=*/false, /*hidden=*/true);
    

      
    if (IntWifiNetworks == 0) {
    }
    
    else {
      
        for (int NetworkCount = 0; NetworkCount < IntWifiNetworks; ++NetworkCount) 
           {
      
            OnboardSeconds = millis();
            OnboardSeconds = OnboardSeconds/1000;
            MillisString = String(OnboardSeconds, 3);      
            appendFile(SD_MMC, WifiScanReadings.c_str(), MillisString.c_str()); 
            appendFile(SD_MMC, WifiScanReadings.c_str(), ",");
        
            String StringWifiNetworks = String(IntWifiNetworks);         // read string until meet newline character  
            appendFile(SD_MMC, WifiScanReadings.c_str(), StringWifiNetworks.c_str()); 
            appendFile(SD_MMC, WifiScanReadings.c_str(), ",");
            
            String StringSSID = String(WiFi.SSID(NetworkCount));
            if(StringSSID != NULL) {
              appendFile(SD_MMC, WifiScanReadings.c_str(), StringSSID.c_str()); 
              }
            else {
              appendFile(SD_MMC, WifiScanReadings.c_str(), "*.*Hidden*.*");
              }
            appendFile(SD_MMC, WifiScanReadings.c_str(), ",");
           
            String StringRSSI = String(WiFi.RSSI(NetworkCount));
            appendFile(SD_MMC, WifiScanReadings.c_str(), StringRSSI.c_str()); 
            appendFile(SD_MMC, WifiScanReadings.c_str(), ",");

            String StringBSSIDstr = String(WiFi.BSSIDstr(NetworkCount));
            appendFile(SD_MMC, WifiScanReadings.c_str(), StringBSSIDstr.c_str()); 
            appendFile(SD_MMC, WifiScanReadings.c_str(), ",");

            String StringChannel = String(WiFi.channel(NetworkCount));
            appendFile(SD_MMC, WifiScanReadings.c_str(), StringChannel.c_str()); 
            appendFile(SD_MMC, WifiScanReadings.c_str(), ",");

            printEncryptionType(WiFi.encryptionType(NetworkCount));
            appendFile(SD_MMC, WifiScanReadings.c_str(), "\n");
           }
       }

      // Delete the scan result to free memory for code below.
      WiFi.scanDelete();
      appendFile(SD_MMC, WifiScanReadings.c_str(), "\n");
    
  
}


void printEncryptionType(int thisType) {

  // read the encryption type and print out the name:
  switch (thisType) {
            case WIFI_AUTH_OPEN:
                appendFile(SD_MMC, WifiScanReadings.c_str(), "open");
                break;
            case WIFI_AUTH_WEP:
                appendFile(SD_MMC, WifiScanReadings.c_str(), "WEP");
                break;
            case WIFI_AUTH_WPA_PSK:
                appendFile(SD_MMC, WifiScanReadings.c_str(), "WPA");
                break;
            case WIFI_AUTH_WPA2_PSK:
                appendFile(SD_MMC, WifiScanReadings.c_str(), "WPA2");
                break;
            case WIFI_AUTH_WPA_WPA2_PSK:
                appendFile(SD_MMC, WifiScanReadings.c_str(), "WPA+WPA2");
                break;
            case WIFI_AUTH_WPA2_ENTERPRISE:
                appendFile(SD_MMC, WifiScanReadings.c_str(), "WPA2-EAP");
                break;
            case WIFI_AUTH_WPA3_PSK:
                appendFile(SD_MMC, WifiScanReadings.c_str(), "WPA3");
                break;
            case WIFI_AUTH_WPA2_WPA3_PSK:
                appendFile(SD_MMC, WifiScanReadings.c_str(), "WPA2+WPA3");
                break;
            case WIFI_AUTH_WAPI_PSK:
                appendFile(SD_MMC, WifiScanReadings.c_str(), "WAPI");
                break;
            default:
                appendFile(SD_MMC, WifiScanReadings.c_str(), "unknown");
  }
}


void CaptureImage() {

  camera_fb_t * fb = NULL;
  
  // Take Picture with Camera
  fb = esp_camera_fb_get();  
  if(!fb) {
    return;
  }
  
  // Construct a filename that looks like "/photo_0000000001.jpg"
  unsigned long pictureNumber = millis(); 
  pictureNumber = pictureNumber / 1000; 
  String filename = RetentionDirectory + "/" + StringBootReference + "_photo_";
  if(pictureNumber < 1000000000) filename += "0";
  if(pictureNumber < 100000000) filename += "0";
  if(pictureNumber < 10000000) filename += "0";
  if(pictureNumber < 1000000) filename += "0";
  if(pictureNumber < 100000) filename += "0";
  if(pictureNumber < 10000) filename += "0";
  if(pictureNumber < 1000) filename += "0";
  if(pictureNumber < 100) filename += "0";
  if(pictureNumber < 10) filename += "0";
  filename += pictureNumber;
  filename += ".jpg";
  // Path where new picture will be saved in SD Card
  String imagefile = String(filename);

  fs::FS &fs = SD_MMC; 
  
  File file = fs.open(imagefile.c_str(), FILE_WRITE);
  file.write(fb->buf, fb->len); // payload (image), payload length
  // file.close();
  esp_camera_fb_return(fb); 

  
}

// Inbound Serial Data from GPS Module for Processing
void InboundSerialData()
{
  // Wait for serial data input, then perform internal functions
  if(Serial.available())                                   // if there is data comming
  {
     if(Serial.read() == '~')
    {
       InboundSerialDataString = Serial.readStringUntil('^');         // read string until meet newline character  
       appendFile(SD_MMC, GPSReadings.c_str(), InboundSerialDataString.c_str()); 
       appendFile(SD_MMC, GPSReadings.c_str(), "\n");
       InboundSerialDataString = "";
    }
  }  
}


void setup() 
{

  HardwareInit();
  BootCounterCheck();
  PrintHeader();
  
}


void loop() {

  InboundSerialData();

  CurrentInterval = millis();
  
  if (CurrentInterval - LastInterval >= ExecuteInterval) {
    if (HoldInterval == 0){
  
      LastInterval = CurrentInterval;   
         
      HoldInterval = 1;
    }
    
  }
    

 if (CurrentInterval - LastInterval >= ExecuteInterval) {
  if (HoldInterval == 1){
  
      LastInterval = CurrentInterval; 
      
      enableWiFi();
      ScanWifiNetworks();
      disableWiFi();
      
      CaptureImage();
    
      HoldInterval = 0;
    }
    
  }
  
}

Here is the code used for the Arduino Pro Mini module. This mirrors the serial stream from the GPS, then it polls and includes the I2C device data in the stream.

// Libraries

#include <SoftwareSerial.h>
#include <TinyGPS++.h>
#include <Wire.h>


// Variables and Constants

// Set these based on your needs
// GPS Home, https://www.google.com/maps
static const double SalmonBayPark_LAT = 47.679477, SalmonBayPark_LON = -122.382035;

TinyGPSPlus gps;

String MillisString;
String GpsDataString;
String GpsString = "";         // a String to hold incoming data

boolean stringComplete = false;  // whether the string is complete

unsigned long last = 0UL;    // For stats that happen every 5 seconds
int PadSeconds;
int PadMinutes;
int PadDays;
int PadMonths;
float OnboardSeconds;

#define SoftRX 2
#define SoftTX 3
SoftwareSerial mySerial(SoftRX, SoftTX); // RX, TX


#include "GY521.h"
GY521 sensor(0x68);

uint32_t counter = 0;


#include "BMP085.h"
BMP085 myBarometer;

float temperature;
float pressure;
float atm;
float altitude;


#include <QMC5883LCompass.h>
QMC5883LCompass compass;

int CompassX = 0;
int CompassY = 0;
int CompassZ = 0;

int SampleCounter = 0;

// Routines and Subroutines

// Hardware Initialization Routine
void HardwareInit()
{  
  //Default baud of NEO-6M is 9600
  Serial.begin(9600);
  mySerial.begin(9600);
  Wire.begin();
  
  delay(100);
  
  while (sensor.wakeup() == false)
    {
      Serial.print(millis());
      Serial.println("\tCould not connect to GY521: please check the GY521 address (0x68/0x69)");
      delay(1000);
    }
  sensor.setAccelSensitivity(2);  //  8g
  sensor.setGyroSensitivity(1);   //  500 degrees/s

  sensor.setThrottle();
  
  myBarometer.init();
  compass.init();
  

  //  set calibration values from calibration sketch.
  sensor.axe = 0.574;
  sensor.aye = -0.002;
  sensor.aze = -1.043;
  sensor.gxe = 10.702;
  sensor.gye = -6.436;
  sensor.gze = -0.676;
} 


// Printout Seconds Since Bootup Timestamp
void PrintOutMillis()
{

    Serial.print("~");
    OnboardSeconds = millis();
    OnboardSeconds = OnboardSeconds/1000;
    Serial.print(OnboardSeconds, 3);
    Serial.print(",");
    
}


// Printout Location Routine
void PrintOutLocation()
{
    Serial.print(gps.location.lat(), 6);
    Serial.print(",");
    Serial.print(gps.location.lng(), 6);
    Serial.print(",");
}


// Printout Date Routine
void PrintOutDate()
{
    PadMonths = gps.date.month();
      if (PadMonths < 10)
        {        
         Serial.print("0");
        }        
    Serial.print(gps.date.month());
    Serial.print("/");
    PadDays = gps.date.day();
      if (PadDays < 10)
        {        
         Serial.print("0");
        }        
    Serial.print(gps.date.day());
    Serial.print("/");
    Serial.print(gps.date.year());
    Serial.print(",");
}


// Printout Time Routine
void PrintOutTime()
{
    Serial.print(gps.time.hour());
    Serial.print(":");
    PadMinutes = gps.time.minute();
      if (PadMinutes < 10)
        {
         Serial.print("0");
        }
    Serial.print(gps.time.minute());
    Serial.print(":");
    PadSeconds = gps.time.second();
      if (PadSeconds < 10)
        {
         Serial.print("0");
        }
    Serial.print(gps.time.second());
    Serial.print(",");
}



// Printout Speed Routine
void PrintOutSpeed()
{
    Serial.print(gps.speed.mph());
    Serial.print(",");
}


// Printout Course Routine
void PrintOutCourse()
{
    Serial.print(gps.course.deg());
    Serial.print(",");
}


// Printout Altitude Routine
void PrintOutAltitude()
{
    Serial.print(gps.altitude.feet());
    Serial.print(",");
}


// Reference Bearing Routine
void BearingReference()
{
      
      double distanceToSalmonBayPark =
        TinyGPSPlus::distanceBetween(
          gps.location.lat(),
          gps.location.lng(),
          SalmonBayPark_LAT, 
          SalmonBayPark_LON);
      double courseToSalmonBayPark =
        TinyGPSPlus::courseTo(
          gps.location.lat(),
          gps.location.lng(),
          SalmonBayPark_LAT, 
          SalmonBayPark_LON);
          
      Serial.print(distanceToSalmonBayPark/1609, 6);
      Serial.print(",");
      Serial.print(courseToSalmonBayPark, 6);
      Serial.print(",");
      Serial.print(TinyGPSPlus::cardinal(courseToSalmonBayPark));
      Serial.print(",");
}


void I2CData() {

  
  sensor.read();
  float x = sensor.getAngleX();
  float y = sensor.getAngleY();
  float z = sensor.getAngleZ();
  
  temperature = myBarometer.bmp085GetTemperature(
          myBarometer.bmp085ReadUT()); //Get the temperature, bmp085ReadUT MUST be called first
  pressure = myBarometer.bmp085GetPressure(myBarometer.bmp085ReadUP());//Get the temperature

  /*
        To specify a more accurate altitude, enter the correct mean sea level
        pressure level.  For example, if the current pressure level is 1019.00 hPa
        enter 101900 since we include two decimal places in the integer value。
  */
  altitude = myBarometer.calcAltitude(101900);

  atm = pressure / 101325;
  
  // Read compass values
  compass.read();

  // Return XYZ readings
  CompassX = compass.getX();
  CompassY = compass.getY();
  CompassZ = compass.getZ();
  

  Serial.print(temperature, 2); //display 2 decimal places
  Serial.print(",");
  Serial.print(pressure, 0); //whole number only.
  Serial.print(",");
  Serial.print(atm, 4); //display 4 decimal places
  Serial.print(",");
  Serial.print(altitude, 2); //display 2 decimal places
  Serial.print(",");
  Serial.print(CompassX);
  Serial.print(",");
  Serial.print(CompassY);
  Serial.print(",");
  Serial.print(CompassZ);
  Serial.print(",");
  Serial.print(x, 1);
  Serial.print(',');
  Serial.print(y, 1);
  Serial.print(',');
  Serial.print(z, 1);
  Serial.print(',');
  SampleCounter++;
  Serial.print("C-");
  Serial.print(SampleCounter);
  Serial.println('^');
  
}


// Inbound Serial Data from GPS Module for Processing
void InboundSerialData()
{
  while (mySerial.available() > 0)
    gps.encode(mySerial.read());

  if (gps.altitude.isUpdated())
  {
    PrintOutGlobalReadings();
  }

  else if (millis() - last > 5000)
  {
    if (gps.location.isValid())
    last = millis();
  }
}


// Printout GPS Results Routine
void PrintOutGlobalReadings()
{
    PrintOutMillis ();
    PrintOutLocation();
    PrintOutDate();
    PrintOutTime();
    PrintOutSpeed();
    PrintOutCourse();
    PrintOutAltitude();
    BearingReference();
    I2CData();
}


void setup() {

  HardwareInit();
  
}


void loop() {

InboundSerialData();
  
}

There data stream timing doesn’t always line up, this results in some rows of data being misaligned with the column header.  Two columns are used to sort the data, “SampleCount” and “OnboardSeconds”.  This will group the malformed rows so they can be discarded.  The “OnboadSeconds” value in each row can be crossreferenced with the WiFi readings and image captures.

Here is a warning for those of you considering using the GY-GPS6MV2 module. I discovered lapses in quality on how these modules were constructed from the batch I ordered. This lead to delays in the development of this project. It’s my hope to spare anyone else this trouble. There were instaces of misaligned surface mounted components where adjacent pads were shorted. In addition to this, the amount of solder paste used to surface mount was excesive causing pooled solder to short adjacent pads as well. Of the 6 modules purchsed, only 3 were usable.

A good rule of thumb is test your components before you assemble. It’s not enough to validate a build on the bench in a breadboard. If you procure parts, test them for quality before commiting with a supplier and proceeding with manufacturing. It’s too late to find out that 50 percent of the parts you ordered that were assembled in the factory are faulty.

Comments are closed.