Solar Powered Weather Station for Weather Underground WU

*Please Note! You will need a different article on this website as a prerequisite for this project. This guide only shows you how to add the solar portion.
Follow this project and complete it first: Click Here

Features:
Never have to change the battery again.
Automatically disconnects power to protect the battery (Mine has never had to disconnect as it has enough backup power to last for weeks of dark winter days). If you have any concerns with this it’s very easy to just add more 18650 batteries in parallel to the 1 that I’m using.
Automatically turns back on once the sun comes out and the batteries charge back up.

Parts List:
18650 charge protector
18650 Li-po battery
18650 Li-po battery holder
5v Step Up – In the photo I used a small stepup that I don’t recommend as they tend to fail, please use the one that I’ve recommended in the link “5v Step Up” as they’re now my go to unit and are awesome.
5v Solar Cell 500mah
Wiring

Photo 1
Photo 2
Photo 3 – In this photo Voltage1 is at the top of the photo and Voltage2 is the bottom rail. You’ll need to know this for the explanations below.

In photo 3 the solar cell wiring is plugged into the bread board first in my example. I did this because it gave me the option to disconnect the solar cell and replace it easily but you don’t have to do this.

Solar Wiring:
The solar cell is connected to the 18650 charge controller at the end that has the micro USB. In photo 3 it goes from a red/white pair to a blue/white pair. It’s marked clearly with a + and a – where the wires connect. I only just realized that the 18650 charge controller will keep your solar cell safe if you have only one solar cell. The voltage will not flow back through to the solar cell and you do not need a blocking diode. If you have two solar panels in parallel you will need a blocking diode on each solar cell you add into the project.

18650 Charge Controller Wiring:
You’ve just connected the solar cell and now we’ll hook up the battery box. The two wires coming off the battery box go to Voltage1 (photo 3 and is the top rail). The top rail is then connected to the 18650 charge controller inside pins. You’ll see them on the board with labels of B+ and B- for battery positive and negative. Those solder pads will block electricity if the 18650 battery gets too low 2.7 volts I believe. They will reactivate when the solar cell charges them up to about 3.2 volts. Those pads will also not let the voltage from the solar take the 18650 batteries too high and will cut off the solar when the battery reaches 4.2 volts. The red light that indicates the solar cell is charging, will turn blue when the battery is full. They’re an amazing add on to any project. The outer pads are labelled out+ and out- and will be connect to Voltage2 (look at photo 3 its the bottom rail).

5v Step Up
On your 5v step up the bottom rail in photo 3 Voltage2 is connected to the “in” side of the step up. On the board it will be labelled IN+ and IN- then you connect the GND and 5V while leaving d+ and D- alone. Those inside solder pads will only give you the voltage of the IN side and we don’t use them in this project. Connect the GND and 5V on the step up’s out side to Vin and GND on your NodeMCU.

Deep Sleep wiring:
You’ll notice a green wire in photo 3 going from RST (beside GND) to D0 on the NodeMCU. This makes a connection to wake the NodeMCU up and will save a lot of power. This is really important as it’ll save on the use of the battery overnight. It’s always better to use software to save power instead of just adding more expensive items like 18650 cells.

That’s it! If you have any comments or questions and what some help with your project I LOVE a good challenge. I’ll help you develop what ever you need without charge. But I’ll most likely end up creating a tutorial for my efforts.

Arduino Weather Station for Weather Underground Nodemcu and BME280

This is a basic weather station using a Nodemcu and BME280 temp humidity and pressure sensor. You can set the timer for updates, currently set for every three minutes. Please ask questions on this page, I love responding to people so don’t be afraid to say hello. If this is your first ever Arduino project then get a hold of me using the comments below and I’ll help you get it setup.

Parts List:
NodeMCU
BME280 – Double check when yours arrives if it’s 3.3v or 5v

NodeMCU 3 pack
NodeMCU 5 pack Best Value
BME280 3 pack – Double check when yours arrives if it’s 3.3v or 5v

.Zip file for the code Click Here or scroll down and both files are printed on the page.

Very Important: Your WU weatherstation will not work on the newer ESP8266 board library. You’ll need to go to your board manager, find your install for esp8266 and then select 2.4.2 I’ve no idea why it doesn’t work on any other version, but I was pulling my hair out with this project and then randomly read that someone had to downgrade to make it work. Once you have it uploaded to your board you can go back to the newest update for your future projects.

In the photo above, the power bank has 4 18650 batteries that will last about 5 days. The battery bank can take a solar panel and use it at the same time to recharge when needed. Let me know if you’re interested in this by posting below and I’ll build a how to article on that subject. The purple module is the BME280 and of course a NodeMCU. I’ll make an article about using a WemosD1 soon. The BME280 is known to read high and this article addresses that issue below.
A close up of the BME280 sensor and the NodeMCU. SDA to D2, SCL to D1, VIN to 3.3v and GND to GND. Do not power the BME from the 5v.

Normally the BME280 sensor reads high from 2 to 4 degrees Celsius. This project includes a way to correct your sensor. You’ll have to decide on how to correct it. If you want it accurate in winter and summer you’ll have to update your code during the season changes.

The alternative is to set it to about 3 degrees offset and have it average out. Currently it’s winter here and I’ve got it set bang on to see how it changes at it warms up around here in spring.

To fix the over temp issue on a BME280 find this line:

float sensor_temperature = bme.readTemperature() * 9/5+32; // Read temperature as Fahrenheit

Change that line where it says 9/5+32 to 9/5+30 like this:

float sensor_temperature = bme.readTemperature() * 9/5+30; // Read temperature as Fahrenheit

If you find that your reading is constantly over temp by say 5 degrees. Then you reduce the number to 27 instead of the original 32.

In the photo above, you can see how my weather station is now reading the same as the nearby stations. It used to drive me NUTS that mine was always 2-3 degrees higher than everyone else. Not that I normally like to follow the crowd, but my home weather station that I purchased was always on par with the people around me so I know that it’s the BME280 that’s off (or used to be).

Here’s the code BUT you also need a second file listed below:
Download both files in a zip here

Here’s the Code for the .ino file:

#ifdef ESP32
  #include <WiFi.h>
#else
  #include <ESP8266WiFi.h>
#endif

#include <WiFiClientSecure.h>
#include <Wire.h> 
#include "system_variables.h"
#include <time.h>

#include <Adafruit_BME280.h>

Adafruit_BME280 bme; // I2C

const char* ssid      = "Wifi name goes here";
const char* password  = "Wifi password goes here";
String WU_pwsID       = ""; // Use the unique ID from Weather Underground for your new weather station
String WU_pwsPASSWORD = ""; // Use the password that Weather Underground gives you, it's not your account password. Each weather station has it's OWN password.

const char* host      = "rtupdate.wunderground.com"; 
const int httpsPort   = 443;

#ifdef ESP32
  const unsigned long  UpdateInterval     = 180000; // Delay between updates, in milliseconds
#else
  const unsigned long  UpdateInterval     = 180000;    // Delay between updates, in milliseconds
#endif

String UploadData, timenow; 
float  sensor_temperature, sensor_humidity, sensor_pressure, sensor_spare; 

void setup() {
  Serial.begin(115200);
  boolean Status = bme.begin(0x76);  
  if (!Status) {
    Serial.println("Could not find a valid BME280 sensor, check wiring!");
    while (1);
  }
  StartWiFi();
  StartAndGetTime();
}

void loop() {

  ReadSensorInformation();
  UpdateTime();
  timenow = "now"; // or comment this line to upload the local time from the ESP time update
  if (!UploadDataToWU()); Serial.println("Error uploading to Weather Underground, trying next time");
  delay(180000); // for a 5-min update rate
  // or to make it sleep uncomment this section and comment out the sleep() line above
//  #ifdef ESP8266 
//    ESP.deepSleep(UpdateInterval, WAKE_RF_DEFAULT); // Sleep for the time set by 'UpdateInterval' 
//  #else 
//    ESP.deepSleep(UpdateInterval);                  // Sleep for the time set by 'UpdateInterval' 
//  #endif
}

boolean UploadDataToWU(){
  WiFiClientSecure client;
  // Use WiFiClientSecure class to create SSL connection
  Serial.println("Connecting to   : "+String(host));
  if (!client.connect(host, httpsPort)) {
    Serial.println("Connection failed");
    return false;
  }
  String url = "/weatherstation/updateweatherstation.php?ID="+WU_pwsID+"&PASSWORD="+WU_pwsPASSWORD+"&dateutc="+timenow +
               #ifdef SW_tempf
                 "&tempf="+WU_tempf +
               #endif
               #ifdef SW_humidity
                 "&humidity="+WU_humidity +
               #endif
               #ifdef SW_dewptf
                 "&dewptf="+WU_dewptf +             // ***** You must report Dew Point for a valid display to be shown on WU
               #endif
               #ifdef SW_baromin
                 "&baromin="+WU_baromin + 
               #endif
               // ancillary parameters
               #ifdef SW_winddir
                 "&winddir="+WU_winddir + 
               #endif
               #ifdef SW_windspeedmph
                 "&windspeedmph="+WU_windspeedmph + 
               #endif
               #ifdef SW_windgustmph
                 "&windgustmph="+WU_windgustmph +
               #endif
               #ifdef SW_rainin
                 "&rainin="+WU_rainin +
               #endif
               #ifdef SW_dailyrainin
                 "&dailyrainin="+WU_dailyrainin + 
               #endif
               #ifdef SW_solarradiation
                 "&solarradiation="+WU_solarradiation +
               #endif
               #ifdef SW_UV
                 "&UV="+WU_UV +
               #endif
               #ifdef SW_indoortempf
                 "&indoortempf="+WU_indoortempf + 
               #endif
               #ifdef SW_indoorhumidity
                 "&indoorhumidity="+WU_indoorhumidity + 
               #endif
               #ifdef SW_soiltempf
                 "&soiltempf="+WU_soiltempf + 
               #endif
               #ifdef SW_soilmoisture
                 "&soilmoisture="+WU_soilmoisture + 
               #endif
               #ifdef SW_leafwetness
                 "&leafwetness="+WU_leafwetness + 
               #endif
               #ifdef SW_visibility
                 "&visibility="+WU_visibility + 
               #endif
               #ifdef SW_weather
                 "&weather="+WU_weather +
               #endif
               #ifdef SW_clouds
                 "&clouds="+WU_clouds +
               #endif
               "&action=updateraw&realtime=1&rtfreq=60";
  Serial.println("Requesting      : "+url);
  client.print(String("GET ") + url + " HTTP/1.1\r\n" +
               "Host: " + host + "\r\n" +
               "User-Agent: G6EJDFailureDetectionFunction\r\n" +
               "Connection: close\r\n\r\n");
  Serial.print("Request sent    : ");
  while (client.connected()) {
    String line = client.readStringUntil('\n');
    if (line == "\r") {
      Serial.println("Headers received");
      break;
    }
  }
  String line = client.readStringUntil('\n');
  //Serial.println(line);
  boolean Status = true;
  if (line == "success") line = "Server confirmed all data received";
  if (line == "INVALIDPASSWORDID|Password or key and/or id are incorrect") {
    line = "Invalid PWS/User data entered in the ID and PASSWORD or GET parameters";
    Status = false;
  }
  if (line == "RapidFire Server") {
    line = "The minimum GET parameters of ID, PASSWORD, action and dateutc were not set correctly";
    Status = false;
  }
  Serial.println("Server Response : "+line);
  Serial.println("Status          : Closing connection");
  return Status;
}

void ReadSensorInformation(){
  float sensor_humidity     = bme.readHumidity(); 
  float sensor_temperature  = bme.readTemperature() * 9/5+30;         // Read temperature as Fahrenheit
  float sensor_temperatureC = bme.readTemperature() - 2;
  float sensor_pressure     = bme.readPressure() / 100.0F * 0.02953 ; // Read pressure in in's"
  if (isnan(sensor_humidity) || isnan(sensor_temperature) || isnan(sensor_pressure)) { 
    Serial.println("Failed to read from BME280 sensor!"); 
    return; 
  } 
  WU_tempf     = String(sensor_temperature);
  // Td = T - ((100 - RH)/5.) where T and Td are in °C for a reasonable DP Calculation
  WU_dewptf    = String((sensor_temperatureC-(100-sensor_humidity)/5.0)*9/5+32); // Needs to be reported in °F
  WU_humidity  = String(sensor_humidity,2);
  WU_baromin   = String(sensor_pressure,2);
}

void StartWiFi(){
  Serial.print("\nConnecting to ");
  Serial.println(ssid);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
}

void StartAndGetTime(){
  configTime(0, 0, "0.uk.pool.ntp.org", "time.nist.gov"); 
  setenv("TZ", "GMT0BST,M3.5.0/01,M10.5.0/02",1); 
  delay(200); 
  time_t rawtime;
  struct tm *info;
  char buffer[80];
  time( &rawtime );
  info = localtime( &rawtime );
  // Upload format for time = dateutc [CCYY-MM-DD HH:MM:SS (mysql format)]
  // 2018-04-30 10:32:35 becomes 2018-04-30+10%3A32%3A35 in url escaped format
  // See: http://www.cplusplus.com/reference/ctime/strftime/
  Serial.println("Upload date-time: CCYY-MM-DD HH:MM:SS   expected");
  strftime(buffer,80,"%Y-%m-%d+%H:%M:%S", info);
  //printf("Upload date-time: %s\n", buffer );
  timenow = buffer; // timenow is correctly formated for WU
  timenow.replace(":", "%3A");
}

void UpdateTime(){
  time_t rawtime;
  struct tm *info;
  char buffer[80];
  time( &rawtime );
  info = localtime( &rawtime );
  strftime(buffer,80,"%Y-%m-%d+%H:%M:%S", info);
  printf("Upload date-time: %s\n", buffer );
  timenow = buffer; 
  timenow.replace(":", "%3A"); // timenow is correctly formated for WU
}

Here’s the code for the system_variables.h file:

// Uncomment each sensor/variable that you want to use. The BME280 can only use Temp, Barom, Dewpoint and Humidity. They have all ready been set for you.

//#define SW_winddir          // e.g. = 230   [°] NOTE do-not assign/send units text!
//#define SW_windspeedmph     // e.g. = 12    [mph]
//#define SW_windgustmph      // e.g. = 12    [mph]
//#define SW_windspdmph_avg2m // e.g. = 12    [mph 2 minute average wind speed]
//#define SW_winddir_avg2m    // e.g. = 170   [° 0-360 2 minute average wind direction]
//#define SW_windgustmph_10m  // e.g. = 45    [mph 10 minutes wind gust]
//#define SW_windgustdir_10m  // e.g. = 252   [° 0-360 10 minutes wind gust direction]
#define SW_tempf            // e.g. = 71.2  [°F] * for extra outdoor sensors use temp2f, temp3f, and so on
//#define SW_temp2f           // e.g. = 71.2  [°F] * for extra outdoor sensors use temp2f, temp3f, and so on
//#define SW_temp3f           // e.g. = 71.2  [°F] * for extra outdoor sensors use temp2f, temp3f, and so on
//#define SW_temp4f           // e.g. = 71.2  [°F] * for extra outdoor sensors use temp2f, temp3f, and so on
//#define SW_indoortempf      // e.g. = 65.3  [°F]
//#define SW_indoorhumidity   // e.g. = 52.3  [% range 0 - 100]
//#define SW_soiltempf        // e.g. = 45.7  [°F] * for sensors 2,3,4 use soiltemp2f, soiltemp3f, and soiltemp4f
//#define SW_soiltemp2f       // e.g. = 45.7  [°F] * for sensors 2,3,4 use soiltemp2f, soiltemp3f, and soiltemp4f
//#define SW_soiltemp3f       // e.g. = 45.7  [°F] * for sensors 2,3,4 use soiltemp2f, soiltemp3f, and soiltemp4f
//#define SW_soiltemp4f       // e.g. = 45.7  [°F] * for sensors 2,3,4 use soiltemp2f, soiltemp3f, and soiltemp4f
//#define SW_soilmoisture     // e.g. = 12    [% range 0 - 100]  * for sensors 2,3,4 use soilmoisture2, soilmoisture3, and soilmoisture4
//#define SW_soilmoisture2    // e.g. = 12    [% range 0 - 100]  * for sensors 2,3,4 use soilmoisture2, soilmoisture3, and soilmoisture4
//#define SW_soilmoisture3    // e.g. = 12    [% range 0 - 100]  * for sensors 2,3,4 use soilmoisture2, soilmoisture3, and soilmoisture4
//#define SW_soilmoisture4    // e.g. = 12    [% range 0 - 100]  * for sensors 2,3,4 use soilmoisture2, soilmoisture3, and soilmoisture4
//#define SW_leafwetness      // e.g. = 5     [% range 0 - 100]  * for sensor 2 use leafwetness2
//#define SW_leafwetness2     // e.g. = 7     [% range 0 - 100]  * for sensor 2 use leafwetness2
//#define SW_solarradiation   // e.g. = 548   [W/m^2]
//#define SW_UV               // e.g. = 2     [index 0 - 10]
//#define SW_visibility       // e.g. = 1.2   [nm visibility]
//#define SW_rainin           // e.g. = 1.65  [inches of rain]
//#define SW_dailyrainin      // e.g. = 0.65  [daily rain in inches accumulated]
#define SW_baromin          // e.g. = 29.1  [inches of mercury]
#define SW_dewptf           // e.g. = 68.2  [°F]
#define SW_humidity         // e.g. = 90    [% range 0 - 100]
//#define SW_weather          // e.g. = "+RA" [text -- METAR style (+RA) means light rain showers]
//#define SW_clouds           // e.g. = "SCT" [text -- METAR style SKC, FEW, SCT, BKN, OVC respectively meaning Sky Clear, Few, Scattered, Broken, Overcast]
//#define SW_softwaretype     // e.g. = "vws" [text version01]
//#define SW_AqNO             // e.g. = 100   [No. (nitric oxide) ppb]
//#define SW_AqNO2T           // e.g. = 100   [(nitrogen dioxide), true measure ppb]
//#define SW_AqNO2            // e.g. = 100   [NO2 computed, NOx - NO ppb]
//#define SW_AqNO2Y           // e.g. = 100   [NO2 computed, NOy - NO ppb]
//#define SW_AqNOX            // e.g. = 100   [NOx (nitrogen oxides) - ppb]
//#define SW_AqNOY            // e.g. = 100   [NOy (total reactive nitrogen) - ppb]
//#define SW_AqNO3            // e.g. = 100   [NO3 ion (nitrate, not adjusted for ammonium ion) UG/M3]
//#define SW_AqSO4            // e.g. = 100   [SO4 ion (sulfate, not adjusted for ammonium ion) UG/M3]
//#define SW_AqSO2            // e.g. = 100   [(sulfur dioxide), conventional ppb]
//#define SW_AqSO2T           // e.g. = 100   [trace levels ppb]
//#define SW_AqCO             // e.g. = 100   [CO (carbon monoxide), conventional ppm]
//#define SW_AqCOT            // e.g. = 100   [CO trace levels ppb]
//#define SW_AqEC             // e.g. = 100   [EC (elemental carbon) – PM2.5 UG/M3]
//#define SW_AqOC             // e.g. = 100   [OC (organic carbon, not adjusted for oxygen and hydrogen) – PM2.5 UG/M3]
//#define SW_AqBC             // e.g. = 100   [BC (black carbon at 880 nm) UG/M3]
//#define SW_AqUV_AETH        // e.g. = 100   [UV-AETH (second channel of Aethalometer at 370 nm) UG/M3]
//#define SW_AqPM2_5          // e.g. = 100   [PM2.5 mass - UG/M3]
//#define SW_AqPM10           // e.g. = 100   [PM10 mass - PM10 mass]
//#define SW_AqOZONE          // e.g. = 100   [Ozone/O3 - ppb]
//############################################################################################################################
String WU_winddir;  // e.g. = 230   [° 0-360] NOTE do-not assign/send units text!
String WU_windspeedmph;     // e.g. = 12    [mph]
String WU_windgustmph;      // e.g. = 12    [mph]
String WU_windgustdir;      // e.g. = 90    [° 0-360]
String WU_windspdmph_avg2m; // e.g. = 12    [mph 2 minute average wind speed]
String WU_winddir_avg2m;    // e.g. = 170   [° 0-360 2 minute average wind direction]
String WU_windgustmph_10m;  // e.g. = 45    [mph 10 minutes wind gust]
String WU_windgustdir_10m;  // e.g. = 252   [° 0-360 10 minutes wind gust direction]
String WU_tempf;            // e.g. = 71.2  [°F] * for extra outdoor sensors use temp2f, temp3f, and so on
String WU_temp2f;           // e.g. = 71.2  [°F] * for extra outdoor sensors use temp2f, temp3f, and so on
String WU_temp3f;           // e.g. = 71.2  [°F] * for extra outdoor sensors use temp2f, temp3f, and so on
String WU_temp4f;           // e.g. = 71.2  [°F] * for extra outdoor sensors use temp2f, temp3f, and so on
String WU_indoortempf;      // e.g. = 65.3  [°F]
String WU_indoorhumidity;   // e.g. = 52.3  [% range 0 - 100]
String WU_soiltempf;        // e.g. = 45.7  [°F] * for sensors 2,3,4 use soiltemp2f, soiltemp3f, and soiltemp4f
String WU_soiltemp2f;       // e.g. = 45.7  [°F] * for sensors 2,3,4 use soiltemp2f, soiltemp3f, and soiltemp4f
String WU_soiltemp3f;       // e.g. = 45.7  [°F] * for sensors 2,3,4 use soiltemp2f, soiltemp3f, and soiltemp4f
String WU_soiltemp4f;       // e.g. = 45.7  [°F] * for sensors 2,3,4 use soiltemp2f, soiltemp3f, and soiltemp4f
String WU_soilmoisture;     // e.g. = 12    [% range 0 - 100]  * for sensors 2,3,4 use soilmoisture2, soilmoisture3, and soilmoisture4
String WU_soilmoisture2;    // e.g. = 12    [% range 0 - 100]  * for sensors 2,3,4 use soilmoisture2, soilmoisture3, and soilmoisture4
String WU_soilmoisture3;    // e.g. = 12    [% range 0 - 100]  * for sensors 2,3,4 use soilmoisture2, soilmoisture3, and soilmoisture4
String WU_soilmoisture4;    // e.g. = 12    [% range 0 - 100]  * for sensors 2,3,4 use soilmoisture2, soilmoisture3, and soilmoisture4
String WU_leafwetness;      // e.g. = 5     [% range 0 - 100]  * for sensor 2 use leafwetness2
String WU_leafwetness2;     // e.g. = 7     [% range 0 - 100]  * for sensor 2 use leafwetness2
String WU_solarradiation;   // e.g. = 548   [W/m^2]
String WU_UV;               // e.g. = 2     [index 0 - 10]
String WU_visibility;       // e.g. = 1.2   [nm visibility]
String WU_rainin;           // e.g. = 1.65  [inches of rain]
String WU_dailyrainin;      // e.g. = 0.65  [daily rain in inches accumulated]
String WU_baromin;          // e.g. = 29.1  [inches of mercury]
String WU_dewptf;           // e.g. = 68.2  [°F]
String WU_humidity;         // e.g. = 90    [% range 0 - 100]
String WU_weather;          // e.g. = "+RA" [text -- METAR style (+RA) means light rain showers]
String WU_clouds;           // e.g. = "SCT" [text -- METAR style SKC, FEW, SCT, BKN, OVC respectively meaning Sky Clear, Few, Scattered, Broken, Overcast]
String WU_softwaretype;     // e.g. = "vws" [text version01]
String WU_AqNO;             // e.g. = 100   [No. (nitric oxide) ppb]
String WU_AqNO2T;           // e.g. = 100   [(nitrogen dioxide), true measure ppb]
String WU_AqNO2;            // e.g. = 100   [NO2 computed, NOx - NO ppb]
String WU_AqNO2Y;           // e.g. = 100   [NO2 computed, NOy - NO ppb]
String WU_AqNOX;            // e.g. = 100   [NOx (nitrogen oxides) - ppb]
String WU_AqNOY;            // e.g. = 100   [NOy (total reactive nitrogen) - ppb]
String WU_AqNO3;            // e.g. = 100   [NO3 ion (nitrate, not adjusted for ammonium ion) UG/M3]
String WU_AqSO4;            // e.g. = 100   [SO4 ion (sulfate, not adjusted for ammonium ion) UG/M3]
String WU_AqSO2;            // e.g. = 100   [(sulfur dioxide), conventional ppb]
String WU_AqSO2T;           // e.g. = 100   [trace levels ppb]
String WU_AqCO;             // e.g. = 100   [CO (carbon monoxide), conventional ppm]
String WU_AqCOT;            // e.g. = 100   [CO trace levels ppb]
String WU_AqEC;             // e.g. = 100   [EC (elemental carbon) – PM2.5 UG/M3]
String WU_AqOC;             // e.g. = 100   [OC (organic carbon, not adjusted for oxygen and hydrogen) – PM2.5 UG/M3]
String WU_AqBC;             // e.g. = 100   [BC (black carbon at 880 nm) UG/M3]
String WU_AqUV_AETH;        // e.g. = 100   [UV-AETH (second channel of Aethalometer at 370 nm) UG/M3]
String WU_AqPM2_5;          // e.g. = 100   [PM2.5 mass - UG/M3]
String WU_AqPM10;           // e.g. = 100   [PM10 mass - PM10 mass]
String WU_AqOZONE;          // e.g. = 100   [Ozone/O3 - ppb]
String WU_dateutc;          // e.g. = "2000-01-01+10%3A32%3A35" or "=now" must be escaped characters formated [YYYY-MM-DD HH:MM:SS (mysql format)] or dateutc=now
String WU_action;           // e.g. = "=updateraw"

ESP32-Cam Motion Camera With Date and Time

In this project we’ll be building a motion detecting camera that saves photos to an SD card with the date and time in the file name.

This camera can last a very long time by using 18650 batteries. I’m using two 18650 batteries and haven’t done any testing yet on how long it will last but of course it depends on how many triggers are happening.

The code for this project includes the ability to transfer the images over to a server. I’m still working on getting the server setup on a wifi lan and waiting for parts. I’ll update this when its ready.

The file name of each photo will have the date and then the time stamped like this:

08012021_11_41_30.jpg

The order is Day Month Year. Then after the first _ the time starts so 11:41 am and the last part is the seconds.

Why I wanted this included is because for a lot of the wildlife tracking I do, knowing what animals are in the area is no enough. Know what day and time it happened is obviously crucial.

Parts List:
ESP32-Cam
DS3231 real time clock
HC-SR501 motion sensor
5V Step up
SN3904 Transistor
18650 Charge controller
18650 Battery Holders x 2
18650 Batteries x 2

Code: Although the code is listed below, you’ll need two other files as well. RTClib.cpp and RTClib.h Here are all of the files in a zip. Click Here

#include <Arduino.h>
#include <Wire.h>
#include "RTClib.h"
#include <WiFi.h>
#include "soc/soc.h"
#include "soc/rtc_cntl_reg.h"
#include "esp_camera.h"
#include "FS.h"
#include "SD_MMC.h"
#include "driver/rtc_io.h"
int save_flag = 0; 
const char* ssid = "arunmobile";
const char* password = "arun1234";
#define ESP32CAM_LED_FLASH 4
String saved_image_name = "/first.jpg";
String serverName = "192.168.43.16";   // REPLACE WITH YOUR Raspberry Pi IP ADDRESS
//String serverName = "example.com";   // OR REPLACE WITH YOUR DOMAIN NAME

String serverPath = "/upload";     // The default serverPath should be upload.php

const int serverPort = 8080;
#define uS_TO_S_FACTOR 1000000ULL  /* Conversion factor for micro seconds to seconds */
#define TIME_TO_SLEEP  30        /* Time ESP32 will go to sleep (in seconds) */
int connection_error_count = 0;
RTC_DATA_ATTR int cnt = 0;

WiFiClient client;

// 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

const int remain_connected_timerInterval = 5000;    // time between each HTTP POST image
const int wifi_timerInterval = 5000;    // time between each HTTP POST image
String status_send = "";
unsigned long previousMillis = 0;   // last time image was sent
char dateTimeFilenamearray[25];
RTC_DS3231 rtc;
uint8_t print_wakeup_reason(){
  esp_sleep_wakeup_cause_t wakeup_reason;

  wakeup_reason = esp_sleep_get_wakeup_cause();
  int status = 0;
  switch(wakeup_reason)
  {
    case ESP_SLEEP_WAKEUP_EXT0 : 
        Serial.println("Wakeup caused by external signal using RTC_IO"); 
        status = 1;
        break;
    case ESP_SLEEP_WAKEUP_EXT1 : 
        Serial.println("Wakeup caused by external signal using RTC_CNTL"); 
        status = 2;
        break;
    case ESP_SLEEP_WAKEUP_TIMER : 
        Serial.println("Wakeup caused by timer");
        status = 3;
        break;
    case ESP_SLEEP_WAKEUP_TOUCHPAD : 
        Serial.println("Wakeup caused by touchpad");
        status = 4;
        break;
    case ESP_SLEEP_WAKEUP_ULP : 
        Serial.println("Wakeup caused by ULP program");
        status = 5;
        break;
    default : 
        Serial.printf("Wakeup was not caused by deep sleep: %d\n",wakeup_reason); break;
  }
  return status;
}

uint8_t capture_image(String path_new)
{
    
    // String path_new = "/pircheck"+String(cnt)+".jpg";
    // cnt++;
    String keypressed = "button";   
  
    Serial.println("Starting SD Card");
      if(!SD_MMC.begin()){
        Serial.println("Card Mount Failed");
        return 0;
    }
    uint8_t cardType = SD_MMC.cardType();

    if(cardType == CARD_NONE){
        Serial.println("No SD_MMC card attached");
        return 0;
    }

    Serial.print("SD_MMC Card Type: ");
    if(cardType == CARD_MMC){
        Serial.println("MMC");
    } else if(cardType == CARD_SD){
        Serial.println("SDSC");
    } else if(cardType == CARD_SDHC){
        Serial.println("SDHC");
    } else {
        Serial.println("UNKNOWN");
    }

    uint64_t cardSize = SD_MMC.cardSize() / (1024 * 1024);
    Serial.printf("SD_MMC Card Size: %lluMB\n", cardSize);   
    
    // digitalWrite(ESP32CAM_LED_FLASH, HIGH);
    camera_fb_t *fb = NULL;
    fb = esp_camera_fb_get();

    if (!fb)
    {
      Serial.println("Camera capture failed");
      return 0;
    }
    else
    {
      Serial.println("Camera Captured");
    }

    delay(1000);

    fs::FS &fs = SD_MMC;
    Serial.printf("Picture file name: %s\n", path_new.c_str());
    
    File file = fs.open(path_new.c_str(), FILE_WRITE);
    if(!file){
      Serial.println("Failed to open file in writing mode");
    } 
    else {
      file.write(fb->buf, fb->len); // payload (image), payload length
      Serial.printf("Saved file to path: %s\n", path_new.c_str());
    }
    file.close();
    delay(1000);
    SD_MMC.end();
    delay(1000);
    esp_camera_fb_return(fb);
    delay(1000);
    // digitalWrite(ESP32CAM_LED_FLASH, LOW);
    
}

String sendPhoto(const char * path) {
  String getAll;
  String getBody;
  static uint8_t buf[1024];

  if(!SD_MMC.begin()){
        Serial.println("Card Mount Failed");
        
    }
    uint8_t cardType = SD_MMC.cardType();

    if(cardType == CARD_NONE){
        Serial.println("No SD_MMC card attached");
        
    }

    Serial.print("SD_MMC Card Type: ");
    if(cardType == CARD_MMC){
        Serial.println("MMC");
    } else if(cardType == CARD_SD){
        Serial.println("SDSC");
    } else if(cardType == CARD_SDHC){
        Serial.println("SDHC");
    } else {
        Serial.println("UNKNOWN");
    }

    uint64_t cardSize = SD_MMC.cardSize() / (1024 * 1024);
    Serial.printf("SD_MMC Card Size: %lluMB\n", cardSize);

  fs::FS &fs = SD_MMC; 

  File file = fs.open(path);
  delay(500);
  if(file)
  {
    Serial.println("file detected.........");  
  }
  Serial.println("Connecting to server: " + serverName);

  if (client.connect(serverName.c_str(), serverPort)) {
    Serial.println("Connection successful!");    
    String head = "--RandomNerdTutorials\r\nContent-Disposition: form-data; name=\"imageFile\"; filename=\"esp32-cam.jpg\"\r\nContent-Type: image/jpeg\r\n\r\n";
    String tail = "\r\n--RandomNerdTutorials--\r\n";

    uint16_t imageLen = file.size();;
    uint16_t extraLen = head.length() + tail.length();
    uint16_t totalLen = imageLen + extraLen;
  
    client.println("POST " + serverPath + " HTTP/1.1");
    client.println("Host: " + serverName);
    client.println("Content-Length: " + String(totalLen));
    client.println("Content-Type: multipart/form-data; boundary=RandomNerdTutorials");
    client.println();
    client.print(head);  
    
    size_t fbLen = file.size();
    for (size_t n=0; n<fbLen; n=n+1024) {
      if (n+1024 < fbLen) {
        file.read(buf, 1024);
        delay(10);
        client.write(buf, 1024);        
      }
      else if (fbLen%1024>0) {
        size_t remainder = fbLen%1024;
        file.read(buf, remainder);
        delay(10);
        client.write(buf, remainder);
      }
    }   
    client.print(tail);    
    
    int timoutTimer = 10000;
    long startTimer = millis();
    boolean state = false;
    
    while ((startTimer + timoutTimer) > millis()) {
      Serial.print(".");
      delay(100);      
      while (client.available()) {
        char c = client.read();
        if (c == '\n') {
          if (getAll.length()==0) { state=true; }
          getAll = "";
        }
        else if (c != '\r') { getAll += String(c); }
        if (state==true) { getBody += String(c); }
        startTimer = millis();
      }
      if (getBody.length()>0) { break; }
    }
    file.close();    
    Serial.println();
    client.stop();
    Serial.println(getBody);
    delay(1000);
    SD_MMC.end();
  }
  else {
    getBody = "";
    Serial.println(getBody);
  }
  return getBody;
}

void renameFile(const char * path1, const char * path2){
    Serial.printf("Renaming file %s to %s\n", path1, path2);

    if(!SD_MMC.begin()){
        Serial.println("Card Mount Failed");
        
    }
    uint8_t cardType = SD_MMC.cardType();

    if(cardType == CARD_NONE){
        Serial.println("No SD_MMC card attached");
        
    }

    Serial.print("SD_MMC Card Type: ");
    if(cardType == CARD_MMC){
        Serial.println("MMC");
    } else if(cardType == CARD_SD){
        Serial.println("SDSC");
    } else if(cardType == CARD_SDHC){
        Serial.println("SDHC");
    } else {
        Serial.println("UNKNOWN");
    }
    fs::FS &fs = SD_MMC; 

    if (fs.rename(path1, path2)) {
        Serial.println("File renamed");
    } else {
        Serial.println("Rename failed");
    }
    SD_MMC.end();
}

void deleteFile(const char * path){
    Serial.printf("Deleting file: %s\n", path);
    if(!SD_MMC.begin()){
        Serial.println("Card Mount Failed");
        
    }
    uint8_t cardType = SD_MMC.cardType();

    if(cardType == CARD_NONE){
        Serial.println("No SD_MMC card attached");
        
    }

    Serial.print("SD_MMC Card Type: ");
    if(cardType == CARD_MMC){
        Serial.println("MMC");
    } else if(cardType == CARD_SD){
        Serial.println("SDSC");
    } else if(cardType == CARD_SDHC){
        Serial.println("SDHC");
    } else {
        Serial.println("UNKNOWN");
    }
    fs::FS &fs = SD_MMC;
    if(fs.remove(path)){
        Serial.println("File deleted");
    } else {
        Serial.println("Delete failed");
    }
    SD_MMC.end();
}

String listDir(const char * dirname, uint8_t levels){
    Serial.printf("Listing directory: %s\n", dirname);

    if(!SD_MMC.begin()){
        Serial.println("Card Mount Failed");
        
    }
    uint8_t cardType = SD_MMC.cardType();

    if(cardType == CARD_NONE){
        Serial.println("No SD_MMC card attached");
        
    }

    Serial.print("SD_MMC Card Type: ");
    if(cardType == CARD_MMC){
        Serial.println("MMC");
    } else if(cardType == CARD_SD){
        Serial.println("SDSC");
    } else if(cardType == CARD_SDHC){
        Serial.println("SDHC");
    } else {
        Serial.println("UNKNOWN");
    }

    uint64_t cardSize = SD_MMC.cardSize() / (1024 * 1024);
    Serial.printf("SD_MMC Card Size: %lluMB\n", cardSize);   
    delay(1000);
    fs::FS &fs = SD_MMC; 

    File root = fs.open(dirname);
    delay(1000);
    if(!root){
        Serial.println("Failed to open directory");
        
    }
    if(!root.isDirectory()){
        Serial.println("Not a directory");
        
    }
    String filename = "";
    File file = root.openNextFile();
    if(file){        
      Serial.print("  FILE: ");
      Serial.print(file.name());
      Serial.print("  SIZE: ");
      Serial.println(file.size());
      filename = String(file.name());
    }   
    file.close();
    SD_MMC.end();
    return filename;
  }


void go_to_deepsleep()
{
    esp_sleep_enable_ext0_wakeup(GPIO_NUM_13,0); //1 = High, 0 = Low
    esp_sleep_enable_timer_wakeup(TIME_TO_SLEEP * uS_TO_S_FACTOR);

    Serial.println("Going to sleep now");
    esp_deep_sleep_start();
    Serial.println("This will never be printed");
}

void setup_wifi()
{
  delay(10);
  // We start by connecting to a WiFi network
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);

  WiFi.begin(ssid, password);
  unsigned long previousMillis = millis(); 
  int flag = 0;
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.print("."); 
    Serial.print("connecting");  
    unsigned long currentMillis = millis(); 
    if (currentMillis - previousMillis >= wifi_timerInterval) 
    {
        flag = 1;
        break;
    }  
  }
  
  if (flag == 0)
  {
    Serial.println("");
    Serial.println("WiFi connected");
    Serial.println("IP address: ");
    Serial.println(WiFi.localIP());
  }
  else
  {
    Serial.println("WiFi not connected");
  }
  
}

void setup() {
  //delay(5000);
  WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); 
  Serial.begin(115200); 
  if (! rtc.begin()) {
    Serial.println("Couldn't find RTC");
    while (1);
  }

  String path_new = "";
  //Print the wakeup reason for ESP32
  uint8_t status = print_wakeup_reason();
  Serial.print("Serial status is "); 
  Serial.print(status); 
  if(status==0)
  {   
    // delay(20000);
    Serial.println("<<<<<<<<<<<<<<<<<<< POWER >>>>>>>>>>>>>>>>>>>"); 
    // go_to_deepsleep();
  }

  if(status==1)
  {   
    Serial.println("<<<<<<<<<<<<<<<<<<< MOTION >>>>>>>>>>>>>>>>>>>"); 
    
  }

  if(status==3)
  {   
    Serial.println("<<<<<<<<<<<<<<<<<<< TIMER >>>>>>>>>>>>>>>>>>>"); 
    
  }  

  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;
    config.frame_size = FRAMESIZE_VGA;
    config.jpeg_quality = 12;  //0-63 lower number means higher quality
    config.fb_count = 1;  

    pinMode(4, INPUT);
    digitalWrite(4, LOW);
    rtc_gpio_hold_dis(GPIO_NUM_4);
    
    // camera init
    esp_err_t err = esp_camera_init(&config);
    if (err != ESP_OK) {
      Serial.printf("Camera init failed with error 0x%x", err);      
      Serial.println("<<<<<<<<<<<<<<<<< Restartig >>>>>>>>>>>>>>>");
      delay(1000);
      ESP.restart();
    }

  if(status==1 || status == 0)
  {
    
    delay(1000);    
    if(status == 0)
    {
      path_new = saved_image_name;
    }
    else
    {
      DateTime now = rtc.now();
      dateTimeFilenamearray[0] = '\0';
      //sprintf(dateTimeFilenamearray, "/%02d%02d%d_%d:%d:%d", now.day(), now.month(), now.year(),now.hour(),now.minute(),now.second());
      sprintf(dateTimeFilenamearray, "/%02d%02d%d_%d_%d_%d", now.day(), now.month(), now.year(),now.hour(),now.minute(),now.second());  
      Serial.println(dateTimeFilenamearray);
      //path_new = "/motion_"+String(cnt)+".jpg";
      path_new = String(dateTimeFilenamearray)+".jpg";
      cnt++;      
    }
    
    capture_image(path_new);
    delay(1000);
    // esp_camera_deinit();
    // delay(10000);
    pinMode(4, OUTPUT);
    digitalWrite(4, LOW);
    rtc_gpio_hold_en(GPIO_NUM_4);
    delay(10000);
    go_to_deepsleep();
  }  

  setup_wifi(); 
  
  previousMillis = millis();
  connection_error_count = 0;
}

void loop() {
  
  if (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.print(".");   
    unsigned long currentMillis = millis(); 
    if (currentMillis - previousMillis >= remain_connected_timerInterval) {
      go_to_deepsleep();
    }
  } 
  else
  {   
    if(connection_error_count < 3)
    {
      String filename = "";
      filename = listDir("/",0);
      if(filename != "")
      {    
        Serial.print("filename is: ");
        Serial.println(filename);    
        delay(1000);
        if(filename != saved_image_name){
          status_send = "";
          status_send = sendPhoto(filename.c_str());   
          if(status_send.indexOf("ok") > 0){
            Serial.print("Response from the server");
            Serial.println(status_send);
            deleteFile(filename.c_str());   
            connection_error_count = 0;     
          } 
          else
          {
            delay(3000);
            connection_error_count++;
          }                
        } 
        else
        {
          deleteFile(filename.c_str());
        }              
        previousMillis = millis();
        delay(2000);
      }
      else
      {
        delay(5000);
        go_to_deepsleep();
      } 
    }
    else
    {
        delay(1000);
        go_to_deepsleep();
    }
       
  }    
}