Community project

IoT Crop Guardian Drone

Photo of IoT Crop Guardian Drone
Generated with AI

Muhammad Shoaib Ali

Published November 2, 2026

The IoT Crop Guardian Drone is a solar-powered wildlife deterrent system built around an ESP32 microcontroller that detects motion with a PIR sensor and responds with audio alerts and visual flashes. When motion is detected in crop fields, the system plays prerecorded deterrent sounds through a speaker and triggers the onboard LED flash to scare away animals, while capturing images for remote monitoring via a connected Raspberry Pi.

This guide provides everything needed to assemble and deploy a crop protection node: a complete parts list, wiring diagram for integrating the motion sensor, audio module, and solar charging system, step-by-step assembly instructions, and ready-to-use ESP32 firmware that handles motion detection, sound playback, image capture, and wireless communication. Builders will learn how to configure the system for their field location and customize deterrent responses.

Wiring diagram

Loading diagram…

Gather all the parts

QtyComponent
1

HC-SR501 PIR Motion Sensor

Passive Infrared (PIR) motion detection module with adjustable sensitivity and delay potentiometers. Operates on 5V supply; digital output is nominally 3.3V or 5V depending on module variant (verify before connecting directly to ESP32 3.3V GPIO — use a voltage divider if output is 5V). Outputs HIGH when motion is detected, LOW when idle. Ideal for triggering countdown timer resets in Focus Mode applications. No firmware library required — output read via standard GPIO digitalRead().

1

Resistor

10 kΩ, 1/4 W

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

20 kΩ, 1/4 W

Through-hole resistor (current-limiting in series with an LED)

1

DFPlayer Mini MP3 Audio Module

Small UART-controlled MP3 playback module with a microSD card slot and onboard audio DAC/amplifier interface. The MCU controls playback over serial while speaker/DAC pins connect to the audio output path.

1

8Ω Speaker

8 Ω, 3 W weather-resistant horn speaker

Generic small 8Ω 0.5-3W loudspeaker (~28mm typical). Pair with an I2S amp (MAX98357A) or class-D amp (TPA3116D2) for usable volume; do not drive directly from a GPIO pin. Audio output for music/voice playback.

1

6V 4W Solar Panel

Polycrystalline or monocrystalline solar panel rated 4W peak output at 6V nominal. Typical small-panel values are Vmp about 6V and Imp about 667mA; open-circuit voltage is higher than the nominal working voltage and must stay within the charger module input rating. Two-wire output (V+ and GND). Outdoor/weatherproof rating depends on the exact panel. Directly compatible with CN3791 MPPT solar charger modules only when that module's input-voltage rating is not exceeded. No firmware library required — purely a passive power source.

1

Lithium Ion Battery - 3.7V 2000mAh

3.7V 2000mAh lithium-ion polymer battery with JST-PH connector and protection circuitry. Output ranges from 4.2 V fully charged to 3.0 V cutoff.

1

Adafruit bq25185 USB / DC / Solar Charger with 5V Boost Board

bq25185 multi-source LiPoly/LiIon charger with power-path support combined with a TPS61023 5V boost output for battery-powered projects that need a regulated 5V rail.

1

microSD card, FAT32, prerecorded deterrent sounds

A removable memory card that holds the recorded animal-deterrent MP3 files for the DFPlayer audio module.

Assemble it in 7 steps

1. Prepare the sound card

On a computer, format sound_card_1 as FAT32. Make a folder named mp3 and copy three different deterrent sounds into it as 0001.mp3, 0002.mp3, and 0003.mp3. Slide the card fully into the side socket on dfplayer_1 with the metal contacts facing the socket contacts.

  • Use short recordings such as barking, an alarm, and a predator call so the sounds vary between events.
  • Do not remove the card while dfplayer_1 has power; removing it while powered can corrupt the sound files.

2. Connect the sound player and speaker

Connect dfplayer_1 VCC to the 5V output of solar_charger_1 (power) and dfplayer_1 GND to the common GND line (ground). Connect dfplayer_1 SPK1 to speaker_1 POS and dfplayer_1 SPK2 to speaker_1 NEG (sound). Connect dfplayer_1 TX to GPIO3 on the ESP32-CAM (data) and dfplayer_1 RX to GPIO1 on the ESP32-CAM (data).

  • Mount the horn speaker facing the crop edge, and keep the speaker and DFPlayer inside a splash-protected box with a downward-facing sound opening.
  • Never connect either speaker wire to GND; SPK1 and SPK2 are the two amplified speaker wires, and grounding one can damage dfplayer_1.

3. Wire the motion sensor safely

Connect pir_1 VCC to the 5V output of solar_charger_1 (power) and pir_1 GND to the common GND line (ground). Connect pir_1 OUT to one leg of pir_divider_top_1, join its other leg to one leg of pir_divider_bottom_1, then connect that joined point to GPIO13 on the ESP32-CAM (motion signal). Connect the remaining leg of pir_divider_bottom_1 to GND (ground).

  • The 10 kΩ resistor is pir_divider_top_1 and the 20 kΩ resistor is pir_divider_bottom_1. Keep the two joined resistor legs and the GPIO13 wire in the same breadboard row or solder junction.
  • Do not connect the PIR OUT pin directly to GPIO13: some PIR boards use a 5 V motion signal, which can damage the 3.3 V ESP32-CAM input.

4. Build the solar power chain

Connect solar_panel_1 V+ to solar_charger_1 VIN (solar charging input) and solar_panel_1 GND to solar_charger_1 GND (solar return). Plug battery_1 into the BAT connector on solar_charger_1. Connect solar_charger_1 5V to the ESP32-CAM 5V pin, the PIR VCC, and the DFPlayer VCC (power). Join every ground wire to solar_charger_1 GND (ground).

  • Place the solar panel where it has daylight, and place the battery and charger inside the dry enclosure.
  • Keep V+ and GND from the solar panel separate; swapped or shorted solar wires can damage the charger. Use only the protected single-cell battery listed here.

5. Mount the camera node

Aim the ESP32-CAM lens toward the approach path and keep its built-in white flash LED visible through a clear window; it is the flashing light deterrent. Put the PIR dome below or beside the lens, pointing at the same area. Keep the camera lens, PIR dome, and speaker opening outside the sealed electronics space, while keeping the circuit boards dry.

  • Avoid aiming the PIR at moving branches, reflective metal, or a road, because these can cause unwanted alarms.
  • Do not seal the PIR dome behind thick plastic or the sensor will not detect body heat and movement reliably.

6. Load the ESP32-CAM safely

Before using the Deploy button, unplug the two serial wires between dfplayer_1 and GPIO1/GPIO3 on the ESP32-CAM. After flashing, reconnect dfplayer_1 TX to GPIO3 and dfplayer_1 RX to GPIO1, then power-cycle the node.

  • Those two pins are shared with the ESP32-CAM programming connection, so temporarily unplugging the MP3 player prevents it from interfering with the firmware upload.
  • Do not swap the two serial wires: TX on dfplayer_1 goes to GPIO3, and RX on dfplayer_1 goes to GPIO1.

7. Connect the Raspberry Pi link

Set the Raspberry Pi Wi-Fi connection to join the CropGuard-Node network using password CropGuard1, then give its Wi-Fi connection the local address 192.168.4.2. Run its photo receiver so it listens at port 8080 on the path /cropguard/photo; the ESP32-CAM sends each photo there when the PIR finds motion.

  • On a phone or computer connected to CropGuard-Node, open 192.168.4.1/status to see the node state, 192.168.4.1/sound to start the sound and light, or 192.168.4.1/capture to request a photo.
  • The Raspberry Pi must have a photo receiver running before animal detection can reach the ML and notification phase; without it, the local sound and flashing light still operate.

Review all connections

1. Connections between "solar_panel_1" and "ESP32"

Functionsolar_panel_1ESP32
powerV+ → Adafruit bq25185 USB / DC / Solar Charger with 5V Boost Board VINEXT
groundGND → solar returnGND

2. Connections between "battery_1" and "ESP32"

Functionbattery_1ESP32
powerBAT+ → Adafruit bq25185 USB / DC / Solar Charger with 5V Boost Board BATEXT
groundBAT- → battery returnGND

3. Connections between "solar_charger_1" and "ESP32"

Functionsolar_charger_1ESP32
power5V → regulated power for the field node5V
groundGND → common groundGND

4. Connections between "pir_1" and "ESP32"

Functionpir_1ESP32
powerVCC → sensor power5V
groundGND → sensor groundGND
digitalOUT → Resistor P1EXT

5. Connections between "pir_divider_top_1" and "ESP32"

Functionpir_divider_top_1ESP32
digitalP2 → Resistor P1EXT

6. Connections between "pir_divider_bottom_1" and "ESP32"

Functionpir_divider_bottom_1ESP32
digitalP1 → 3.3 V-safe motion signalGPIO 13
groundP2 → divider groundGND

7. Connections between "dfplayer_1" and "ESP32"

Functiondfplayer_1ESP32
powerVCC → MP3 player power5V
groundGND → MP3 player groundGND
uartTX → MP3-player status serial data to the camera boardGPIO 3
uartRX → sound-selection serial data from the camera boardGPIO 1
dataSPK1 → 8Ω Speaker POSEXT
dataSPK2 → 8Ω Speaker NEGEXT

Deploy the firmware

#include <Arduino.h>
#include "esp_camera.h"
#include <WiFi.h>
#include <WebServer.h>
#include <HTTPClient.h>

// AI Thinker ESP32-CAM camera wiring is fixed on the board.
#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

constexpr int PIR_PIN = 13;
constexpr int DFPLAYER_RX_PIN = 3;  // DFPlayer TX -> ESP32-CAM RX0
constexpr int DFPLAYER_TX_PIN = 1;  // DFPlayer RX -> ESP32-CAM TX0
constexpr int FLASH_LED_PIN = 4;    // white flash LED already fitted to the ESP32-CAM

const char *AP_SSID = "CropGuard-Node";
const char *AP_PASSWORD = "CropGuard1";
const char *PI_UPLOAD_URL = "http://192.168.4.2:8080/cropguard/photo";
const char *NODE_ID = "field-node-01";
const char *NODE_LOCATION = "replace-with-field-GPS-location";

WebServer server(80);
HardwareSerial &dfSerial = Serial;
bool alarmActive = false;
bool motionArmed = true;
uint32_t alarmStartedAt = 0;
uint32_t lastFlashAt = 0;
uint32_t eventCount = 0;
const uint32_t ALARM_DURATION_MS = 60000;
const uint32_t FLASH_INTERVAL_MS = 250;

uint16_t dfChecksum(uint8_t command, uint16_t parameter) {
  return 0U - (0xFF + 0x06 + command + (parameter >> 8) + (parameter & 0xFF));
}

void dfCommand(uint8_t command, uint16_t parameter) {
  const uint16_t check = dfChecksum(command, parameter);
  const uint8_t packet[] = {0x7E, 0xFF, 0x06, command, 0x00,
    static_cast<uint8_t>(parameter >> 8), static_cast<uint8_t>(parameter),
    static_cast<uint8_t>(check >> 8), static_cast<uint8_t>(check), 0xEF};
  dfSerial.write(packet, sizeof(packet));
}

void playRandomDeterrentSound() {
  dfCommand(0x03, random(1, 4)); // /mp3/0001.mp3 to /mp3/0003.mp3
}

bool captureAndUpload() {
  camera_fb_t *frame = esp_camera_fb_get();
  if (frame == nullptr) return false;
  HTTPClient http;
  http.setConnectTimeout(1500);
  http.setTimeout(3500);
  http.begin(PI_UPLOAD_URL);
  http.addHeader("Content-Type", "image/jpeg");
  http.addHeader("X-CropGuard-Node", NODE_ID);
  http.addHeader("X-CropGuard-Location", NODE_LOCATION);
  http.addHeader("X-CropGuard-Event", String(eventCount));
  const int result = http.POST(frame->buf, frame->len);
  http.end();
  esp_camera_fb_return(frame);
  return result >= 200 && result < 300;
}

void startAlarm() {
  if (alarmActive) return;
  eventCount++;
  captureAndUpload();
  playRandomDeterrentSound();
  alarmActive = true;
  alarmStartedAt = millis();
  lastFlashAt = 0;
  digitalWrite(FLASH_LED_PIN, HIGH);
}

void handleStatus() {
  String json = "{\"node\":\"" + String(NODE_ID) + "\",\"alarm\":";
  json += alarmActive ? "true" : "false";
  json += ",\"events\":" + String(eventCount) + "}";
  server.send(200, "application/json", json);
}

void handleSound() {
  startAlarm();
  server.send(200, "text/plain", "Level 1 sound and light started");
}

void handleCapture() {
  eventCount++;
  const bool uploaded = captureAndUpload();
  server.send(uploaded ? 200 : 503, "text/plain", uploaded ? "Photo sent to Raspberry Pi" : "Photo captured but Raspberry Pi did not accept it");
}

bool initCamera() {
  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;
  config.fb_count = psramFound() ? 2 : 1;
  return esp_camera_init(&config) == ESP_OK;
}

void setup() {
  pinMode(PIR_PIN, INPUT);
  pinMode(FLASH_LED_PIN, OUTPUT);
  digitalWrite(FLASH_LED_PIN, LOW);
  // UART0 is shared with programming: unplug DFPlayer TX/RX before flashing.
  dfSerial.begin(9600, SERIAL_8N1, DFPLAYER_RX_PIN, DFPLAYER_TX_PIN);
  delay(1200);
  dfCommand(0x3F, 0);
  delay(500);
  dfCommand(0x06, 25);
  initCamera();
  randomSeed(micros());
  WiFi.mode(WIFI_AP);
  WiFi.softAP(AP_SSID, AP_PASSWORD);
  server.on("/status", HTTP_GET, handleStatus);
  server.on("/sound", HTTP_GET, handleSound);
  server.on("/capture", HTTP_GET, handleCapture);
  server.begin();
}

void loop() {
  server.handleClient();
  const bool motionNow = digitalRead(PIR_PIN) == HIGH;
  if (!motionNow) motionArmed = true;
  if (motionNow && motionArmed && !alarmActive) {
    motionArmed = false;
    startAlarm();
  }
  if (alarmActive) {
    const uint32_t now = millis();
    if (now - lastFlashAt >= FLASH_INTERVAL_MS) {
      lastFlashAt = now;
      digitalWrite(FLASH_LED_PIN, !digitalRead(FLASH_LED_PIN));
    }
    if (now - alarmStartedAt >= ALARM_DURATION_MS) {
      alarmActive = false;
      digitalWrite(FLASH_LED_PIN, LOW);
      dfCommand(0x16, 0);
    }
  }
  delay(10);
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace, diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik