Uniostar
Published © GPL3+

ESP32 RC Plane Camera Flight Recorder with Dashboard

A tiny ESP32 flight recorder that turns any RC airplane into a real flying black box.

BeginnerFull instructions provided30 minutes997
ESP32 RC Plane Camera Flight Recorder with Dashboard

Things used in this project

Hardware components

Espressif esp32 cam
×1
6 DOF Sensor - MPU6050
DFRobot 6 DOF Sensor - MPU6050
×1
BME 280
×1
Resistor 10k ohm
Resistor 10k ohm
×1
Push Button
×1

Software apps and online services

Arduino IDE
Arduino IDE

Story

Read more

Schematics

Schematic

Code

Main Code

C/C++
Copy it into Arduino IDE's empty sketch.
#include "esp_camera.h"
#include "FS.h"
#include "SD_MMC.h"
#include "SPI.h"
#include <Wire.h>
#include <SparkFunBME280.h>
#include <SoftwareSerial.h>

// Camera pin definitions
#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

#define BUTTON_PIN 4

#define SDA 13
#define SCL 16

#define MPU_ADDR 0x68

BME280 bme;

const unsigned long RECORD_DURATION_MS = 180000;
const unsigned long FRAME_PERIOD_MS    = 1000 / 15;

bool recording = false;
unsigned long recordStartTime = 0;
unsigned long frameCount = 0;

String folderName;
String csvPath;

int16_t ax, ay, az;
int16_t gx, gy, gz;

float pitch = 0, roll = 0, yaw = 0;

unsigned long lastTime = 0;
float dt;

// FIX: Reduced from 0.98 to trust accelerometer more and correct drift faster
const float alpha = 0.96;

float gx_offset = 0, gy_offset = 0, gz_offset = 0;

// Gyro deadband — readings below this in deg/s are treated as zero
// Prevents tiny noise from accumulating into drift
const float GYRO_DEADBAND = 0.5f;

// If total gyro magnitude is below this, sensor is considered still
// Used to trigger dynamic bias correction
const float STILL_THRESHOLD = 1.0f;

// How aggressively to nudge offsets during still periods (0 = never, 1 = instant)
const float BIAS_ADAPT_RATE = 0.001f;

void setup()
{
  Serial.begin(115200);
  pinMode(33, OUTPUT);

  // ===== Camera config =====
  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     = 1;
  config.fb_location  = CAMERA_FB_IN_DRAM;
  config.grab_mode    = CAMERA_GRAB_WHEN_EMPTY;

  if (esp_camera_init(&config) != ESP_OK)
  {
    Serial.println("Camera failed");
    while(1);
  }

  if (!SD_MMC.begin("/sdcard", true))
  {
    Serial.println("SD Card init failed");
    while(1);
  }

  if (SD_MMC.cardType() == CARD_NONE)
  {
    Serial.println("SD Card not found");
    while(1);
  }

  pinMode(BUTTON_PIN, INPUT);

  Wire.begin(SDA, SCL);

  bme.settings.I2CAddress = 0x76;
  bme.settings.runMode = 3;
  bme.settings.tStandby = 0;
  bme.settings.filter = 0;
  bme.settings.tempOverSample = 1;
  bme.settings.pressOverSample = 1;
  bme.settings.humidOverSample = 1;

  if (!bme.beginI2C())
  {
    Serial.println("BME280 not found");
    while(1);
  }

  Wire.beginTransmission(MPU_ADDR);
  Wire.write(0x6B);
  Wire.write(0x00);
  Wire.endTransmission();

  Wire.beginTransmission(MPU_ADDR);
  Wire.write(0x75);
  Wire.endTransmission(false);
  Wire.requestFrom(MPU_ADDR, 1, true);

  uint8_t whoAmI = Wire.read();

  if (whoAmI != 0x68)
  {
    Serial.print("MPU6050 not found, WHO_AM_I returned: 0x");
    Serial.println(whoAmI, HEX);
    while(1);
  }

  lastTime = millis();
}

void startRecording()
{
  recordStartTime = millis();
  frameCount = 0;

  folderName = "/VID_" + String(recordStartTime);
  SD_MMC.mkdir(folderName);

  csvPath = folderName + "/data.csv";
  File csvFile = SD_MMC.open(csvPath.c_str(), FILE_WRITE, true);

  if (csvFile)
  {
    csvFile.println("timestamp_ms,temperature_C,pressure_hPa,alt_ft,humidity_%,pitch_deg,roll_deg,yaw_deg");
    csvFile.close();
  }

  recording = true;
}

void stopRecording()
{
  recording = false;
}

void loop()
{
  if (!recording)
  {
    if (digitalRead(BUTTON_PIN) == HIGH)
    {
      delay(30);
      if (digitalRead(BUTTON_PIN) == HIGH)
      {
        calibrateGyro();
        startRecording();
        while (digitalRead(BUTTON_PIN) == HIGH);
      }
    }
    return;
  }

  if (millis() - recordStartTime >= RECORD_DURATION_MS)
  {
    stopRecording();
    return;
  }

  unsigned long frameStart = millis();

  camera_fb_t *fb = esp_camera_fb_get();

  if (fb)
  {
    String imgPath = folderName + "/IMG_" + String(frameCount) + ".jpg";
    File imgFile = SD_MMC.open(imgPath.c_str(), FILE_WRITE);

    if (imgFile)
    {
      imgFile.write(fb->buf, fb->len);
      imgFile.close();
    }

    esp_camera_fb_return(fb);
  }

  float temp = bme.readTempC();
  float pres = bme.readFloatPressure() / 100.0;
  float alt  = bme.readFloatAltitudeFeet();
  float hum  = bme.readFloatHumidity();

  readMPU6050();
  computeMPU6050Values();

  File csvFile = SD_MMC.open(csvPath.c_str(), FILE_APPEND);

  if (csvFile)
  {
    csvFile.print(millis());   csvFile.print(",");
    csvFile.print(temp, 2);   csvFile.print(",");
    csvFile.print(pres, 2);   csvFile.print(",");
    csvFile.print(alt, 2);    csvFile.print(",");
    csvFile.print(hum, 2);    csvFile.print(",");
    csvFile.print(pitch, 2);  csvFile.print(",");
    csvFile.print(roll, 2);   csvFile.print(",");
    csvFile.println(yaw, 2);
    csvFile.close();
  }

  frameCount++;
  unsigned long elapsed = millis() - frameStart;

  if (elapsed < FRAME_PERIOD_MS)
    delay(FRAME_PERIOD_MS - elapsed);
}

void calibrateGyro()
{
  // FIX: Increased samples from 200 to 1000 for a much more accurate offset
  const int samples = 1000;
  long gx_sum = 0, gy_sum = 0, gz_sum = 0;

  for (int i = 0; i < samples; i++)
  {
    readMPU6050();
    gx_sum += gx;
    gy_sum += gy;
    gz_sum += gz;
    delay(2);
  }

  gx_offset = gx_sum / (float)samples;
  gy_offset = gy_sum / (float)samples;
  gz_offset = gz_sum / (float)samples;

  lastTime = millis();
}

void readMPU6050()
{
  Wire.beginTransmission(MPU_ADDR);
  Wire.write(0x3B);
  Wire.endTransmission(false);
  Wire.requestFrom(MPU_ADDR, 14, true);

  ax = Wire.read() << 8 | Wire.read();
  ay = Wire.read() << 8 | Wire.read();
  az = Wire.read() << 8 | Wire.read();
  Wire.read(); Wire.read(); // skip temp
  gx = Wire.read() << 8 | Wire.read();
  gy = Wire.read() << 8 | Wire.read();
  gz = Wire.read() << 8 | Wire.read();
}

void computeMPU6050Values()
{
  unsigned long now = millis();
  dt = (now - lastTime) / 1000.0f;
  lastTime = now;

  float gx_cal = gx - gx_offset;
  float gy_cal = gy - gy_offset;
  float gz_cal = gz - gz_offset;

  float gx_dps = gx_cal / 131.0f;
  float gy_dps = gy_cal / 131.0f;
  float gz_dps = gz_cal / 131.0f;

  // FIX: Deadband — zero out tiny gyro readings that are just noise
  if (fabs(gx_dps) < GYRO_DEADBAND) gx_dps = 0;
  if (fabs(gy_dps) < GYRO_DEADBAND) gy_dps = 0;
  if (fabs(gz_dps) < GYRO_DEADBAND) gz_dps = 0;

  float ax_g = ax / 16384.0f;
  float ay_g = ay / 16384.0f;
  float az_g = az / 16384.0f;

  float pitch_acc = atan2(ay_g, sqrt(ax_g*ax_g + az_g*az_g)) * 180.0f / PI;
  float roll_acc  = atan2(-ax_g, az_g) * 180.0f / PI;

  pitch = alpha * (pitch + gx_dps * dt) + (1.0f - alpha) * pitch_acc;
  roll  = alpha * (roll  + gy_dps * dt) + (1.0f - alpha) * roll_acc;

  yaw += gz_dps * dt;
  if (yaw >  180.0f) yaw -= 360.0f;
  if (yaw < -180.0f) yaw += 360.0f;

  // FIX: Dynamic bias correction — if sensor is nearly still, slowly
  // nudge offsets toward current raw readings to cancel temperature drift
  float total_dps = fabs(gx_dps) + fabs(gy_dps) + fabs(gz_dps);

  if (total_dps < STILL_THRESHOLD)
  {
    gx_offset += gx_cal * BIAS_ADAPT_RATE;
    gy_offset += gy_cal * BIAS_ADAPT_RATE;
    gz_offset += gz_cal * BIAS_ADAPT_RATE;
  }
}

Flight Visualizer

Credits

Uniostar
12 projects • 11 followers
Electrical Engineering Undergrad Student specialized in low-level programming, IoT projects, and microcontroller electronics.

Comments