ESP32-CAM - Fotos speichern und senden

Das ESP32-CAM-Board besitzt einen Slot für den Zugriff auf eine Mikro-SD-Karte von max. 4GB Größe. Hierauf können z.B. aufgenommene Fotos gespeichert werden. Eine andere Möglichkeit ist, die aufgenommenen Fotos auf einem Server zu sichern.

Verwendete Bauteile

Aufbau und Programmierung

Der grundsätzliche Aufbau und die Einrichtung, um das ESP32-CAM-Board programmieren zu können, wird in dem Beitrag ESP32-CAM - Video Streaming Server beschrieben.
Zusätzlich dazu sollte die verwendete SD-Karte mit dem PC als FAT32 formatiert werden. Größere SD-Karten als 4GB können angeblich auch verwendet werden, allerdings werden nur die ersten 4GB verwendet. Dies habe ich aber nicht getestet.

Speichern auf SD-Karte

Der folgende Sketch initialisiert zunächst die Kamera und speichert dann in loop() alle 5 Sekunden ein Foto auf der SD-Karte. Bei einem Reset des ESP32-CAM werden die zuvor gespeicherten Fotos überschrieben.

#include "esp_camera.h"
#include "FS.h"
#include "SD_MMC.h"

// Pin definition for 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

int imageNumber = 1;
char fileNumber[6];

void setup()
{
    Serial.begin(115200);

    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()) {
        config.frame_size = FRAMESIZE_UXGA;
        config.jpeg_quality = 10;
        config.fb_count = 2;
    } else {
        config.frame_size = FRAMESIZE_SVGA;
        config.jpeg_quality = 10;
        config.fb_count = 1;
    }

    Serial.print("Camera init...");
    esp_err_t err = esp_camera_init(&config);
    if (err != ESP_OK) {
        Serial.printf("error 0x%x", err);
        while (true) {}
    }
    Serial.println("ok");

    Serial.print("SD-Card init...");
    if (!SD_MMC.begin()) {
        Serial.println("failed!");
        return;
    }
    Serial.println("ok");

    uint8_t cardType = SD_MMC.cardType();
    if (cardType == CARD_MMC) {
        Serial.println("Typ: MMC");
    } else if (cardType == CARD_SD) {
        Serial.println("Typ: SDSC");
    } else if (cardType == CARD_SDHC) {
        Serial.println("Typ: SDHC");
    } else if (cardType == CARD_NONE) {
        Serial.println("No SD-Card attached!");
        return;
    } else {
        Serial.println("Typ: UNKNOWN");
        return;
    }

    uint64_t cardSize = SD_MMC.cardSize() / (1024 * 1024);
    Serial.printf("SD-Card Size: %lluMB\n", cardSize);
    Serial.printf("Total space: %lluMB\n", SD_MMC.totalBytes() / (1024 * 1024));
    Serial.printf("Used space: %lluMB\n", SD_MMC.usedBytes() / (1024 * 1024));
}

void loop()
{
    camera_fb_t * frameBuffer = NULL;

    // Take Picture with Camera
    frameBuffer = esp_camera_fb_get();
    if (!frameBuffer) {
        Serial.println("Camera capture failed");
        return;
    }

    sprintf(fileNumber, "%05d", imageNumber++);
    String imageFilename = "/image"+String(fileNumber)+".jpg";

    fs::FS &fs = SD_MMC;
    File imgFile = fs.open(imageFilename.c_str(), FILE_WRITE);
    if (!imgFile) {
        Serial.println("Failed to open file in writing mode");
    } else {
        imgFile.write(frameBuffer->buf, frameBuffer->len);
        Serial.println("Saved " + imageFilename);
        imageNumber++;
    }
    imgFile.close();

    esp_camera_fb_return(frameBuffer);
    delay(5000);
}
Gespeichertes Foto der ESP32-CAM auf SD-Karte
Abb.: Beispiel für ein gespeichertes Foto der ESP32-CAM auf SD-Karte (40% verkleinert)

Sichern/Hochladen auf einen WWW-Server

Die aufgenommenen Standbilder können auch anstatt lokal auf der SD-Karte gespeichert zu werden auf einen externen WWW-Server hochgeladen werden, wo auf sie sofort und von jedem Ort der Welt zugegriffen werden könnten. Zunächst wird ein Sketch benötigt, der die Bilder von der ESP32-CAM aufnimmt und an eine Gegenstelle im WWW verschickt (per HTTP Post), in den folgenden Fall passiert dies alle 10 Sekunden:

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

#define WIFI_SSID       "mySSID"
#define WIFI_PASSWORD   "myPassword"
#define SERVER_HOSTNAME "esp32cam.example.com"
#define SERVER_PATHNAME "/esp32cam-upload.php"
#define SERVER_PORT     80

// Pin definition for 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

WiFiClient client;

void setup()
{
  Serial.begin(115200);

  Serial.print("WiFi init...");
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(1000);
  }
  Serial.println("ok");

  Serial.print("Camera init...");
  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;

  // init with high specs to pre-allocate larger buffers
  if (psramFound()) {
    config.frame_size = FRAMESIZE_SVGA;
    config.jpeg_quality = 10;  //0-63 lower number means higher quality
    config.fb_count = 2;
  } else {
    config.frame_size = FRAMESIZE_CIF;
    config.jpeg_quality = 12;  //0-63 lower number means higher quality
    config.fb_count = 1;
  }

  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("error 0x%x", err);
    while (true) {}
  }
  Serial.println("ok");
}

void loop()
{
  uploadImage();
  delay(10000);
}

void uploadImage()
{
  String getAll;
  String getBody;

  Serial.print("Capturing image...");
  camera_fb_t * frameBuffer = NULL;

  // Take Picture with Camera
  frameBuffer = esp_camera_fb_get();
  if (!frameBuffer) {
    Serial.println("failed!");
    return;
  }
  Serial.println("ok");

  Serial.print("Connect server...");
  if (!client.connect(SERVER_HOSTNAME, SERVER_PORT)) {
    Serial.println("failed!");
    return;
  }
  Serial.println("ok");


  Serial.print("Uploading image...");
  String head = "--ESP32CAM\r\nContent-Disposition: form-data; name=\"imageFile\"; filename=\"image.jpg\"\r\nContent-Type: image/jpeg\r\n\r\n";
  String tail = "\r\n--ESP32CAM--\r\n";

  uint32_t totalLen = frameBuffer->len + head.length() + tail.length();

  client.println("POST " + String(SERVER_PATHNAME) + " HTTP/1.1");
  client.println("Host: " + String(SERVER_HOSTNAME));
  client.println("Content-Length: " + String(totalLen));
  client.println("Content-Type: multipart/form-data; boundary=ESP32CAM");
  client.println();
  client.print(head);

  uint8_t *fbBuf = frameBuffer->buf;
  size_t fbLen = frameBuffer->len;
  for (size_t n = 0; n < fbLen; n = n + 1024) {
    if (n + 1024 < fbLen) {
      client.write(fbBuf, 1024);
      fbBuf += 1024;
    } else if (fbLen % 1024 > 0) {
      size_t remainder = fbLen % 1024;
      client.write(fbBuf, remainder);
    }
  }
  client.print(tail);
  Serial.println("ok");

  esp_camera_fb_return(frameBuffer);

  int timoutTimer = 10000;
  long startTimer = millis();
  boolean state = false;

  while ((startTimer + timoutTimer) > millis()) {
    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;
    }
  }

  client.stop();
  Serial.println("Result:" + String(getBody));
}

Auf dem WWW-Server muss nun ein Programm vorhanden sein, welches die hochgeladenen Bilder in Empfang nimmt und abspeichert. Dies wird in diesem Falle mit einem PHP-Script gelöst:

<?php

define('IMAGES_PATH', 'esp32cam-uploads/');

$imageFiletype = strtolower(pathinfo($_FILES['imageFile']['name'], PATHINFO_EXTENSION));
$imageFilename = IMAGES_PATH . 'img-' . time() . '.' . $imageFiletype;

if (isset($_POST['submit'])) {
    $check = getimagesize($_FILES['imageFile']['tmp_name']);
    if ($check === false) {
        logAction('ERROR: Uploaded file is not an image!');
    }
}
if (file_exists($imageFilename)) {
    logAction('ERROR: Uploaded file already exists!');
}
if ($_FILES['imageFile']['size'] > 500000) {
    logAction('ERROR: Uploaded too large!');
}
if (!in_array($imageFiletype, ['jpg', 'jpeg', 'png'])) {
    logAction('ERROR: Only JPG, JPEG and PNG formats are allowed! Give');
}
if (move_uploaded_file($_FILES['imageFile']['tmp_name'], $imageFilename)) {
    logAction('SUCCESS: Uploaded and saves ' . basename($_FILES['imageFile']['name']) . ' » ' . $imageFilename);
} else {
    logAction('ERROR: Server error on uploading file!');
}

function logAction($message) {
    $message = date('c', time()) . ' - ' . $message . ' (IP: ' . $_SERVER['REMOTE_ADDR'] . ')' . PHP_EOL;
    error_log($message, 3, IMAGES_PATH . 'actions.log');
    exit($message);
}
zurück