ESP32Cam TimeLapse + NodeJS Server

Depois que o cara de Amarelo e Azul deixou o pacote com vários ESP32Cams aqui, veio a pressão interna em descobrir projetinhos para fazer essas camerazinhas funcionarem, um dos projetos que está destinado a elas aqui na minha “agenda de projetos” é fazer uma horta hidropônica e filmar ela, deixar uma câmera lá vendo tudo, mostrando cada centímetro de crescimento das hortaliças, pensando misso resolvi fazer um teste, daqueles bem “TESTE”, pendurei na janela com fita crepe fiz 1 sketch pro ESP mandar as imagens pra um servidorzinho em NodeJS pra chegarmos na maravilha abaixo

Primeiros testes de ESP32Cam

Ta bom, não estão assim tão maravilhoso, mas para um primeiro TimeLapse está “Ótimo” – Obrigado 😉

A ideia é que essa câmera suba no telhado, de modo consiga mirar ela para um horizonte um pouco menos poluído, porque tem muito fio logo de frente com minha casa, então depois de achar um lugar legal e conseguir deixar a câmera mais fixa, acredito que teremos alguns timelapses mais “glamorosos”, ao menos eu vou tentar! 😉

Porém a ideia aqui é registrar os aprendizados tidos até agora, facilitando o caminho pra quem quiser usar sua ESP32Cam pra se aventurar nos timelapses, tenho alguns links que compartilharei no final do post que me ajudou e com certeza poderá fazer diferença no seu setup também, ficarei mais focado nos dois pedaços do meio, a parte de tirar fotos com o ESP e como fazer elas chegarem no nosso servidor, porque a parte artistica sei que cada um tem a sua, e a minha como viram no vídeo é um tanto quanto limitada 😀 (mentira, são os fios que deixaram meu timelapse menos Monito!).

ESP32Cam

Parecia que seria fácil, porém nem tudo são flores, tirar a foto foi de fato bem tranquilo, usei o próprio sketch do exemplo, prestando atenção nas portas, no meu caso o meu HW é compatível com o AI Tinker, então depois de verificar se estava tirando as fotos fui ver como enviar essa foto para um servidor, e aqui começaram algumas coisas não tão usuais, vi alguns exemplos na internet, eles funcionavam, porém eles não mandavam nosso post no formato de formulário com uma imagem “embutida”, isso fez perder um tempo até descobrir um código que resolvesse isso. Nessa hora entrou os tutoriais da Randon Nerd Tutorials, que apresentou um sketch aonde contemplava essa forma de envio, abaixo o código completo

/*
  Rui Santos
  Complete project details at https://RandomNerdTutorials.com/esp32-cam-post-image-photo-server/

  Permission is hereby granted, free of charge, to any person obtaining a copy
  of this software and associated documentation files.

  The above copyright notice and this permission notice shall be included in all
  copies or substantial portions of the Software.
*/

#include <Arduino.h>
#include <WiFi.h>
#include "soc/soc.h"
#include "soc/rtc_cntl_reg.h"
#include "esp_camera.h"
#include <NTPClient.h>

// Wifi
const char* ssid     = "kaduzius";
const char* password = "umasenhabemlegal27";

// NTP
WiFiUDP udp;
NTPClient ntp(udp, "a.st1.ntp.br", -3 * 3600, 60000);
String hora;

// Upload server
String serverName    = "IP_OR_SERVER_NAME";   // REPLACE WITH YOUR Raspberry Pi IP ADDRESS
String serverPath    = "/upload";     // The default serverPath should be upload.php
const int serverPort = 3000;          // Server port

// photo config / timelapse
const int timeIntervalInMinutes = 5;
const int timerInterval         = timeIntervalInMinutes*60000; // time between each HTTP POST image
unsigned long previousMillis    = 0;                           // last time image was sent
framesize_t picSize             = FRAMESIZE_SVGA;              // Image size

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

String sendPhoto() {
  String getAll;
  String getBody;

  camera_fb_t * fb = NULL;
  fb = esp_camera_fb_get();
  if(!fb) {
    Serial.println("Camera capture failed");
    delay(1000);
    ESP.restart();
  }

  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=\"profile_pic\"; filename=\"esp32-cam.jpg\"\r\nContent-Type: image/jpeg\r\n\r\n";
    String tail = "\r\n--RandomNerdTutorials--\r\n";

    uint32_t imageLen = fb->len;
    uint32_t extraLen = head.length() + tail.length();
    uint32_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);

    uint8_t *fbBuf = fb->buf;
    size_t fbLen = fb->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);

    esp_camera_fb_return(fb);

    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; }
    }
    Serial.println();
    client.stop();
    Serial.println(getBody);
  }
  else {
    getBody = "Connection to " + serverName +  " failed.";
    Serial.println(getBody);
  }
  return getBody;
}

void setup() {
  WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);
  Serial.begin(115200);
  pinMode(4, OUTPUT);
  digitalWrite(4, HIGH);

  WiFi.mode(WIFI_STA);
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(500);
  }
  digitalWrite(4, LOW);
  Serial.println();
  Serial.print("ESP32-CAM IP Address: ");
  Serial.println(WiFi.localIP());

  ntp.begin();
  ntp.forceUpdate();

  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 = picSize;  // RESOLUTION
    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;
  }

  // camera init
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera init failed with error 0x%x", err);
    delay(1000);
    ESP.restart();
  }

  sendPhoto();
}

void loop() {
  unsigned long currentMillis = millis();
  if (currentMillis - previousMillis >= timerInterval) {
    sendPhoto();
    previousMillis = currentMillis;
    hora = ntp.getFormattedTime();
    ntp.getDay();
  }
}

Agora podemos conversar sobre código e já vou destacar algumas informações aqui, na linha 20 e 21 temos as informações da conexão wifi, logo abaixo tem a configuração do NTP, que é pra ter o horário correto no nosso sistema, apesar de ainda não estar usando, se num futuro for por alguma metadata ou alguma outra informação, fica bem fácil. Depois nas linhas 28 a 31 fazemos o setup do nosso servidor, que é pra onde irá nossas fotos.
Abaixo partindo da linha 33 até a 37, fazemos a configuração do intervalo de fotos e do tamanho da imagem, eu estou usando FRAMESIZE_SVGA qué é 800×600, aqui vai uma listinha das constantes e valores:

FRAMESIZE_QQVGA,    // 160x120
FRAMESIZE_QQVGA2,   // 128x160
FRAMESIZE_QCIF,     // 176x144
FRAMESIZE_HQVGA,    // 240x176
FRAMESIZE_QVGA,     // 320x240
FRAMESIZE_CIF,      // 400x296
FRAMESIZE_VGA,      // 640x480
FRAMESIZE_SVGA,     // 800x600
FRAMESIZE_XGA,      // 1024x768
FRAMESIZE_SXGA,     // 1280x1024
FRAMESIZE_UXGA,     // 1600x1200
FRAMESIZE_QXGA,     // 2048*1536

Como você pode ver, resolução não é o problema, temos uma gama grande, vale observar que nem todos os ESP32Cams vão chegar em 2048×1536.

Padawan Perguntando

“Como assim Kadu, estamos na linha 37 e nosso código tem 210, aonde que acabou?”

Poxa, temos mais um código pra comentar aqui, se formos linha a linha, você para de ler aqui! não é!? 😛 – Agora pra você não ficar triste, varei mais algumas observações

String head = "--RandomNerdTutorials\r\nContent-Disposition: form-data; name=\"profile_pic\"; filename=\"esp32-cam.jpg\"\r\nContent-Type: image/jpeg\r\n\r\n";

Aqui temos a linha 75, destaco o “profile_pic” que é o nome do campo da foto no formulário, imaginando que estamos simulando o envio de um formulário com uma imagem anexa, já o “esp32-cam.jpg” é o nome do arquivo, eventualmente você pode querer mudar esses caras, de repente enviar a data no nome do arquivo, enfim, acho sábio termos essa informação na mente.

EU estava esquecendo de uma rotina mágica, e preciso comentar aqui antes de partimos pro nodeJS, se você olhar no ESP32Cam tem um Led de Flash, super forte, e eu resolvi usar ele como feedback visual para nossa conexão com a Internet, por algum motivo as vezes no primeiro boot o ESP32 não pega IP, fica lá tentando eternamente, tem várias formas de implementar um workaround, porém resolvi colocar esse led para ascender, e logo que ele pega IP, o led apaga, assim fica fácil de você sem nada por perto ver se o led está acesso, então não estranhe caso isso aconteça, só reiniciar o ESP (pode ser tomada off mesmo) 😉

Acho que agora podemos ir para o lado JS da força pequeno Padawan?

Recebendo as fotos com NodeJS

Aqui realmente podemos falar que nosso trabalho foi bastante encurtado pelas maravilhas do NodeJS, que já tras uma série de funcionalidades prontas, bastando dar aquela “juntada” maneira, estruturar algumas linhas e pronto, tudo está funcionando!

Nesse projeto usei:

  • express – Servidor Web [link]
  • express-fileupload – Um middleware que ajuda no fileupload [link]
  • e só – Me pareceu 2 pacotes simples de mais, resolvi adicionar esse pacotezinho dumb só de fuleragem 😛

O projeto conta com uma rota chamada “/upload”, nela acontece tudo, recebe o arquivo, renomeia para um nome baseado na data, evitando conflitos de nomes, copia para o diretório uploads e avisa que deu tudo certo

E na ultima linha tem o app.listen, que de fato inicia nosso servidor pra que tudo de certo. Vejamos o código

const express = require('express');
const fileUpload = require('express-fileupload');
const app = express();

const port = process.env.PORT || 3000;

function addZero(number){
  if (number <= 9)
      return "0" + number;
  else
      return number;
}

// default options
app.use(fileUpload({
    useTempFiles : true,
    tempFileDir : '/tmp/',
    debug: false
}));

app.post('/upload', function(req, res) {
  let sampleFile;
  let uploadPath;

  if (!req.files || Object.keys(req.files).length === 0) {
    return res.status(400).send('No files were uploaded.');
  }

  let date = new Date();
  let formatatedDate = (addZero(date.getDate() )) + "-" + (addZero(date.getMonth() + 1)) + "-" + date.getFullYear() + "_" + addZero(date.getHours()) + addZero(date.getMinutes()) + addZero(date.getSeconds());
  console.log(formatatedDate);

  // The name of the input field (i.e. "sampleFile") is used to retrieve the uploaded file
  sampleFile = req.files.profile_pic;
  uploadPath = __dirname + '\\uploads\\' + formatatedDate+".jpg";

  // Use the mv() method to place the file somewhere on your server
  sampleFile.mv(uploadPath, function(err) {
    if (err)
      return res.status(500).send(err);

    res.send('File uploaded!');
  });
});

“Simpre, né irmão!?”

Com tudo isso pronto, operando, temos as fotos, agora falta o “TimmmeeeeeeLaaaapppssseeeeeeee”

Transformando fotos em timelapse vídeos

Nesse ponto, pra quem já brincou um pouco com o FFMPEG sabe que ele será nosso Gandalf, fumará as fotos e produzira a magia do vídeo de fotos aceleradas 🙂

O comando que usei para ter a saída do vídeo acima, foi o seguinte, na pasta uploads, aonde estão todas as fotos:

$ ffmpeg -framerate 30 -pattern_type glob -i "./*.jpg" -s:v 800x600 -c:v libx264 -crf 17 -pix_fmt yuv420p my-timelapse.mp4

Nesse comando tempos algumas observações a fazer, a primeira é o -framerate, que é a quantidade de quadros que o vídeo terá por segundo, número acima de 24 fará vídeos menos travados, já o -pattern_type é pra que ele pegue todos jpg do diretório corrente, depois temos a resolução, que está igual a que eu usei nas fotos, alguns outros parametros e no final o nome do arquivo my-timelapse.mp4

Sei que tem ffmpeg pra windows, porém eu instalei o Ubuntu no meu Windows (Valeu por isso Microsoft) e então a coisa foi bem mais tranquila, já que as vezes pra esses programas funcionarem no windows puro, da trabalho.

Aqui está “quase” tudo que aprendi fazendo o meu primeiro Timelapse com o ESP32Cam + NodeJS + Express + Express-uploadfile, e gostaria de deixar compartilhado e registrado aqui, porém como disse lá no começo, aqui uma série de links que tem ainda mais informação, incluindo até como fazer o vídeo ficar ainda mais “empolgante”

+Links

  • Random Nerd Tutorials – Já apresentado no artigo, daqui veio o código do ESP que envia a imagem da maneira que precisamos
  • Express-Fileupload – Todo nosso servidor nodeJS se resume ao uso do Express com esse middleware, logo o README do github foi nosso guia, e aqui temos mais informações, podendo ainda dar uma turbinada no nosso server
  • Handling file uploads in nodejs with express and multer – Esse foi nosso primeiro código, tentamos fazer ele funcinar na live, porém como o código do nosso ESP32 não estava legal, acabou que não usamos, porém é um excelente tutorial, vale a pena a lida!
  • Creating a Time-Lapse Video Through the Command-Line (Using FFmpeg) – Talvez esse seja o link mais importante, e é exatamente por isso que ele é o último, se não você iria perder todas as piadas que eu fiz, e eu não iria gostar :P. Nesse tutorial tem as informações do FFMPEG, com as dicas do que cada parâmetro faz, como deixar o vídeo mais rápido ou mais devagar, e ainda como aplicar alguns efeitos de crop e pan, pra dar aquela impressão que a câmera está se mexendo, quando conseguir pendurar minha câmera no telhado, vou aplicar essas técnicas pra ver se de repente meu vídeo fica um pouco menos feio – torçam por mim!

Chegou aqui, deixa um comentário ai 😉

4 comentários em “ESP32Cam TimeLapse + NodeJS Server”

  1. tudo bem Kadu ?
    crio abelhas nativas e tenho vontade de filma-las em time lapse.
    procurei e achei seu projeto, vou acompanha-lo com carinho e tomara que me sirva com poucos ajustes (devido meu desconhecimento de linguagens de programação)
    mas ja de cara lhe agradeço.
    parabens pelo seu projeto.

    Responder
      • agradeço seu retorno.
        meu projeto em passos lentos, mas vou implementa-lo
        tão logo o faça, te retorno.
        mas suas dicas e seu codigo, me ajudarão muito.
        agradeço imensamente
        desejo-lhe otimos e novos projetos e que possamos acompanhar.
        forte abraço.

        Responder

Deixe um comentário