Camera for Bluebird House

Summary

I built a bluebird house that included a ESP32-CAM and recorded a pair of bluebirds as they built a nest, laid eggs, and raised their babies.

Stages of the project included:

Extensive discussion of the results and planned improvements is included.

Background

Bluebirds are the state bird of Missouri. Many people put up bluebird houses to help the birds and to enjoy seeing them in their yards. A standard design for a bluebird house includes a door on the side designed for maintenance and clean-out, but which can also be used to monitor the nest. However, monitoring the nest too early can discourage nesting. Monitoring too frequently can stress the birds. Opening the door after the 13th day after hatching can result in early fledging which increases the risk to the babies.

I designed a system to allow real-time monitoring of the activity within the birdhouse without disturbing the birds. It also allows recording of images of the birds throughout the nesting period.

Programming the ESP32-CAM

I programmed the ESP32-CAM based off tutorials from http://randomnerdtutorials.com which are listed in the references below.

Features of the program include:

  • Recording images and saving them to the SD card
  • Transmitting images via WiFi for real-time monitoring of the nest
  • Transmitting data about the functioning of the program itself
  • Enabling modification of certain parameters, such as timing of the images, via WiFi
  • Enabling initialization of parameters via a file stored on the SD card

A copy of the program can be found in the appendix at the end of this page.

3D printing a holder for the ESP32-CAM

I 3D printed a holder for the ESP32-CAM.

Building a modified bluebird house

I modified a standard one-board bluebird house by adding an additional 2 inches to the height of the house which allowed room for the camera.

Installing the bluebird house

Frequent recording of images over an extended period of time would quickly run down a battery, so the ESP32-CAM is powered by an extension cord which required placement of the bird house fairly close to the house. Fortunately, this did not keep the birds from using the house.

The connection between the ESP32-CAM’s cord and the extension cord was protected inside an upside down Gatorade bottle.

Monitoring the bluebirds

The ESP32-CAM acts as a WiFi access point, transmitting an image every 2 seconds, allowing monitoring of progress throughout the nesting period.

After monitoring the birdhouse for 10 days, it was very exciting to see the first signs that a bird had accepted the birdhouse as a place to build a nest on 3/23.
This was the state of the nest on 3/28. I still didn’t know what species of bird was building the nest.
Mother bluebird was found on the nest for the first time on 4/7.
Three eggs were found on 4/7. Four eggs were found on 4/10.

Although my program was not designed to transmit video, I realized I could record my iPhone screen as a video to create a video of the web page updates.

These were the eggs on 4/22.
All eggs had hatched on 4/23.
4/27
5/1
5/3
5/9, the day before fledging.
This was the last bird to leave on fledging day, 5/10/25.

Creating a time lapse video

Images were taken in two different modes. One mode involved recording an image every 15 minutes throughout the nesting period. The other mode involved recording a burst of 500 images as fast as possible, which for the ESP32-CAM seems to be about once every 15 seconds.

These still images were combined in Abode Photoshop to make a time lapse video.

This is a time lapse video of the day the birds fledged. It was created from a burst of 500 images taken over a few minutes.
This is the full time lapse from 3/21 – 5/10/25.

Results of observations of birds

Timing

EventDateDays since previous event
Bluebird house installed3/13/25
First sign of nest building3/23/2510 days
Hatching4/22/2530 days
Fledging5/10/2518 days

Based on WiFi observations, my best estimate of egg laying is 4/7/25 for the third egg and 4/8/25 for the 4th egg.

Presence of mother bird

The mother sat on the nest much less than I expected.

Movement of grass in the nest suggested that the birds were frequently visiting the nest but did not stay long.

The mother was more present after the fourth egg was laid until the babies began to have feathers. Once the babies had feathers, the mother’s presence dropped off to a level similar to what it was prior to hatching the eggs. Even during the period when the mother was most present, the average was around 70% of the time.

Once the babies had feathers, the parents were frequently visiting as evidenced by the growth of the babies, but the duration of each visit was brief, so it was rarely captured on camera.

Observations of the recording system

Average Photo Intensity

I wrote a Python program to calculate the average grayscale intensity of each photo. This shows the day and night cycle as well as the times when the LED was on continuously. It also shows when the frequency of images was increased.

Lighting

The built-in LED was used periodically to make occasional night time observations. It was also used to record all images for several days at a rate of about once every 15 minutes. This did not seem to bother the birds.

The images were more uniformly lit with the LED, although the color was too yellow.

Having the images lit with natural lighting enabled determination of the date and approximate time of day an image was taken. Using an LED lost this information and consistent use of the LED for several days caused complete loss of date information.

Images

5,291 images were recorded over a period of 58 days (3/13-5/10), averaging one image every 15 minutes. Of these, 6 images were corrupted, all at night on two separate nights. The corrupted images were easily identified as they a size of 0 bytes.

WiFi range

The ESP32-CAM can act as an access point. When it is inside the birdhouse, the WiFi range was about 30 feet. This required being near the birdhouse which did cause some defensive behavior by the adults.

WiFi glitches

The WiFi would occasionally not be working and power had to be cut to reboot the system. It is unclear how long these glitches might be occurring before they were discovered but it is likely to be at least hours. There is no obvious discontinuities in the image set, suggesting the system continued to record images even when the WiFi was not working.

Discussion

Bird Observations

Although this was primarily a demonstration of the capabilities of the ESP32-CAM and a trial of the program I was using, I was able to obtain some interesting results.

Recording frequent images within the birdhouse allowed for accurate dating of the initiation of nest building, and an estimate of the date the last egg was laid. This, in turn, allowed for an accurate estimation of the date of hatching and ultimately the date of fledging, which allowed for closer monitoring of these events.

It also allowed for quantification of the percent of the time that an adult bird was present.

The birds did not seem to be bothered by the LED flashing every 15 minutes.

It would be good to have a second camera set up outside the birdhouse on the day that they fledge.

System Trial

The system settings that seemed to work the best include:

Image frequencyevery 15 minutes
LED duration200 msec
LED duty cycle20%
Flashoff

The system has the ability to take a burst of 500 images as quickly as possible. This worked well although there were few times when it seemed helpful.

Problems and Planned Improvements

Inconsistent Lighting

Using natural lighting led to inconsistent lighting and images that were so dark that details were difficult to see. It may be possible to have the LED on at a low level at all times to improve the lighting. Occasional light flashing did not seem to concern the birds. However, frequent flashing may be more disturbing. A constant low level of light may work better.

Yellow Lighting

The LED flash produces a yellow tint to the images. This might be corrected with an external white LED.

Inability to Accurately Date Events

One of the major limitations I experienced was not being able to accurately date events, such as the timing of egg laying. Passage of time can be easily determined by the cycle of light when natural lighting is used. However, during testing of the LED, this information was lost.

There are several methods that can be used to accurately date events in future projects. These include:

  • Keeping the frequency of image recording constant
  • Using natural lighting consistently
  • Using a real-time clock
  • Logging changes to the settings to keep track of the frequency of image recording

Many Dark Images

Many of the images were taken in the dark. It is unclear if any interesting activity occurred after dark. Perhaps the mother was more frequently present at night when the temperatures are lower and she is not spending time feeding.

The ESP32-CAM is able to use a 32MB SD card. Despite the system taking over 7,000 images, there was plenty of memory. Recording dark images did not cause any memory concerns.

However, nearly half of the images were too dark to be useful. These images used significant computer time and manpower as the images were converted into a video.

The ESP32-CAM does not have the processing power to analyze the images and determine if they are worth saving. However, an external photodiode could be used to determine if there is enough light to take a picture.

A list of images that are too dark to use can be generated with a Python program and then excluded from the video without manual labor.

Filenames Were Not Sequential When Alphabetized

Without a real time clock, the date and time that a file is recorded is not accurate and is useless if the system has to be rebooted, which happens from time to time. Filenames were numbered in a way that led to them not being sequential when alphabetized. This led to a significant amount of work to fix this later.

This can be addressed in future projects by padding the numbers in the filenames with zeroes.

WiFi Glitches

The WiFi occasionally does not work. At other times, the WiFi shows that it is available, but it will not respond to page update requests. It is unclear why these happen or whether the glitches affect the ability to record images.

I also do not know how frequently these glitches occur, although my impression is they occur every few days.

Loss of Setting Changes

Rebooting the system results in loss of any changes to the system. For instance, if the system is initially set up to record once an hour (reasonable before nest building begins), but then set to every 15 minutes after activity in the birdhouse is noted, it will revert back to record once an hour after a system reboot. It may be several hours before this is noticed and corrected.

This can be addressed by saving the changes to the SD card so that the system can be initialized with the new settings after rebooting.

Summary of Planned Improvements and Observations

  • set up camera outside of birdhouse on the expected fledging day
  • use consistent frequency and lighting
  • log errors, resets, WiFi accessed and setting changes
  • pad numbers in filenames with zeroes
  • take note of when glitches occur
  • take note of what time of day provides the best natural lighting
  • take a burst of images on hatching day
  • determine how quickly new nest building starts
  • save new settings to the SD card for initialization after rebooting

References

Birding

Bluebird Nestling Day 1-21 Time Sequence Photos – Sialis.org: Info on Bluebirds & Other Small Cavity Nesters

Where to Place a Bluebird House [The Best Location] – Bluebird Landlord

Nest Box Monitoring Protocol — Virginia Bluebird Society

Bluebirds in Missouri

Coding

ESP32-CAM: Set Access Point (AP) using Arduino IDE | Random Nerd Tutorials

ESP32-CAM Take Photo and Save to MicroSD Card | Random Nerd Tutorials

ESP32-CAM Take Photo and Display in Web Server | Random Nerd Tutorials

Appendix

Code

/* Program Name:  Time Lapse Version 5.2
 * 
 * Description:
 * Initializes variables by reading "instructions.txt" from SD card
 * Records image to SD card
 * Sets up access point & streams image as well as data 
 *    Data includes:
 *      frequency of images recorded
 *      current picture number
 *      number of pictures recorded
 *      file name
 *      comment from instructions.txt
 *      elapsed time since reboot
 *      elapsed time since last photo
 *      free disk space
 *      error message
 *    
 * Version 5:  Put inputs at top
 *             More inputs 
 *             Read instructions in any order
 *             Save new instructions
 *             limitValues() to limit possible input errors
 *             burst pictures for about 20 seconds
 *                    button on website to start burst
 *                    stops website asking for updates
 *                    takes 500 pictures as fast as possible
 *                    uses different routine with fewer checks to speed picture taking
 *                    program checks every 1 second to see if ready to burst
 *             
 * Example "instructions.txt" file:   
comment:  Record inside bluebird house
frequency: 900
filename: bluebird
flash:  off
led duration:  100
duty cycle:  20
ssid: 
password: (must be at least 8 characters)

This is the instruction file (config file) for Time_Lapse_V5_2
This file must be named instructions.txt
The keys can be in any order.



 */

#include "esp_http_server.h"
#include "esp_camera.h"
#include "WiFi.h"
#include "FS.h"
#include "soc/soc.h"           // Disable brownout problems
#include "soc/rtc_cntl_reg.h"  // Disable brownout problems
#include "SD_MMC.h"
#include <EEPROM.h>
#include <stdio.h>
#include <stdlib.h>

#define EEPROM_SIZE 2
#define CAMERA_MODEL_AI_THINKER  // Define for AI Thinker ESP32-CAM model

String ssid = "ESP32-CAM";
String password = //put your password here
unsigned long startTime;
unsigned long pictureTime;
unsigned long elapsedTime;

//PWM
const int pwmChannel = 12;  // Use channel 12 (chosen at random), channel 0 used for camera clock
const int pwmFreq = 5000;  // 5 kHz frequency
const int pwmResolution = 8;  // 8-bit resolution (0-255)
const int ledPin = 4;  // GPIO4 for output
int ledDuration = 100;
int dutyCycle = 20;

//burst
String burstEnabled = "on";
String burstNow = "off";


int pictureNumber = 0;  //number appended to filename, taken from EEPROM
int pictureCount = 0;  //number taken since reboot
int freq = 30;  //1 = 1 sec
String filename = "default";
String message = "";
String comment = "";
String flash = "on";
int testFlash = false;

String IPaddress = "";

// Camera pin definitions (for AI Thinker model)
#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

// ============================
// 🚀 Initialize Camera
// ============================
void init_camera() {
    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;

    if (psramFound()) {
        Serial.println("PSRAM found");
        config.frame_size = FRAMESIZE_SVGA;
        config.jpeg_quality = 10;
        config.fb_count = 2;
    } else {
        Serial.println("PSRAM not found");
        config.frame_size = FRAMESIZE_SVGA;
        config.jpeg_quality = 12;
        config.fb_count = 1;
    }

    if (esp_camera_init(&config) != ESP_OK) {
        Serial.println("Camera init failed!");
        ESP.restart();
    }
}

// ============================
// 📸 Capture Image Handler
// ============================
esp_err_t capture_handler(httpd_req_t *req) {
    //Version 4:  
    //Serial.println("changed duty Cycle");
    if (flash == "on" && testFlash) {
//      pinMode(4,OUTPUT);
//      digitalWrite(4,HIGH);
      ledcWrite(pwmChannel, dutyCycle); // Change duty cycle

      delay(ledDuration);
      testFlash = false;
    }
    camera_fb_t *fb = esp_camera_fb_get();

    //Version 4:
    ledcWrite(pwmChannel, 0);

    
    if (!fb) {
        Serial.println("Camera capture failed");
        message = "Camera capture failed";
        return ESP_FAIL;
    }

    // Set the response type to image/jpeg
    httpd_resp_set_type(req, "image/jpeg");

    // Send the captured image data
    httpd_resp_send(req, (const char *)fb->buf, fb->len);

    // Return the framebuffer
    esp_camera_fb_return(fb);
    digitalWrite(4,LOW);
    return ESP_OK;
}

// ============================
// 📊 Sensor Data Handler
// ============================
esp_err_t sensor_handler(httpd_req_t *req) {
    elapsedTime = millis() - startTime; // Calculate elapsed time
    unsigned long seconds = (elapsedTime / 1000) % 60;
    unsigned long minutes = (elapsedTime / (1000 * 60)) % 60;
    unsigned long hours = (elapsedTime / (1000 * 60 * 60)) % 24;
    String elapsedTimeString = String(hours) + ":" + 
                    (minutes < 10 ? "0" : "") + String(minutes) + ":" + 
                    (seconds < 10 ? "0" : "") + String(seconds);

    elapsedTime = millis() - pictureTime; // Calculate elapsed time
    seconds = (elapsedTime / 1000) % 60;
    minutes = (elapsedTime / (1000 * 60)) % 60;
    hours = (elapsedTime / (1000 * 60 * 60)) % 24;
    String elapsedTimeString2 = String(hours) + ":" + 
                    (minutes < 10 ? "0" : "") + String(minutes) + ":" + 
                    (seconds < 10 ? "0" : "") + String(seconds);
    
    uint64_t freeBytes = SD_MMC.totalBytes() - SD_MMC.usedBytes();
    float freeMB = (float)freeBytes / (1024 * 1024);  // Convert to MB
    char buffer[21];  
    sprintf(buffer, "%.2f", freeMB);
    String freeSpaceStr = String(buffer);
                                        
    String json = "{ \"freq\": " + String(freq) + 
              ", \"number\": " + String(pictureNumber) + 
              ", \"pictureCount\": " + String(pictureCount) +
              ", \"filename\": \"" + filename + "\"" + 
              ", \"flash\": \"" + flash + "\"" + 
              ", \"ledDuration\": \"" + ledDuration + "\"" + 
              ", \"dutyCycle\": \"" + dutyCycle + "\"" +
              ", \"comment\": \"" + comment + "\"" +
              ", \"elapsedTime\": \"" + elapsedTimeString + "\"" + 
              ", \"elapsedTime2\": \"" + elapsedTimeString2 + "\"" + 
              ", \"freeBytes\": \"" + freeSpaceStr + "\"" +
              ", \"message\": \"" + message + "\"}"; 
    httpd_resp_send(req, json.c_str(), HTTPD_RESP_USE_STRLEN);
    return ESP_OK;
}

// ============================
// 🌐 Serve HTML Page
// ============================
const char *html_content = R"rawliteral(
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ESP32-CAM Streaming</title>
<style>
body { font-family: Arial, sans-serif; text-align: center; }
#stream { max-width: 100%; height: auto; margin-top: 20px; }
#sensor-data { font-size: 20px; margin-top: 20px; font-weight: bold; }
</style>
</head>
<body>
  <img id="esp32Image" alt="Live Stream"><br><br>
  Enter new frequency:<br>
  <input id="inputFreq"></input><br><br>
  <input id="flashcheckbox" type="checkbox">Flash On</input><br><br>
  Enter new LED duration:<br>
  <input id="ledPWM"></input><br><br>
  Enter new LED duty cycle: <br>
  <input id="ledBrightness"></input><br><br>
  <button id="btn" onclick="sendFrequency()">Submit</button>
  <br><br>
  <button id="burstNow" onclick = "burstNow()">Burst Now</button>
  <br>
  <p id="data1">Frequency: -- seconds</p>
  <p id="data3">File Name: </p>
  <p id="flash">Flash: </p>
  <p id="ledDuration">LED Duration: </p>
  <p id="dutyCycle">Duty Cycle: </p>
  <p id="data2">Picture Number: </p>
  <p id="pictureCount">Number of Pictures Saved Since Last Reboot:</p>
  <p id="refresh">Refresh: </p>
  <p id="elapsedTime">Elapsed Time Since Last Reboot: </p>
  <p id="elapsedTime2">Elapsed Time Since Last Photo: </p>
  <p id="comment">Comment: </p>
  <p id="freeBytes">Memory Free: </p>
  <p id="data4">Error Message: </p>
  <p id="program">Version: <br><b>Time Lapse V5.2.1</b></p>
  <br>

  
<script>
refreshCount = 0;
let fetch1 = "";
let fetch2 = "";

function fetchSensorData() {
    refreshCount++;
    fetch(window.location.origin+"/sensor")
        .then(response => response.json())
        .then(data => {
          document.getElementById("data1").innerHTML = "Frequency: <br><b>" + data.freq + " seconds</b>";
          document.getElementById("data2").innerHTML = "Picture Number: <br><b>" + data.number+"</b>";
          document.getElementById("data3").innerHTML = "File Name: <br><b>" + data.filename+"</b>";
          document.getElementById("flash").innerHTML = "Flash: <br><b>" + data.flash+"</b>";
          document.getElementById("ledDuration").innerHTML = "LED Duration: <br><b>" + data.ledDuration+" msec </b>";
          document.getElementById("dutyCycle").innerHTML = "LED Duty Cycle: <br><b>" + data.dutyCycle + "</b>";
          document.getElementById("pictureCount").innerHTML = "Number of Pictures Saved Since Last Reboot: <br><b>" + data.pictureCount+"</b>";
          document.getElementById("refresh").innerHTML = "Refresh: <br><b>" + refreshCount+"</b>";
          document.getElementById("elapsedTime").innerHTML = "Elapsed Time Since Reboot: <br><b>" + data.elapsedTime+"</b>";
          document.getElementById("elapsedTime2").innerHTML = "Elapsed Time Since Last Photo: <br><b>" + data.elapsedTime2+"</b>";
          document.getElementById("comment").innerHTML = "Comment: <br><b>" + data.comment+"</b>";
          document.getElementById("freeBytes").innerHTML = "Memory Free: <br><b>" + data.freeBytes + " MB</b>";
          document.getElementById("data4").innerHTML = "Error Message: <br><b>" + data.message+"</b>";
        })        
        .catch(error => console.error("Sensor fetch error:", error));
}
function fetchImage() {
    const url = window.location.origin+"/capture";
    fetch(url)
        .then(response => {
            if (response.ok) {
                return response.blob();  // Convert to blob (binary data)
            } else {
                throw new Error("Failed to fetch the image");
            }
        })
        .then(blob => {
            const imageUrl = URL.createObjectURL(blob);
            document.getElementById("esp32Image").src = imageUrl;
        })
        .catch(error => {
            console.error("Error fetching image:", error);
        });
}

function sendFrequency() {
   f = document.getElementById("inputFreq").value;
   g = document.getElementById("ledPWM").value;
   h = document.getElementById("ledBrightness").value;
   i = "off";
   if (document.getElementById("flashcheckbox").checked) {
    i = "on";
   }
   var xhr = new XMLHttpRequest();
   xhr.open("GET", "/action?freq=" + f+"&led="+g+"&bright="+h+"&flash="+i+"&burst=off", true);
   xhr.send();
}

function burstNow() {
  var xhr = new XMLHttpRequest();
  xhr.open("GET", "/action?freq=&led=&bright=&flash=&burst=on", true);
  xhr.send();
  clearInterval(fetch1);
  clearInterval(fetch2);
  setTimeout(setIntervals,20000);
}

function setIntervals() {
  fetch1 = setInterval(fetchSensorData, 2000);
  fetch2 = setInterval(fetchImage, 2000);
}

setIntervals();

</script>
</body>
</html>
)rawliteral";

// ============================
// 📄 HTML Page Handler
// ============================
esp_err_t index_handler(httpd_req_t *req) {
    httpd_resp_send(req, html_content, HTTPD_RESP_USE_STRLEN);
    Serial.println("in page handler");
    return ESP_OK;
}


// ============================
// 📄 HTML Command Handler
// ============================
static esp_err_t cmd_handler(httpd_req_t *req){
  char*  buf;
  size_t buf_len;
  char variable[32] = {0,};
  char ledVariable[32] = {0,};
  char ledBright[32] = {0,};
  char flashVariable[32] = {0,};
  char burstVariable[32] = {0,};
  
  buf_len = httpd_req_get_url_query_len(req) + 1;
  if (buf_len > 1) {
    buf = (char*)malloc(buf_len);
    if(!buf){
      Serial.println("failure 1");
      httpd_resp_send_500(req);

      return ESP_FAIL;
    }
    if (httpd_req_get_url_query_str(req, buf, buf_len) == ESP_OK) {
      if (httpd_query_key_value(buf, "freq", variable, sizeof(variable)) == ESP_OK) {
      } else {
        free(buf);
        Serial.println("failure 2");
        httpd_resp_send_404(req);
        return ESP_FAIL;
      }

      if (httpd_query_key_value(buf, "led", ledVariable, sizeof(ledVariable)) == ESP_OK) {
      } else {
        free(buf);
        Serial.println("failure 6");
        httpd_resp_send_404(req);
        return ESP_FAIL;
      }

      if (httpd_query_key_value(buf, "bright", ledBright, sizeof(ledBright)) == ESP_OK) {
      } else {
        free(buf);
        Serial.println("failure 7");
        httpd_resp_send_404(req);
        return ESP_FAIL;
      }
      if (httpd_query_key_value(buf, "flash", flashVariable, sizeof(flashVariable)) == ESP_OK) {
      } else {
        free(buf);
        Serial.println("failure 8");
        httpd_resp_send_404(req);
        return ESP_FAIL;
      }
      if (httpd_query_key_value(buf, "burst", burstVariable, sizeof(burstVariable)) == ESP_OK) {
      } else {
        free(buf);
        Serial.println("failure 9");
        httpd_resp_send_404(req);
        return ESP_FAIL;
      }
    } else {
      free(buf);
      Serial.println("failure 4");
      httpd_resp_send_404(req);
      return ESP_FAIL;
    }
    free(buf);
  } else {
    Serial.println("failure 3");
    httpd_resp_send_404(req);
    return ESP_FAIL;
  }
  Serial.print("variable = ");
  Serial.println(variable);
  Serial.print("ledVariable = ");
  Serial.println(ledVariable);
  Serial.print("flash = ");
  Serial.println(flashVariable);
  if (atoi(variable) > 0) {
    freq = atoi(variable);
  }
  if (atoi(ledVariable) > 0) {
    ledDuration = atoi(ledVariable);
    testFlash = true;
  }
  if (atoi(ledBright) > 0) {
    dutyCycle = atoi(ledBright);
    testFlash = true;
  }
  flash = "off";
  Serial.print("flashVariable = ");
  Serial.println(flashVariable);
  if (strcmp(flashVariable,"on") == 0) {  //changed
    flash = "on";
  }
  burstNow = "off";
  Serial.print("burstVariable = ");
  Serial.print(burstVariable);
  Serial.println("END");
  if (strcmp(burstVariable,"on") == 0) {  //changed
    Serial.println("in burstVariable");
    burstNow = "on";
  }
  limitInputs();
}

void limitInputs() {
  if (freq < 0) {
    freq = 0;
  }
  if (freq > 32000) {
    freq = 32000;
  }
  if (flash != "on") {
    flash = "off";
  }
  if (ledDuration < 0) {
    ledDuration = 0;
  }
  if (ledDuration > 32000) {
    ledDuration = 32000;
  }
  if (dutyCycle < 0) {
    dutyCycle = 0;
  }
  if (dutyCycle > 100) {
    dutyCycle = 100;
  }
}

// ============================
// 🌍 Start HTTP Server
// ============================
esp_err_t start_http_server() {
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
    httpd_handle_t server = NULL;

    if (httpd_start(&server, &config) == ESP_OK) {
        httpd_uri_t capture_uri = { .uri = "/capture", .method = HTTP_GET, .handler = capture_handler, .user_ctx = NULL };
        httpd_register_uri_handler(server, &capture_uri);

        httpd_uri_t sensor_uri = { .uri = "/sensor", .method = HTTP_GET, .handler = sensor_handler, .user_ctx = NULL };
        httpd_register_uri_handler(server, &sensor_uri);

        httpd_uri_t index_uri = { .uri = "/", .method = HTTP_GET, .handler = index_handler, .user_ctx = NULL };
        httpd_register_uri_handler(server, &index_uri);

        httpd_uri_t action_uri = {
            .uri      = "/action",
            .method   = HTTP_GET,
            .handler  = cmd_handler,
            .user_ctx = NULL
        };
        httpd_register_uri_handler(server, &action_uri);

        return ESP_OK;
    }
    return ESP_FAIL;
}

void init_SD() {
      //Serial.println("Starting SD Card");
  if(!SD_MMC.begin("/sdcard",true)){  //"true" sets 1-bit mode
    Serial.println("SD Card Mount Failed");
    message = "SD Card Mount Failed";
    return;
  }
  
  uint8_t cardType = SD_MMC.cardType();
  if(cardType == CARD_NONE){
    Serial.println("No SD Card attached");
    message = "No SD Card attached";
    return;
  }
}

void readInput() {
  //Read input file  
  File inputFile = SD_MMC.open("/instructions.txt",FILE_READ);
    if (!inputFile) {
      Serial.println("Failed to open instructions.txt");
      message = "Failed to open instructions.txt";
    }
    else {
      int linecount = 0;
      while (inputFile.available()) {
        String line = inputFile.readStringUntil('\n');
        Serial.println(line);
        int colonIndex = line.indexOf(':');
        if (colonIndex != -1) {
          String key = line.substring(0,colonIndex);
          String value = line.substring(colonIndex + 1);
          key.trim();
          value.trim();
          Serial.println(value);
          linecount++;
          if (key == "comment") {
              comment = value;
              Serial.print("comment = ");
              Serial.println(comment);
          }
          if (key == "frequency") {
              freq = value.toInt();
              Serial.print("frequency = ");
              Serial.println(freq);
          }
          if (key == "filename") {
              filename = value;
              Serial.print("filename = ");
              Serial.println(filename);
          }
          if (key == "password") {
              password = value;
              break;
          }
          if (key == "flash") {
              flash = value;
              Serial.print("flash = ");
              Serial.println(flash);
          }
          if (key == "led duration") {
              ledDuration = value.toInt();
              Serial.print("led duration = ");
              Serial.println(ledDuration);
          }
          if (key == "duty cycle") {
              dutyCycle = value.toInt();
              Serial.print("duty cycle = ");
              Serial.println(dutyCycle);
          }
        }
      }
      ssid = "ESP32-CAM-"+filename;
      limitInputs();
    }
}

void savePicture() {
    camera_fb_t * fb = NULL;

    //PWM:  Added Version 4
//    for (int ledDuration = 0; ledDuration <= 5; ledDuration++) {
//        ledcWrite(pwmChannel, ledDuration); // Change duty cycle
//        Serial.println(ledDuration);
//        delay(1000);
//    }
    Serial.println(flash);
    if (flash == "on") {
      Serial.println("flash is on");
      ledcWrite(pwmChannel, ledDuration); // Change duty cycle
//      pinMode(ledPin,OUTPUT);
//      digitalWrite(ledPin,HIGH);
      delay(ledDuration);
    }

//  ledcDetachPin(ledPin);
  
  // Take Picture with Camera
//  pinMode(4,OUTPUT);  //uncomment these two lines to turn flash on
//  digitalWrite(4,HIGH);
  fb = esp_camera_fb_get();  
  if(!fb) {
    Serial.println("Camera capture failed");
    message = "Camera capture failed";
    return;
  }
  ledcWrite(pwmChannel, 0); // Change duty cycle

  digitalWrite(4,LOW);
  // initialize EEPROM with predefined size
  EEPROM.begin(EEPROM_SIZE);
  int i = EEPROM.read(0);
  int j = EEPROM.read(1)+1;
  if (j > 255){
    j = 0;
    i++;
  }
  if (i > 255) {
    i = 0;
    j = 0;
  }
  
  pictureNumber = i*256+j;

   // Path where new picture will be saved in SD Card
   String path = "/"+filename + String(pictureNumber)+".jpg";
   if (burstEnabled == "on" && burstNow == "on") {
     String path = "/burst-"+filename + String(pictureNumber)+".jpg";
   }

  fs::FS &fs = SD_MMC; 
  Serial.printf("Picture file name: %s\n", path.c_str());
  
  File file = fs.open(path.c_str(), FILE_WRITE);
  if(!file){
    Serial.println("Failed to open file in writing mode");
    message = "Failed to open file in writing mode";
   // flashLED(20);
  } 
  else {
    file.write(fb->buf, fb->len); // payload (image), payload length
    Serial.printf("Saved file to path: %s\n", path.c_str());
    pictureCount++;
    pictureTime = millis();
    EEPROM.write(0, i);
    EEPROM.write(1, j);
    EEPROM.commit();
  }
  file.close();
  esp_camera_fb_return(fb); 
//
//  //Slowly turn LED off
//  //PWM:  Added Version 4
//   for (int ledDuration = 5; ledDuration >= 0; ledDuration--) {
//        ledcWrite(pwmChannel, ledDuration);
//        Serial.println(ledDuration);
//        delay(1000);
//    }
  
}

void burstSavePicture() {
    camera_fb_t * fb = NULL;

    if (flash == "on") {
      ledcWrite(pwmChannel, ledDuration); // Change duty cycle
      delay(ledDuration);
    }
  
  // Take Picture with Camera
  fb = esp_camera_fb_get();  
  if(!fb) {
    Serial.println("Camera capture failed");
    message = "Camera capture failed";
    return;
  }
  ledcWrite(pwmChannel, 0); // Change duty cycle

  digitalWrite(4,LOW);
  // initialize EEPROM with predefined size
  EEPROM.begin(EEPROM_SIZE);
  int i = EEPROM.read(0);
  int j = EEPROM.read(1)+1;
  if (j > 255){
    j = 0;
    i++;
  }
  if (i > 255) {
    i = 0;
    j = 0;
  }
  
  pictureNumber = i*256+j;

   // Path where new picture will be saved in SD Card
  String path = "/burst-"+filename + String(pictureNumber)+".jpg";

  fs::FS &fs = SD_MMC; 
  
  File file = fs.open(path.c_str(), FILE_WRITE);
  if(!file){
    Serial.println("Failed to open file in writing mode");
    message = "Failed to open file in writing mode";
  } 
  else {
    file.write(fb->buf, fb->len); // payload (image), payload length
    pictureCount++;
    pictureTime = millis();
    EEPROM.write(0, i);
    EEPROM.write(1, j);
    EEPROM.commit();
  }
  file.close();
  esp_camera_fb_return(fb);   
}

// ============================
// 🔧 Setup & Loop
// ============================
void setup() {
    Serial.begin(115200);
    
    startTime = millis();

    init_SD();

    readInput();
    
    WiFi.softAP(ssid.c_str(), password.c_str());
    Serial.println("Wi-Fi AP Started");
    Serial.println(WiFi.softAPIP());
    IPaddress = WiFi.softAPIP().toString();
    
    // Turn-off the 'brownout detector'
    WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);

    init_camera();

    if (start_http_server() != ESP_OK) {
        Serial.println("Failed to start HTTP server");
        message = "Failed to start HTTP server";
    }

    //PWM: Added Version 4
    ledcSetup(pwmChannel, pwmFreq, pwmResolution); // Configure PWM
    ledcAttachPin(ledPin, pwmChannel); // Attach channel to pin
}

void loop() {
    savePicture();
    for (int i = 0; i < freq; i++) {
      if (burstEnabled == "on" && burstNow == "on") {
        Serial.println("bursting...");
        for (int j = 0; j < 500; j++) {
          burstSavePicture();
          delay(1);
        }
        Serial.println("done bursting");
        burstNow = "off";
      }
      delay(1000);
    }
}

Comments

Leave a comment