{"id":4728,"date":"2024-09-28T12:00:22","date_gmt":"2024-09-28T19:00:22","guid":{"rendered":"https:\/\/www.cloudacm.com\/?p=4728"},"modified":"2024-09-28T08:50:47","modified_gmt":"2024-09-28T15:50:47","slug":"esp32-cam-data-retention","status":"publish","type":"post","link":"https:\/\/www.cloudacm.com\/?p=4728","title":{"rendered":"ESP32-Cam Data Retention"},"content":{"rendered":"<p>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.<\/p>\n<p>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.<\/p>\n<p>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.<\/p>\n<p><a href=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/09\/Untitled-Sketch-Version-Mix-5-3.3v_bb.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-large wp-image-4731\" src=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/09\/Untitled-Sketch-Version-Mix-5-3.3v_bb-1024x431.png\" alt=\"\" width=\"640\" height=\"269\" srcset=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/09\/Untitled-Sketch-Version-Mix-5-3.3v_bb-1024x431.png 1024w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/09\/Untitled-Sketch-Version-Mix-5-3.3v_bb-300x126.png 300w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/09\/Untitled-Sketch-Version-Mix-5-3.3v_bb-768x323.png 768w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/09\/Untitled-Sketch-Version-Mix-5-3.3v_bb-1536x646.png 1536w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/09\/Untitled-Sketch-Version-Mix-5-3.3v_bb-604x254.png 604w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/09\/Untitled-Sketch-Version-Mix-5-3.3v_bb.png 1848w\" sizes=\"auto, (max-width: 640px) 100vw, 640px\" \/><\/a><\/p>\n<p>Here is a list of the parts for the build.<\/p>\n<ul>\n<li>ESP32-Cam Module<\/li>\n<li>Arduino Pro Mini 8Mhz 3.3V<\/li>\n<li>NEO06MV2 GPS Module (Warning below)<\/li>\n<li>GY-521 3-Axis Gyroscope<\/li>\n<li>GY-271 3-Axis Compass<\/li>\n<li>GY-68 Barometer<\/li>\n<\/ul>\n<p>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.<\/p>\n<p>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.<\/p>\n<p>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.<\/p>\n<p><a href=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/09\/Enclosure_Mount.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-large wp-image-4732\" src=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/09\/Enclosure_Mount-1024x410.png\" alt=\"\" width=\"640\" height=\"256\" srcset=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/09\/Enclosure_Mount-1024x410.png 1024w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/09\/Enclosure_Mount-300x120.png 300w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/09\/Enclosure_Mount-768x308.png 768w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/09\/Enclosure_Mount-604x242.png 604w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/09\/Enclosure_Mount.png 1056w\" sizes=\"auto, (max-width: 640px) 100vw, 640px\" \/><\/a><\/p>\n<p>Here is the code used for the ESP32-Cam module.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"cpp\">\/\/ Libraries\r\n#include \"FS.h\"                \/\/ SD Card ESP32\r\n#include \"SD_MMC.h\"            \/\/ SD Card ESP32\r\n#include \"WiFi.h\"\r\n#include \"esp_camera.h\"\r\n\r\n\/\/ Variables and Constants\r\n\r\nString GPSReadings = \"\/GPS_I2C_Readings.csv\";\r\nString WifiScanReadings = \"\/Wifi_Readings.csv\";\r\nString BootCounter = \"\/BootCounter.txt\";\r\nString StringBootReference = \"\";\r\nString RetentionDirectory = \"Data_\";\r\nString InboundSerialDataString = \"\";\r\n\r\nString MillisString;\r\n\r\n\/\/boolean stringComplete = false;  \/\/ whether the string is complete\r\n\r\nunsigned long ExecuteInterval = 2000;\r\nunsigned long LastInterval = 0;\r\nunsigned long CurrentInterval = 0;\r\nint HoldInterval = 0;\r\n\r\nunsigned long last = 0UL;    \/\/ For stats that happen every 5 seconds\r\nfloat OnboardSeconds;\r\n\r\n\/\/ Pin definition for CAMERA_MODEL_AI_THINKER\r\n#define PWDN_GPIO_NUM     32\r\n#define RESET_GPIO_NUM    -1\r\n#define XCLK_GPIO_NUM      0\r\n#define SIOD_GPIO_NUM     26\r\n#define SIOC_GPIO_NUM     27\r\n#define Y9_GPIO_NUM       35\r\n#define Y8_GPIO_NUM       34\r\n#define Y7_GPIO_NUM       39\r\n#define Y6_GPIO_NUM       36\r\n#define Y5_GPIO_NUM       21\r\n#define Y4_GPIO_NUM       19\r\n#define Y3_GPIO_NUM       18\r\n#define Y2_GPIO_NUM        5\r\n#define VSYNC_GPIO_NUM    25\r\n#define HREF_GPIO_NUM     23\r\n#define PCLK_GPIO_NUM     22\r\n\r\n\r\n\/\/ Routines and Subroutines\r\n\r\n\/\/ Hardware Initialization Routine\r\nvoid HardwareInit()\r\n{  \r\n  \/\/Default baud of NEO-6M is 9600\r\n  Serial.begin(9600);\r\n  delay(100);\r\n  StartupCamera();\r\n  delay(100);\r\n  StartupMicroSD();\r\n  delay(100);\r\n} \r\n\r\nvoid StartupCamera() {\r\n  \r\n  camera_config_t config;\r\n  config.ledc_channel = LEDC_CHANNEL_0;\r\n  config.ledc_timer = LEDC_TIMER_0;\r\n  config.pin_d0 = Y2_GPIO_NUM;\r\n  config.pin_d1 = Y3_GPIO_NUM;\r\n  config.pin_d2 = Y4_GPIO_NUM;\r\n  config.pin_d3 = Y5_GPIO_NUM;\r\n  config.pin_d4 = Y6_GPIO_NUM;\r\n  config.pin_d5 = Y7_GPIO_NUM;\r\n  config.pin_d6 = Y8_GPIO_NUM;\r\n  config.pin_d7 = Y9_GPIO_NUM;\r\n  config.pin_xclk = XCLK_GPIO_NUM;\r\n  config.pin_pclk = PCLK_GPIO_NUM;\r\n  config.pin_vsync = VSYNC_GPIO_NUM;\r\n  config.pin_href = HREF_GPIO_NUM;\r\n  config.pin_sscb_sda = SIOD_GPIO_NUM;\r\n  config.pin_sscb_scl = SIOC_GPIO_NUM;\r\n  config.pin_pwdn = PWDN_GPIO_NUM;\r\n  config.pin_reset = RESET_GPIO_NUM;\r\n  config.xclk_freq_hz = 20000000;\r\n  config.pixel_format = PIXFORMAT_JPEG;  \/\/YUV422,GRAYSCALE,RGB565,JPEG\r\n  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)\r\n  config.jpeg_quality = 10; \/\/ 10-63 lower number means higher quality\r\n  config.fb_count = 1;\r\n  \r\n  \/\/ Init Camera\r\n  esp_err_t err = esp_camera_init(&amp;config);\r\n  if (err != ESP_OK) {\r\n    return;\r\n  }\r\n\r\n  sensor_t * s = esp_camera_sensor_get();\r\n  s-&gt;set_whitebal(s, 0);       \/\/ 0 = disable , 1 = enable\r\n  s-&gt;set_awb_gain(s, 0);       \/\/ 0 = disable , 1 = enable\r\n \r\n}\r\n\r\nvoid StartupMicroSD() {\r\n  \r\n  if(!SD_MMC.begin()){\r\n    return;\r\n  }\r\n  \r\n  uint8_t cardType = SD_MMC.cardType();\r\n  if(cardType == CARD_NONE){\r\n    return;\r\n  }\r\n  \r\n}\r\n\r\n\r\n\/\/Append to the end of file in SD card\r\nvoid appendFile(fs::FS &amp;fs, const char * path, const char * message){\r\n\r\n    File file = fs.open(path, FILE_APPEND);\r\n    if(!file){\r\n        return;\r\n    }\r\n    if(file.print(message)){\r\n    } else {\r\n    }\r\n}\r\n\r\n\r\n\/\/Read variable from file in SD card\r\nint readBootReference(fs::FS &amp;fs, const char *path) {\r\n  File file = fs.open(path);\r\n  if (!file) {\r\n    return -1;\r\n  }\r\n\r\n  int value = file.parseInt();\r\n  file.close();\r\n  return value;\r\n}\r\n\r\n\r\n\/\/Write variable to file in SD card\r\nvoid writeBootReference(fs::FS &amp;fs, const char *path, int value) {\r\n  File file = fs.open(path, FILE_WRITE);\r\n  if (!file) {\r\n    return;\r\n  }\r\n  file.print(value);\r\n  file.close();\r\n}\r\n\r\n\r\n\/\/Create directory in SD card\r\nvoid createDir(fs::FS &amp;fs, const char * path){\r\n    if(fs.mkdir(path)){\r\n    } else {\r\n    }\r\n}\r\n\r\n\r\nvoid BootCounterCheck() {\r\n  \r\n    int BootReference = readBootReference(SD_MMC, BootCounter.c_str());\r\n    if (BootReference == -1) {\r\n      BootReference = 1;\r\n      writeBootReference(SD_MMC, BootCounter.c_str(), BootReference);\r\n      StringBootReference = String(BootReference);\r\n    } else {\r\n      BootReference++;\r\n      writeBootReference(SD_MMC, BootCounter.c_str(), BootReference);\r\n      StringBootReference = String(BootReference);\r\n    }\r\n\r\n}\r\n\r\n\r\nvoid PrintHeader()\r\n{\r\n\r\n   RetentionDirectory = \"\/Data_\" + StringBootReference;\r\n   createDir(SD_MMC, RetentionDirectory.c_str());\r\n\r\n   GPSReadings = RetentionDirectory + \"\/\" + StringBootReference + \"_GPS_I2C_Readings.csv\";\r\n   WifiScanReadings = RetentionDirectory + \"\/\" + StringBootReference + \"_Wifi_Readings.csv\";\r\n  \r\n   appendFile(SD_MMC, GPSReadings.c_str(), \"Arduino_ProMini_GPS_I2C_ver4\");\r\n   appendFile(SD_MMC, GPSReadings.c_str(), \"\\n\");\r\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\");\r\n   appendFile(SD_MMC, GPSReadings.c_str(), \"\\n\");\r\n\r\n   appendFile(SD_MMC, WifiScanReadings.c_str(), \"ESP32-Cam_Car-Interior-Camera-Sensor_ver4\");\r\n   appendFile(SD_MMC, WifiScanReadings.c_str(), \"\\n\");\r\n   appendFile(SD_MMC, WifiScanReadings.c_str(), \"OnboardSeconds,Networks Found,SSID,dBm,AP Mac Address,Channel,Security\");\r\n   appendFile(SD_MMC, WifiScanReadings.c_str(), \"\\n\");\r\n\r\n  \r\n}\r\n\r\n\r\n\/\/ https:\/\/mischianti.org\/esp32-practical-power-saving-manage-wifi-and-cpu-1\/\r\nvoid disableWiFi(){\r\n  \r\n  \/\/ Switch WiFi off\r\n  WiFi.mode(WIFI_OFF);    \/\/ Switch WiFi off\r\n  delay(100);\r\n}\r\n\r\n\r\nvoid enableWiFi(){\r\n\r\n  \/\/ Set WiFi to station mode and disconnect from an AP if it was previously connected.\r\n  WiFi.mode(WIFI_STA);\r\n  WiFi.disconnect();\r\n  delay(100);\r\n}\r\n\r\n\r\nvoid ScanWifiNetworks()\r\n{\r\n\r\n      \/\/ WiFi.scanNetworks will return the number of networks found.\r\n    int IntWifiNetworks = WiFi.scanNetworks(\/*async=*\/false, \/*hidden=*\/true);\r\n    \r\n\r\n      \r\n    if (IntWifiNetworks == 0) {\r\n    }\r\n    \r\n    else {\r\n      \r\n        for (int NetworkCount = 0; NetworkCount &lt; IntWifiNetworks; ++NetworkCount) \r\n           {\r\n      \r\n            OnboardSeconds = millis();\r\n            OnboardSeconds = OnboardSeconds\/1000;\r\n            MillisString = String(OnboardSeconds, 3);      \r\n            appendFile(SD_MMC, WifiScanReadings.c_str(), MillisString.c_str()); \r\n            appendFile(SD_MMC, WifiScanReadings.c_str(), \",\");\r\n        \r\n            String StringWifiNetworks = String(IntWifiNetworks);         \/\/ read string until meet newline character  \r\n            appendFile(SD_MMC, WifiScanReadings.c_str(), StringWifiNetworks.c_str()); \r\n            appendFile(SD_MMC, WifiScanReadings.c_str(), \",\");\r\n            \r\n            String StringSSID = String(WiFi.SSID(NetworkCount));\r\n            if(StringSSID != NULL) {\r\n              appendFile(SD_MMC, WifiScanReadings.c_str(), StringSSID.c_str()); \r\n              }\r\n            else {\r\n              appendFile(SD_MMC, WifiScanReadings.c_str(), \"*.*Hidden*.*\");\r\n              }\r\n            appendFile(SD_MMC, WifiScanReadings.c_str(), \",\");\r\n           \r\n            String StringRSSI = String(WiFi.RSSI(NetworkCount));\r\n            appendFile(SD_MMC, WifiScanReadings.c_str(), StringRSSI.c_str()); \r\n            appendFile(SD_MMC, WifiScanReadings.c_str(), \",\");\r\n\r\n            String StringBSSIDstr = String(WiFi.BSSIDstr(NetworkCount));\r\n            appendFile(SD_MMC, WifiScanReadings.c_str(), StringBSSIDstr.c_str()); \r\n            appendFile(SD_MMC, WifiScanReadings.c_str(), \",\");\r\n\r\n            String StringChannel = String(WiFi.channel(NetworkCount));\r\n            appendFile(SD_MMC, WifiScanReadings.c_str(), StringChannel.c_str()); \r\n            appendFile(SD_MMC, WifiScanReadings.c_str(), \",\");\r\n\r\n            printEncryptionType(WiFi.encryptionType(NetworkCount));\r\n            appendFile(SD_MMC, WifiScanReadings.c_str(), \"\\n\");\r\n           }\r\n       }\r\n\r\n      \/\/ Delete the scan result to free memory for code below.\r\n      WiFi.scanDelete();\r\n      appendFile(SD_MMC, WifiScanReadings.c_str(), \"\\n\");\r\n    \r\n  \r\n}\r\n\r\n\r\nvoid printEncryptionType(int thisType) {\r\n\r\n  \/\/ read the encryption type and print out the name:\r\n  switch (thisType) {\r\n            case WIFI_AUTH_OPEN:\r\n                appendFile(SD_MMC, WifiScanReadings.c_str(), \"open\");\r\n                break;\r\n            case WIFI_AUTH_WEP:\r\n                appendFile(SD_MMC, WifiScanReadings.c_str(), \"WEP\");\r\n                break;\r\n            case WIFI_AUTH_WPA_PSK:\r\n                appendFile(SD_MMC, WifiScanReadings.c_str(), \"WPA\");\r\n                break;\r\n            case WIFI_AUTH_WPA2_PSK:\r\n                appendFile(SD_MMC, WifiScanReadings.c_str(), \"WPA2\");\r\n                break;\r\n            case WIFI_AUTH_WPA_WPA2_PSK:\r\n                appendFile(SD_MMC, WifiScanReadings.c_str(), \"WPA+WPA2\");\r\n                break;\r\n            case WIFI_AUTH_WPA2_ENTERPRISE:\r\n                appendFile(SD_MMC, WifiScanReadings.c_str(), \"WPA2-EAP\");\r\n                break;\r\n            case WIFI_AUTH_WPA3_PSK:\r\n                appendFile(SD_MMC, WifiScanReadings.c_str(), \"WPA3\");\r\n                break;\r\n            case WIFI_AUTH_WPA2_WPA3_PSK:\r\n                appendFile(SD_MMC, WifiScanReadings.c_str(), \"WPA2+WPA3\");\r\n                break;\r\n            case WIFI_AUTH_WAPI_PSK:\r\n                appendFile(SD_MMC, WifiScanReadings.c_str(), \"WAPI\");\r\n                break;\r\n            default:\r\n                appendFile(SD_MMC, WifiScanReadings.c_str(), \"unknown\");\r\n  }\r\n}\r\n\r\n\r\nvoid CaptureImage() {\r\n\r\n  camera_fb_t * fb = NULL;\r\n  \r\n  \/\/ Take Picture with Camera\r\n  fb = esp_camera_fb_get();  \r\n  if(!fb) {\r\n    return;\r\n  }\r\n  \r\n  \/\/ Construct a filename that looks like \"\/photo_0000000001.jpg\"\r\n  unsigned long pictureNumber = millis(); \r\n  pictureNumber = pictureNumber \/ 1000; \r\n  String filename = RetentionDirectory + \"\/\" + StringBootReference + \"_photo_\";\r\n  if(pictureNumber &lt; 1000000000) filename += \"0\";\r\n  if(pictureNumber &lt; 100000000) filename += \"0\";\r\n  if(pictureNumber &lt; 10000000) filename += \"0\";\r\n  if(pictureNumber &lt; 1000000) filename += \"0\";\r\n  if(pictureNumber &lt; 100000) filename += \"0\";\r\n  if(pictureNumber &lt; 10000) filename += \"0\";\r\n  if(pictureNumber &lt; 1000) filename += \"0\";\r\n  if(pictureNumber &lt; 100) filename += \"0\";\r\n  if(pictureNumber &lt; 10) filename += \"0\";\r\n  filename += pictureNumber;\r\n  filename += \".jpg\";\r\n  \/\/ Path where new picture will be saved in SD Card\r\n  String imagefile = String(filename);\r\n\r\n  fs::FS &amp;fs = SD_MMC; \r\n  \r\n  File file = fs.open(imagefile.c_str(), FILE_WRITE);\r\n  file.write(fb-&gt;buf, fb-&gt;len); \/\/ payload (image), payload length\r\n  \/\/ file.close();\r\n  esp_camera_fb_return(fb); \r\n\r\n  \r\n}\r\n\r\n\/\/ Inbound Serial Data from GPS Module for Processing\r\nvoid InboundSerialData()\r\n{\r\n  \/\/ Wait for serial data input, then perform internal functions\r\n  if(Serial.available())                                   \/\/ if there is data comming\r\n  {\r\n     if(Serial.read() == '~')\r\n    {\r\n       InboundSerialDataString = Serial.readStringUntil('^');         \/\/ read string until meet newline character  \r\n       appendFile(SD_MMC, GPSReadings.c_str(), InboundSerialDataString.c_str()); \r\n       appendFile(SD_MMC, GPSReadings.c_str(), \"\\n\");\r\n       InboundSerialDataString = \"\";\r\n    }\r\n  }  \r\n}\r\n\r\n\r\nvoid setup() \r\n{\r\n\r\n  HardwareInit();\r\n  BootCounterCheck();\r\n  PrintHeader();\r\n  \r\n}\r\n\r\n\r\nvoid loop() {\r\n\r\n  InboundSerialData();\r\n\r\n  CurrentInterval = millis();\r\n  \r\n  if (CurrentInterval - LastInterval &gt;= ExecuteInterval) {\r\n    if (HoldInterval == 0){\r\n  \r\n      LastInterval = CurrentInterval;   \r\n         \r\n      HoldInterval = 1;\r\n    }\r\n    \r\n  }\r\n    \r\n\r\n if (CurrentInterval - LastInterval &gt;= ExecuteInterval) {\r\n  if (HoldInterval == 1){\r\n  \r\n      LastInterval = CurrentInterval; \r\n      \r\n      enableWiFi();\r\n      ScanWifiNetworks();\r\n      disableWiFi();\r\n      \r\n      CaptureImage();\r\n    \r\n      HoldInterval = 0;\r\n    }\r\n    \r\n  }\r\n  \r\n}\r\n\r\n<\/pre>\n<p>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.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"cpp\">\/\/ Libraries\r\n\r\n#include &lt;SoftwareSerial.h&gt;\r\n#include &lt;TinyGPS++.h&gt;\r\n#include &lt;Wire.h&gt;\r\n\r\n\r\n\/\/ Variables and Constants\r\n\r\n\/\/ Set these based on your needs\r\n\/\/ GPS Home, https:\/\/www.google.com\/maps\r\nstatic const double SalmonBayPark_LAT = 47.679477, SalmonBayPark_LON = -122.382035;\r\n\r\nTinyGPSPlus gps;\r\n\r\nString MillisString;\r\nString GpsDataString;\r\nString GpsString = \"\";         \/\/ a String to hold incoming data\r\n\r\nboolean stringComplete = false;  \/\/ whether the string is complete\r\n\r\nunsigned long last = 0UL;    \/\/ For stats that happen every 5 seconds\r\nint PadSeconds;\r\nint PadMinutes;\r\nint PadDays;\r\nint PadMonths;\r\nfloat OnboardSeconds;\r\n\r\n#define SoftRX 2\r\n#define SoftTX 3\r\nSoftwareSerial mySerial(SoftRX, SoftTX); \/\/ RX, TX\r\n\r\n\r\n#include \"GY521.h\"\r\nGY521 sensor(0x68);\r\n\r\nuint32_t counter = 0;\r\n\r\n\r\n#include \"BMP085.h\"\r\nBMP085 myBarometer;\r\n\r\nfloat temperature;\r\nfloat pressure;\r\nfloat atm;\r\nfloat altitude;\r\n\r\n\r\n#include &lt;QMC5883LCompass.h&gt;\r\nQMC5883LCompass compass;\r\n\r\nint CompassX = 0;\r\nint CompassY = 0;\r\nint CompassZ = 0;\r\n\r\nint SampleCounter = 0;\r\n\r\n\/\/ Routines and Subroutines\r\n\r\n\/\/ Hardware Initialization Routine\r\nvoid HardwareInit()\r\n{  \r\n  \/\/Default baud of NEO-6M is 9600\r\n  Serial.begin(9600);\r\n  mySerial.begin(9600);\r\n  Wire.begin();\r\n  \r\n  delay(100);\r\n  \r\n  while (sensor.wakeup() == false)\r\n    {\r\n      Serial.print(millis());\r\n      Serial.println(\"\\tCould not connect to GY521: please check the GY521 address (0x68\/0x69)\");\r\n      delay(1000);\r\n    }\r\n  sensor.setAccelSensitivity(2);  \/\/  8g\r\n  sensor.setGyroSensitivity(1);   \/\/  500 degrees\/s\r\n\r\n  sensor.setThrottle();\r\n  \r\n  myBarometer.init();\r\n  compass.init();\r\n  \r\n\r\n  \/\/  set calibration values from calibration sketch.\r\n  sensor.axe = 0.574;\r\n  sensor.aye = -0.002;\r\n  sensor.aze = -1.043;\r\n  sensor.gxe = 10.702;\r\n  sensor.gye = -6.436;\r\n  sensor.gze = -0.676;\r\n} \r\n\r\n\r\n\/\/ Printout Seconds Since Bootup Timestamp\r\nvoid PrintOutMillis()\r\n{\r\n\r\n    Serial.print(\"~\");\r\n    OnboardSeconds = millis();\r\n    OnboardSeconds = OnboardSeconds\/1000;\r\n    Serial.print(OnboardSeconds, 3);\r\n    Serial.print(\",\");\r\n    \r\n}\r\n\r\n\r\n\/\/ Printout Location Routine\r\nvoid PrintOutLocation()\r\n{\r\n    Serial.print(gps.location.lat(), 6);\r\n    Serial.print(\",\");\r\n    Serial.print(gps.location.lng(), 6);\r\n    Serial.print(\",\");\r\n}\r\n\r\n\r\n\/\/ Printout Date Routine\r\nvoid PrintOutDate()\r\n{\r\n    PadMonths = gps.date.month();\r\n      if (PadMonths &lt; 10)\r\n        {        \r\n         Serial.print(\"0\");\r\n        }        \r\n    Serial.print(gps.date.month());\r\n    Serial.print(\"\/\");\r\n    PadDays = gps.date.day();\r\n      if (PadDays &lt; 10)\r\n        {        \r\n         Serial.print(\"0\");\r\n        }        \r\n    Serial.print(gps.date.day());\r\n    Serial.print(\"\/\");\r\n    Serial.print(gps.date.year());\r\n    Serial.print(\",\");\r\n}\r\n\r\n\r\n\/\/ Printout Time Routine\r\nvoid PrintOutTime()\r\n{\r\n    Serial.print(gps.time.hour());\r\n    Serial.print(\":\");\r\n    PadMinutes = gps.time.minute();\r\n      if (PadMinutes &lt; 10)\r\n        {\r\n         Serial.print(\"0\");\r\n        }\r\n    Serial.print(gps.time.minute());\r\n    Serial.print(\":\");\r\n    PadSeconds = gps.time.second();\r\n      if (PadSeconds &lt; 10)\r\n        {\r\n         Serial.print(\"0\");\r\n        }\r\n    Serial.print(gps.time.second());\r\n    Serial.print(\",\");\r\n}\r\n\r\n\r\n\r\n\/\/ Printout Speed Routine\r\nvoid PrintOutSpeed()\r\n{\r\n    Serial.print(gps.speed.mph());\r\n    Serial.print(\",\");\r\n}\r\n\r\n\r\n\/\/ Printout Course Routine\r\nvoid PrintOutCourse()\r\n{\r\n    Serial.print(gps.course.deg());\r\n    Serial.print(\",\");\r\n}\r\n\r\n\r\n\/\/ Printout Altitude Routine\r\nvoid PrintOutAltitude()\r\n{\r\n    Serial.print(gps.altitude.feet());\r\n    Serial.print(\",\");\r\n}\r\n\r\n\r\n\/\/ Reference Bearing Routine\r\nvoid BearingReference()\r\n{\r\n      \r\n      double distanceToSalmonBayPark =\r\n        TinyGPSPlus::distanceBetween(\r\n          gps.location.lat(),\r\n          gps.location.lng(),\r\n          SalmonBayPark_LAT, \r\n          SalmonBayPark_LON);\r\n      double courseToSalmonBayPark =\r\n        TinyGPSPlus::courseTo(\r\n          gps.location.lat(),\r\n          gps.location.lng(),\r\n          SalmonBayPark_LAT, \r\n          SalmonBayPark_LON);\r\n          \r\n      Serial.print(distanceToSalmonBayPark\/1609, 6);\r\n      Serial.print(\",\");\r\n      Serial.print(courseToSalmonBayPark, 6);\r\n      Serial.print(\",\");\r\n      Serial.print(TinyGPSPlus::cardinal(courseToSalmonBayPark));\r\n      Serial.print(\",\");\r\n}\r\n\r\n\r\nvoid I2CData() {\r\n\r\n  \r\n  sensor.read();\r\n  float x = sensor.getAngleX();\r\n  float y = sensor.getAngleY();\r\n  float z = sensor.getAngleZ();\r\n  \r\n  temperature = myBarometer.bmp085GetTemperature(\r\n          myBarometer.bmp085ReadUT()); \/\/Get the temperature, bmp085ReadUT MUST be called first\r\n  pressure = myBarometer.bmp085GetPressure(myBarometer.bmp085ReadUP());\/\/Get the temperature\r\n\r\n  \/*\r\n        To specify a more accurate altitude, enter the correct mean sea level\r\n        pressure level.  For example, if the current pressure level is 1019.00 hPa\r\n        enter 101900 since we include two decimal places in the integer value\u3002\r\n  *\/\r\n  altitude = myBarometer.calcAltitude(101900);\r\n\r\n  atm = pressure \/ 101325;\r\n  \r\n  \/\/ Read compass values\r\n  compass.read();\r\n\r\n  \/\/ Return XYZ readings\r\n  CompassX = compass.getX();\r\n  CompassY = compass.getY();\r\n  CompassZ = compass.getZ();\r\n  \r\n\r\n  Serial.print(temperature, 2); \/\/display 2 decimal places\r\n  Serial.print(\",\");\r\n  Serial.print(pressure, 0); \/\/whole number only.\r\n  Serial.print(\",\");\r\n  Serial.print(atm, 4); \/\/display 4 decimal places\r\n  Serial.print(\",\");\r\n  Serial.print(altitude, 2); \/\/display 2 decimal places\r\n  Serial.print(\",\");\r\n  Serial.print(CompassX);\r\n  Serial.print(\",\");\r\n  Serial.print(CompassY);\r\n  Serial.print(\",\");\r\n  Serial.print(CompassZ);\r\n  Serial.print(\",\");\r\n  Serial.print(x, 1);\r\n  Serial.print(',');\r\n  Serial.print(y, 1);\r\n  Serial.print(',');\r\n  Serial.print(z, 1);\r\n  Serial.print(',');\r\n  SampleCounter++;\r\n  Serial.print(\"C-\");\r\n  Serial.print(SampleCounter);\r\n  Serial.println('^');\r\n  \r\n}\r\n\r\n\r\n\/\/ Inbound Serial Data from GPS Module for Processing\r\nvoid InboundSerialData()\r\n{\r\n  while (mySerial.available() &gt; 0)\r\n    gps.encode(mySerial.read());\r\n\r\n  if (gps.altitude.isUpdated())\r\n  {\r\n    PrintOutGlobalReadings();\r\n  }\r\n\r\n  else if (millis() - last &gt; 5000)\r\n  {\r\n    if (gps.location.isValid())\r\n    last = millis();\r\n  }\r\n}\r\n\r\n\r\n\/\/ Printout GPS Results Routine\r\nvoid PrintOutGlobalReadings()\r\n{\r\n    PrintOutMillis ();\r\n    PrintOutLocation();\r\n    PrintOutDate();\r\n    PrintOutTime();\r\n    PrintOutSpeed();\r\n    PrintOutCourse();\r\n    PrintOutAltitude();\r\n    BearingReference();\r\n    I2CData();\r\n}\r\n\r\n\r\nvoid setup() {\r\n\r\n  HardwareInit();\r\n  \r\n}\r\n\r\n\r\nvoid loop() {\r\n\r\nInboundSerialData();\r\n  \r\n}\r\n<\/pre>\n<p>There data stream timing doesn&#8217;t always line up, this results in some rows of data being misaligned with the column header.\u00a0 Two columns are used to sort the data, &#8220;SampleCount&#8221; and &#8220;OnboardSeconds&#8221;.\u00a0 This will group the malformed rows so they can be discarded.\u00a0 The &#8220;OnboadSeconds&#8221; value in each row can be crossreferenced with the WiFi readings and image captures.<\/p>\n<p>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&#8217;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.<\/p>\n<p>A good rule of thumb is test your components before you assemble. It&#8217;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&#8217;s too late to find out that 50 percent of the parts you ordered that were assembled in the factory are faulty.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>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&#8230;<\/p>\n<p class=\"read-more\"><a class=\"btn btn-default\" href=\"https:\/\/www.cloudacm.com\/?p=4728\"> Read More<span class=\"screen-reader-text\">  Read More<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-4728","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=\/wp\/v2\/posts\/4728","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=4728"}],"version-history":[{"count":9,"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=\/wp\/v2\/posts\/4728\/revisions"}],"predecessor-version":[{"id":4735,"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=\/wp\/v2\/posts\/4728\/revisions\/4735"}],"wp:attachment":[{"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=4728"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=4728"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=4728"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}