Binocular camera using ESP32CAM, sending images through ethernet

I have 2 ESP32CAMs that I wish to use as a binocular camera for short-range depth mapping development.
Ethernet with basic PoE would be much nicer than Wi-Fi, since running just one cable would be absolutely awesome, so that's what I want to go with in extracting high-res images from the cameras.

Bandwidth is not a problem, I just want to take one image on both cameras at the same time and then send them to a server/NAS on a local network, which can take half an hour for all I care.

However I am a little bit out of ideas on how to send the images from both ESP32CAMs through one ethernet module (W5100 or W5500, haven't decided yet, but the W5100 could be better).

Do I connect both ESPs to the module and somehow send both images sequentially? (I have no idea if that is even possible)
Or do I instead tell one of the ESPs to send the newly acquired image to the other ESP and that one sends both images through Ethernet?

How would I go about doing this?

Thank you for any and all suggestions!

I use that dang WiFi thing and have the ESP32-CAM's just ftp the image to the server.

#include "sdkconfig.h" // used for log printing
#include "esp_system.h"
#include "freertos/FreeRTOS.h" //freeRTOS items to be used
#include "freertos/task.h"
#include "certs.h"
#include "esp_camera.h"
#include "soc/soc.h"           // Disable brownout problems
#include "soc/rtc_cntl_reg.h"  // Disable brownout problems
#include "driver/rtc_io.h"
#include <WiFi.h>
#include <WiFiClient.h>
#include "ESP32_FTPClient.h"
#include <PubSubClient.h>
#include <ESP32Time.h>
//
WiFiClient      wifiClient; // do the WiFi instantiation thing
PubSubClient    MQTTclient( mqtt_server, mqtt_port, wifiClient ); //do the MQTT instantiation thing
ESP32_FTPClient ftp (ftp_server, ftp_user, ftp_pass, 5000, 2);
ESP32Time       rtc;
////
//#define evtDoMQTTParse  ( 1 << 0 ) // declare an event
//EventGroupHandle_t eg; // variable for the event group handle
////
SemaphoreHandle_t sema_MQTT_KeepAlive;
SemaphoreHandle_t sema_mqttOK;
////
QueueHandle_t xQ_Message; // payload and topic queue of MQTT payload and topic
const int payloadSize = 300;
struct stu_message
{
  char payload [payloadSize] = {'\0'};
  String topic ;
} x_message;
////
int  mqttOK = 0;
bool TimeSet = false;
bool FlashMode = false;
//
void IRAM_ATTR WiFiEvent(WiFiEvent_t event)
{
  switch (event) {
    case SYSTEM_EVENT_STA_CONNECTED:
      log_i("Connected to WiFi access point");
      break;
    case SYSTEM_EVENT_STA_DISCONNECTED:
      log_i("Disconnected from WiFi access point");
      break;
    case SYSTEM_EVENT_AP_STADISCONNECTED:
      log_i("WiFi client disconnected");
      break;
    default: break;
  }
} // void IRAM_ATTR WiFiEvent(WiFiEvent_t event)
//
void IRAM_ATTR mqttCallback(char* topic, byte * payload, unsigned int length)
{
  // clear locations
  memset( x_message.payload, '\0', payloadSize );
  x_message.topic = ""; //clear string buffer
  x_message.topic = topic;
  int i = 0;
  for ( i; i < length; i++)
  {
    x_message.payload[i] = ((char)payload[i]);
  }
  x_message.payload[i] = '\0';
  xQueueOverwrite( xQ_Message, (void *) &x_message );// send data to queue
} // void mqttCallback(char* topic, byte* payload, unsigned int length)
////
void setup()
{
  pinMode( GPIO_NUM_4, OUTPUT);
  digitalWrite( GPIO_NUM_4, LOW);
  //
  WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); //disable brownout detector
  //
  //eg = xEventGroupCreate(); // get an event group handle
  //
  x_message.topic.reserve( payloadSize );
  //
  xQ_Message  = xQueueCreate( 1, sizeof(stu_message) );
  //
  sema_mqttOK    =  xSemaphoreCreateBinary();
  xSemaphoreGive( sema_mqttOK );
  //
  xTaskCreatePinnedToCore( MQTTkeepalive, "MQTTkeepalive", 7000, NULL, 3, NULL, 1 );
  xTaskCreatePinnedToCore( fparseMQTT, "fparseMQTT", 9000, NULL, 4, NULL, 1 ); // assign all to core 1, WiFi in use.
  xTaskCreatePinnedToCore( fmqttWatchDog, "fmqttWatchDog", 3000, NULL, 2, NULL, 1 );
  if ( configInitCamera() )
  {
    log_i("        start camera task" );
    xTaskCreatePinnedToCore( capturePhoto_sendFTP, "capturePhoto_sendFTP", 60000, NULL, 6, NULL, 1 );
  } else {
    log_i( "       camera failed to initilize, rebooting in 5." );
    vTaskDelay( 2000 );
    ESP.restart();
  }
} // void setup()
////
void capturePhoto_sendFTP( void *pvParameters )
{
  TickType_t xLastWakeTime    = xTaskGetTickCount();
  const TickType_t xFrequency = 1000 * 15; //delay for mS
  //!!!!!Must wait for MQTT connection and proper setup of the sema_MQTT_KeepAlive!!!!
  while ( !MQTTclient.connected() )
  {
    vTaskDelay( 500 );
  }
  for (;;)
  {
    log_i( "tick");
    xSemaphoreTake( sema_MQTT_KeepAlive, portMAX_DELAY );
    if ( (wifiClient.connected()) && (WiFi.status() == WL_CONNECTED) )
    {
      camera_fb_t * fb = NULL; // pointer
      if( FlashMode )
      {
        digitalWrite( GPIO_NUM_4, HIGH );
        vTaskDelay( 20 );
      }
      fb = esp_camera_fb_get();
      digitalWrite( GPIO_NUM_4, LOW );
      if (!fb)
      {
        log_i( "Camera capture failed" );
        esp_camera_fb_return(fb);
      } else
      {
        ftp.OpenConnection(); // try open FTP
        if ( ftp.isConnected() )
        {
          //try send file ftp
          ftp.ChangeWorkDir( ftp_path );
          ftp.DeleteFile( ftp_file_name );
          ftp.InitFile( ftp_file_type ); //"Type I"
          ftp.NewFile( ftp_file_name );
          ftp.WriteData( (unsigned char *)fb->buf, fb->len );
          ftp.CloseFile();
          ftp.CloseConnection();
        }
        esp_camera_fb_return(fb); //return the frame buffer back to the driver for reuse
      }
    }
    xSemaphoreGive( sema_MQTT_KeepAlive );
    xLastWakeTime = xTaskGetTickCount();
    vTaskDelayUntil( &xLastWakeTime, xFrequency );
  }
  vTaskDelete( NULL );
} //void capturePhoto_sendFTP( void *pvParameters )
////
void configureCameraSettings_grpE( int _lenC, int _Hmirror, int _Vflip, int _dcw, int _colorbar )
{
  sensor_t * s = esp_camera_sensor_get();
  s->set_lenc(s, _lenC);         // 0 = disable , 1 = enable
  s->set_hmirror(s, _Hmirror);   // 0 = disable , 1 = enable
  s->set_vflip(s, _Vflip);       // 0 = disable , 1 = enable
  s->set_dcw(s, _dcw);           // 0 = disable , 1 = enable
  ////s->set_colorbar(s, _colorbar); // 0 = disable , 1 = enable
}
////
void configureCameraSettings_grpD( int _gc, int _agc, int _celing, int _bpc, int _wpc, int _gma )
{
  sensor_t * s = esp_camera_sensor_get();
  s->set_gain_ctrl(s, _gc); // 0 = disable , 1 = enable
  s->set_agc_gain(s, _agc); // 0 to 30
  s->set_gainceiling(s, (gainceiling_t)_celing);  // 0 to 6
  s->set_bpc(s, _bpc);      // 0 = disable , 1 = enable
  s->set_wpc(s, _wpc);      // 0 = disable , 1 = enable
  s->set_raw_gma(s, _gma);  // 0 = disable , 1 = enable
}
////
void configureCameraSettings_grpC( int _exctl, int _aec2, int _ae, int _aec )
{
  sensor_t * s = esp_camera_sensor_get();
  s->set_exposure_ctrl(s, _exctl); // 0 = disable , 1 = enable
  s->set_aec2(s, _aec2);           // 0 = disable , 1 = enable
  s->set_ae_level(s, _ae);         // -2 to 2
  ////s->set_aec_value(s, _aec);       // 0 to 1200 CAUSES BLACK IMAGE????
}
////
void configureCameraSettings_grpB( int se, int wb, int awb, int wbmode )
{
  sensor_t * s = esp_camera_sensor_get();
  s->set_special_effect(s, se); // 0 to 6 (0 - No Effect, 1 - Negative, 2 - Grayscale, 3 - Red Tint, 4 - Green Tint, 5 - Blue Tint, 6 - Sepia)
  s->set_whitebal(s, wb);       // 0 = disable , 1 = enable
  s->set_awb_gain(s, awb);      // 0 = disable , 1 = enable
  s->set_wb_mode(s, wbmode);    // 0 to 4 - if awb_gain enabled (0 - Auto, 1 - Sunny, 2 - Cloudy, 3 - Office, 4 - Home)
}
////
void configureCameraSettings_grpA(int brightness, int contrast, int saturation )
{
  if ( ((brightness > -3) && (brightness < 3)) && ((contrast > -3) && (contrast < 3)) && ((saturation > -3) && (saturation < 3)) )
  {
    sensor_t * s = esp_camera_sensor_get();
    s->set_brightness( s, brightness ); // -2 to 2
    s->set_contrast( s, contrast );    // -2 to 2
    s->set_saturation( s, saturation );  // -2 to 2
  }
} //void configureCameraSettings()
////
void configureCameraSettings()
{
  sensor_t * s = esp_camera_sensor_get(); //see certs.h for more info
  s->set_brightness(s, -1);     // -2 to 2 **************************
  s->set_contrast(s, 0);       // -2 to 2
  s->set_saturation(s, 0);     // -2 to 2
  s->set_special_effect(s, 0); // 0 to 6 (0 - No Effect, 1 - Negative, 2 - Grayscale, 3 - Red Tint, 4 - Green Tint, 5 - Blue Tint, 6 - Sepia)
  s->set_whitebal(s, 1);       // 0 = disable , 1 = enable
  s->set_awb_gain(s, 1);       // 0 = disable , 1 = enable
  s->set_wb_mode(s, 0);        // 0 to 4 - if awb_gain enabled (0 - Auto, 1 - Sunny, 2 - Cloudy, 3 - Office, 4 - Home)
  s->set_exposure_ctrl(s, 1);  // 0 = disable , 1 = enable
  s->set_aec2(s, 0);           // 0 = disable , 1 = enable
  s->set_ae_level(s, 0);       // -2 to 2
  s->set_aec_value(s, 300);    // 0 to 1200
  s->set_gain_ctrl(s, 1);      // 0 = disable , 1 = enable
  s->set_agc_gain(s, 0);       // 0 to 30
  s->set_gainceiling(s, (gainceiling_t)0);  // 0 to 6
  s->set_bpc(s, 0);            // 0 = disable , 1 = enable
  s->set_wpc(s, 1);            // 0 = disable , 1 = enable
  s->set_raw_gma(s, 1);        // 0 = disable , 1 = enable
  s->set_lenc(s, 1);           // 0 = disable , 1 = enable
  s->set_hmirror(s, 0);        // 0 = disable , 1 = enable
  s->set_vflip(s, 0);          // 0 = disable , 1 = enable
  s->set_dcw(s, 1);            // 0 = disable , 1 = enable
  s->set_colorbar(s, 0);       // 0 = disable , 1 = enable
} //void configureCameraSettings()
////
bool configInitCamera()
{
  camera_config_t config = {}; // Stores the camera configuration parameters
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer   = LEDC_TIMER_0;
  config.pin_d0       = GPIO_NUM_5; //Y2
  config.pin_d1       = GPIO_NUM_18; //Y3
  config.pin_d2       = GPIO_NUM_19; //Y4
  config.pin_d3       = GPIO_NUM_21; //Y5
  config.pin_d4       = GPIO_NUM_36; //Y6
  config.pin_d5       = GPIO_NUM_39; //Y7
  config.pin_d6       = GPIO_NUM_34; //Y8
  config.pin_d7       = GPIO_NUM_35; // Y9
  config.pin_xclk     = GPIO_NUM_0; //XCLK
  config.pin_pclk     = GPIO_NUM_22; //PCLK
  config.pin_vsync    = GPIO_NUM_25; //VSSYNC
  config.pin_href     = GPIO_NUM_23; // HREF
  config.pin_sscb_sda = GPIO_NUM_26; //SIOD
  config.pin_sscb_scl = GPIO_NUM_27; //SIOC
  config.pin_pwdn     = GPIO_NUM_32; //PWDN
  config.pin_reset    = -1; //RESET
  config.xclk_freq_hz = 20000000;
  config.pixel_format = PIXFORMAT_JPEG; //assuming default is has PSRAM
  config.frame_size = FRAMESIZE_UXGA; // FRAMESIZE_ + QVGA|CIF|VGA|SVGA|XGA|SXGA|UXGA
  config.jpeg_quality = 10; //0-63 lower number means higher quality
  config.fb_count = 2;
  // Initialize the Camera
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    log_i("Camera init failed with error %d", err );
    return false;
  } else {
    configureCameraSettings();
    return true;
  }
} //void configInitCamera()
////
void connectToWiFi()
{
  int TryCount = 0;
  while ( WiFi.status() != WL_CONNECTED )
  {
    TryCount++;
    WiFi.disconnect();
    WiFi.begin( SSID, PASSWORD );
    vTaskDelay( 2500 );
    if ( TryCount == 10 )
    {
      ESP.restart();
    }
  }
  WiFi.onEvent( WiFiEvent );
}
////
void MQTTkeepalive( void *pvParameters )
{
  sema_MQTT_KeepAlive   = xSemaphoreCreateBinary();
  xSemaphoreGive( sema_MQTT_KeepAlive ); // found keep alive can mess with a publish, stop keep alive during publish
  MQTTclient.setKeepAlive( 90 ); // setting keep alive to 90 seconds makes for a very reliable connection, must be set before the 1st connection is made.
  TickType_t xLastWakeTime = xTaskGetTickCount();
  const TickType_t xFrequency = 250; //delay for ms
  for (;;)
  {
    //check for a is-connected and if the WiFi 'thinks' its connected, found checking on both is more realible than just a single check
    if ( (wifiClient.connected()) && (WiFi.status() == WL_CONNECTED) )
    {
      xSemaphoreTake( sema_MQTT_KeepAlive, portMAX_DELAY ); // !!!!!whiles MQTTlient.loop() is running no other mqtt operations should be in process!!!!!
      MQTTclient.loop();
      xSemaphoreGive( sema_MQTT_KeepAlive );
    }
    else {
      if ( !(wifiClient.connected()) || !(WiFi.status() == WL_CONNECTED) )
      {
        connectToWiFi();
      }
      connectToMQTT();
    }
    xLastWakeTime = xTaskGetTickCount();
    vTaskDelayUntil( &xLastWakeTime, xFrequency );
  }
  vTaskDelete ( NULL );
}
////
void connectToMQTT()
{
  byte mac[5]; // create client ID from mac address
  WiFi.macAddress(mac); // get mac address
  String clientID = String(mac[0]) + String(mac[4]) ; // use mac address to create clientID
  while ( !MQTTclient.connected() )
  {
    MQTTclient.connect( clientID.c_str(), mqtt_username, mqtt_password );
    vTaskDelay( 250 );
  }
  MQTTclient.setCallback( mqttCallback );
  MQTTclient.subscribe( topicOK );
  MQTTclient.subscribe( topicGrpA );
  MQTTclient.subscribe( topicGrpB );
  MQTTclient.subscribe( topicGrpC );
  MQTTclient.subscribe( topicGrpD );
  MQTTclient.subscribe( topicGrpE );
  MQTTclient.subscribe( topicFlash );
} //void connectToMQTT()
//////
void fmqttWatchDog( void * paramater )
{
  int UpdateImeTrigger = 86400; //seconds in a day
  int UpdateTimeInterval = 85000; // get another reading when = UpdateTimeTrigger
  int maxNonMQTTresponse = 3;
  TickType_t xLastWakeTime = xTaskGetTickCount();
  const TickType_t xFrequency = 60000; //delay for mS
  for (;;)
  {
    xLastWakeTime = xTaskGetTickCount();
    vTaskDelayUntil( &xLastWakeTime, xFrequency );
    xSemaphoreTake( sema_mqttOK, portMAX_DELAY ); // update mqttOK
    mqttOK++;
    xSemaphoreGive( sema_mqttOK );
    if ( mqttOK >= maxNonMQTTresponse )
    {
      log_i( "mqtt watchdog rest" );
      vTaskDelay( 200 );
      ESP.restart();
    }
    UpdateTimeInterval++; // trigger new time get
    if ( UpdateTimeInterval >= UpdateImeTrigger )
    {
      TimeSet = false; // sets doneTime to false to get an updated time after a days count of seconds
      UpdateTimeInterval = 0;
    }
  }
  vTaskDelete( NULL );
} //void fmqttWatchDog( void * paramater )
////
void fparseMQTT( void *pvParameters )
{
  struct stu_message px_message;
  for (;;)
  {
    if ( xQueueReceive(xQ_Message, &px_message, portMAX_DELAY) == pdTRUE )
    {
      xSemaphoreTake( sema_mqttOK, portMAX_DELAY );
      mqttOK = 0;
      xSemaphoreGive( sema_mqttOK );
      if ( !TimeSet)
      {
        if ( String(px_message.topic) == topicOK )
        {
          String temp = "";
          temp        = px_message.payload[0];
          temp        += px_message.payload[1];
          temp        += px_message.payload[2];
          temp        += px_message.payload[3];
          int year    =  temp.toInt();
          temp        = "";
          temp        = px_message.payload[5];
          temp        += px_message.payload[6];
          int month   =  temp.toInt();
          temp        = "";
          temp        = px_message.payload[8];
          temp        += px_message.payload[9];
          int day     =  temp.toInt();
          temp        = "";
          temp        = px_message.payload[11];
          temp        += px_message.payload[12];
          int hour    =  temp.toInt();
          temp        = "";
          temp        = px_message.payload[14];
          temp        += px_message.payload[15];
          int min     =  temp.toInt();
          rtc.setTime( 0, min, hour, day, month, year );
          log_i( "%s   rtc  %s ", px_message.payload, rtc.getTime() );
          TimeSet     = true;
        }
      }
      if ( String(px_message.topic) == topicGrpA )
      {
        //finding first letter
        String sTmp = String( px_message.payload );
        int commaIndex = sTmp.indexOf(',');
        if ( sTmp.substring(0, commaIndex) == "a" );
        {
          int commaIndex = sTmp.indexOf(',');
          int brt =  (sTmp.substring(0, commaIndex)).toInt();
          sTmp.remove( 0, (commaIndex + 1) );
          int ctrast = (sTmp.substring(0, commaIndex)).toInt();
          sTmp.remove( 0, (commaIndex + 1) ); // chop off begining of message
          int sat = (sTmp.substring(0, commaIndex)).toInt();
          configureCameraSettings_grpA( brt, ctrast, sat );
        }
      }
      if ( String(px_message.topic) == topicGrpB )
      {
        String sTmp = String( px_message.payload );
        int commaIndex = sTmp.indexOf(',');
        if ( sTmp.substring(0, commaIndex) == "b" );
        {
          int commaIndex = sTmp.indexOf(',');
          int se =  (sTmp.substring(0, commaIndex)).toInt();
          sTmp.remove( 0, (commaIndex + 1) );
          int wbb = (sTmp.substring(0, commaIndex)).toInt();
          sTmp.remove( 0, (commaIndex + 1) );
          int wba = (sTmp.substring(0, commaIndex)).toInt();
          sTmp.remove( 0, (commaIndex + 1) );
          int wbm = (sTmp.substring(0, commaIndex)).toInt();
          configureCameraSettings_grpB( se, wbb, wba, wbm );
        }
      }
      if ( String(px_message.topic) == topicGrpC )
      {
        String sTmp = String( px_message.payload );
        int commaIndex = sTmp.indexOf(',');
        if ( sTmp.substring(0, commaIndex) == "c" );
        {
          int commaIndex = sTmp.indexOf(',');
          int ex =  (sTmp.substring(0, commaIndex)).toInt();
          sTmp.remove( 0, (commaIndex + 1) );
          int aec2 = (sTmp.substring(0, commaIndex)).toInt();
          sTmp.remove( 0, (commaIndex + 1) );
          int ae = (sTmp.substring(0, commaIndex)).toInt();
          sTmp.remove( 0, (commaIndex + 1) );
          int aec = (sTmp.substring(0, commaIndex)).toInt();
          configureCameraSettings_grpC( ex, aec2, ae, aec );
        }
      }
      if ( String(px_message.topic) == topicGrpD )
      {
        String sTmp = String( px_message.payload );
        int commaIndex = sTmp.indexOf(',');
        if ( sTmp.substring(0, commaIndex) == "d" );
        {
          int commaIndex = sTmp.indexOf(',');
          int gc =  (sTmp.substring(0, commaIndex)).toInt();
          sTmp.remove( 0, (commaIndex + 1) );
          int agc = (sTmp.substring(0, commaIndex)).toInt();
          sTmp.remove( 0, (commaIndex + 1) );
          int celing = (sTmp.substring(0, commaIndex)).toInt();
          sTmp.remove( 0, (commaIndex + 1) );
          int bpc = (sTmp.substring(0, commaIndex)).toInt();
          sTmp.remove( 0, (commaIndex + 1) );
          int wpc = (sTmp.substring(0, commaIndex)).toInt();
          sTmp.remove( 0, (commaIndex + 1) );
          int gma = (sTmp.substring(0, commaIndex)).toInt();
          configureCameraSettings_grpD( gc, agc, celing, bpc, wpc, gma );
        }
      }
      if ( String(px_message.topic) == topicGrpE )
      {
        String sTmp = String( px_message.payload );
        int commaIndex = sTmp.indexOf(',');
        if ( sTmp.substring(0, commaIndex) == "e" );
        {
          int commaIndex = sTmp.indexOf(',');
          int lenC =  (sTmp.substring(0, commaIndex)).toInt();
          sTmp.remove( 0, (commaIndex + 1) );
          int Hmirror = (sTmp.substring(0, commaIndex)).toInt();
          sTmp.remove( 0, (commaIndex + 1) );
          int Vflip = (sTmp.substring(0, commaIndex)).toInt();
          sTmp.remove( 0, (commaIndex + 1) );
          int dcw = (sTmp.substring(0, commaIndex)).toInt();
          sTmp.remove( 0, (commaIndex + 1) );
          int colorbar = (sTmp.substring(0, commaIndex)).toInt();
          configureCameraSettings_grpE( lenC, Hmirror, Vflip, dcw, colorbar );
        }
      }
      if( String(px_message.topic) == topicFlash )
      {
        FlashMode = !FlashMode;
      }
    } //if ( xQueueReceive(xQ_Message, &px_message, portMAX_DELAY) == pdTRUE )
  } //for(;;)
  vTaskDelete( NULL );
} // void fparseMQTT( void *pvParameters )
////
void loop() {}
1 Like

Alright, FTP sounds good. Thank you!
But I am still waiting on an answer about the Ethernet module and how to send images from both cameras through a single ethernet module.

If anyone has any ideas, feel free to share!

EDIT: I found something I could use: https://forum.arduino.cc/t/transfer-pictures-from-esp32-cam-to-esp32-via-serial/647488/58
So I could probably just send the image taken by one ESP32 to the other one and that one will send both using FTP.

Is this a viable approach?

That should work, but keep us updated. This is a very interesting idea!

Presumably you will use software on a PC to do the depth mapping. Can you tell us what that is?

How will you deal with camera misalignments and lens distortion?

1 Like

Yes, a PC will do the work, however I haven't decided yet on what to go with, OpenCV seems simple but I first wanted to get the cameras working.

Camera misalignment is supposed to be minimal as I made a 3D printed holder for them, lens distortion could be a big problem if I used a wide-angle lens, however for my application I do not need that so while I will look into lens distortion deeper, I am hoping it won't need much.

I didn't think this would be considered as an interesting idea, should I document this more and release it as an instructable or something?

For many years people have used the stereo camera idea for robot navigation (i.e. using a depth-mapped forward scene for obstacle avoidance and position determination). But it is hard to do in real time on a small robot, and commercial solutions tended to be closed source and expensive.

The ESP32-CAM with RPi-4 (built in WiFi AP!) and OpenCV could be a cheap solution for a small robot, and would be very popular with hobbyists if an out-of-the-box package were available.

Lens distortion is certainly present in the ESP32-CAM and for accurate depth mapping, the cameras would have to be calibrated. Discussion here

1 Like

Thanks for the link! I will definitely look into calibration.
I am not sure how exactly that is going to work, but I will try to do most of that work on the PC.

However, to do that I first need to get the cameras to take the photos and then send them to that PC.

I guess I will start working on the code. If you have any other suggestions, feel free to share!

As you probably know, depth mapping from stereo images is built into OpenCV: OpenCV: Depth Map from Stereo Images

My thought is that an RPi-4 could acquire images via WiFi from the two cameras separately, then submit a pair to OpenCV for analysis. That solves the image transmission problem entirely in code, and analysis could proceed concurrently.

But of course, algorithm development could happen on any computer.

I was afraid the ESP32CAM does not have enough pins usable for SPI chipselect. However the GPIO2 pin can be used if the SD card is not in use.

Also there was a problem with the ethernet library examples, for some reason it didn't detect the module (error said "Ethernet shield was not found. Sorry, can't run without hardware. :(". This was fixed by adding spi.begin() with the corresponding GPIO pins before initializing ethernet.

Connections are thus:
GPIO14 - SCLK (Clock)
GPIO12 - MISO (Master In Slave Out)
GPIO13 - MOSI (Master Out Slave In)
GPIO15 - SCS (Chip select) - GPIO2 is also usable, not sure about any others since I need GPIO 1 and 3, 0 is out of the question and 16 doesn't seem accessible. GPIO4 is for the flash diode and it creates problems because of that circuit. Maybe uninstalling the diode would allow it to work.

Next I figure out how to take a picture and send it over via FTP to a server. Then I worry about taking a picture with both ESP32CAMs and having one of them send it over to the master for transfer via FTP.

Here is the modified example code for the ESP32CAM:


/*
  DHCP-based IP printer

  This sketch uses the DHCP extensions to the Ethernet library
  to get an IP address via DHCP and print the address obtained.
  using an Arduino Wiznet Ethernet shield.

  Circuit:
   Ethernet shield attached to pins 10, 11, 12, 13

  created 12 April 2011
  modified 9 Apr 2012
  by Tom Igoe
  modified 02 Sept 2015
  by Arturo Guadalupi

 */

#include <SPI.h>
#include <Ethernet.h>

// Enter a MAC address for your controller below.
// Newer Ethernet shields have a MAC address printed on a sticker on the shield
byte mac[] = {
  0x00, 0xAA, 0xBB, 0xCC, 0xDE, 0x02
};

void setup() {
  // You can use Ethernet.init(pin) to configure the CS pin
  //Ethernet.init(10);  // Most Arduino shields
  //Ethernet.init(5);   // MKR ETH shield
  //Ethernet.init(0);   // Teensy 2.0
  //Ethernet.init(20);  // Teensy++ 2.0
  //Ethernet.init(15);  // ESP8266 with Adafruit Featherwing Ethernet
  //Ethernet.init(33);  // ESP32 with Adafruit Featherwing Ethernet
  Ethernet.init(15);  // ESP32CAM

  SPI.begin(14,12,13,15);

  // Open serial communications and wait for port to open:
  Serial.begin(9600);
  while (!Serial) {
    ; // wait for serial port to connect. Needed for native USB port only
  }

  // start the Ethernet connection:
  Serial.println("Initialize Ethernet with DHCP:");
  if (Ethernet.begin(mac) == 0) {
    Serial.println("Failed to configure Ethernet using DHCP");
    if (Ethernet.hardwareStatus() == EthernetNoHardware) {
      Serial.println("Ethernet shield was not found.  Sorry, can't run without hardware. :(");
    } else if (Ethernet.linkStatus() == LinkOFF) {
      Serial.println("Ethernet cable is not connected.");
    }
    // no point in carrying on, so do nothing forevermore:
    while (true) {
      delay(1);
    }
  }
  // print your local IP address:
  Serial.print("My IP address: ");
  Serial.println(Ethernet.localIP());
}

void loop() {
  switch (Ethernet.maintain()) {
    case 1:
      //renewed fail
      Serial.println("Error: renewed fail");
      break;

    case 2:
      //renewed success
      Serial.println("Renewed success");
      //print your local IP address:
      Serial.print("My IP address: ");
      Serial.println(Ethernet.localIP());
      break;

    case 3:
      //rebind fail
      Serial.println("Error: rebind fail");
      break;

    case 4:
      //rebind success
      Serial.println("Rebind success");
      //print your local IP address:
      Serial.print("My IP address: ");
      Serial.println(Ethernet.localIP());
      break;

    default:
      //nothing happened
      break;
  }
}

That library seems to have all the functions I would need, plus there are already examples of it being used for the ESP32CAM just like yours.

However, it doesn't work with the Ethernet library as it was created for Wi-Fi instead.
I may have to rewrite it to work with the Ethernet library, however I have never done that so I am not sure if I will be able to do it.

I will try though.

Turns out it wasn't as bad as I thought.
WifiClient and EthernetClient are very similarly written.

Thus changing the words "WiFi" to "Ethernet" and deleting a few instances of "timeout" since it isn't used in a few of the Ethernet functions.

The full code is on pastebin here: Laggger164's Pastebin - Pastebin.com

Unfortunately there seems to be a problem when trying to write a new file, every function then hangs up on FTP error: Offline and I am not sure why.
It does read the content fine though.

My suspicion is that this function has something to do with it. I tried deleting the "timeout" variable from it but nothing changed, just failed faster.

void ESP32_FTPClient::GetFTPAnswer (char* result, int offsetStart) {
  char thisByte;
  outCount = 0;

  unsigned long _m = millis();
  while (!client.available() && millis() < _m + timeout) delay(1);

  if( !client.available()){
    memset( outBuf, 0, sizeof(outBuf) );
    strcpy( outBuf, "Offline");

    _isConnected = false;
    isConnected();
    return;
  }

Definitely a step in the right direction, just have to figure out why it doesn't want to write the file.
Then I can try using this to get the picture across.

Finally I will be making a communication system that would take both pictures, send a pic from one ESP32CAM to the other, connected to which is the W5500 ethernet module and then send both pictures to a server.
This will start as a simple button command, however I will make a remote command somehow.

Here is the output from serial monitor:

16:40:23.332 -> rst:0x1 (POWERON_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
16:40:23.332 -> configsip: 0, SPIWP:0xee
16:40:23.332 -> clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
16:40:23.332 -> mode:DIO, clock div:1
16:40:23.332 -> load:0x3fff0018,len:4
16:40:23.332 -> load:0x3fff001c,len:1216
16:40:23.332 -> ho 0 tail 12 room 4
16:40:23.332 -> load:0x40078000,len:10944
16:40:23.332 -> load:0x40080400,len:6388
16:40:23.332 -> entry 0x400806b4
16:40:24.325 -> Initialize Ethernet with DHCP:
16:40:29.282 -> My IP address: 192.168.55.117
16:40:29.282 -> Connecting to: 192.168.55.200
16:40:29.282 -> Command connected
16:40:29.282 -> Send USER
16:40:29.282 -> Send PASSWORD
16:40:29.282 -> Send SYST
16:40:29.282 -> Send TYPE
16:40:29.282 -> Type A
16:40:29.282 -> Send PASV
16:40:29.282 -> Data port: 37862
16:40:29.282 -> Data connection established
16:40:29.282 -> Send CWD
16:40:29.282 -> Send MLSD
16:40:29.282 -> Result start
16:40:29.282 -> Result: 150 Opening ASCII mode data connection for MLSD
16:40:29.282 -> Result end
16:40:29.282 -> 
16:40:29.282 -> Directory info: 
16:40:29.282 -> modify=20210707143317;perm=flcdmpe;type=cdir;unique=D249C87BUC;UNIX.group=0;UNIX.groupname=media;UNIX.mode=0755;UNIX.owner=1000;UNIX.ownername=josai; .

16:40:29.282 -> modify=20210707143147;perm=flcdmpe;type=pdir;unique=D249C87BU9;UNIX.group=0;UNIX.groupname=media;UNIX.mode=0755;UNIX.owner=1000;UNIX.ownername=josai; ..

16:40:29.282 -> modify=20210707143322;perm=adfrw;size=3;type=file;unique=D249C87BUF;UNIX.group=0;UNIX.groupname=media;UNIX.mode=0744;UNIX.owner=1000;UNIX.ownername=josai; pootis.txt

16:40:29.282 -> Send TYPE
16:40:29.282 -> Type A
16:40:34.256 -> FTP error: Offline
16:40:34.291 -> Send PASV
16:40:39.294 -> FTP error: Offline
16:40:39.294 -> Bad PASV Answer
16:40:39.703 -> Connection closed
16:40:39.703 -> Send MKD
16:40:39.703 -> FTP error: Offline
16:40:39.703 -> Send CWD
16:40:39.703 -> FTP error: Offline
16:40:39.703 -> Send TYPE
16:40:39.703 -> FTP error: Offline
16:40:39.703 -> Send STOR
16:40:39.703 -> FTP error: Offline
16:40:39.703 -> Writing
16:40:39.703 -> FTP error: Offline
16:40:39.703 -> Close File
16:40:39.703 -> Send TYPE
16:40:39.703 -> FTP error: Offline
16:40:39.703 -> Send STOR
16:40:39.703 -> FTP error: Offline
16:40:39.703 -> Write File
16:40:39.703 -> FTP error: Offline
16:40:39.703 -> Close File
16:40:39.703 -> Connection closed

Turns out there is some weirdness going on server side causing the connection to close prematurily.

Thankfully another user managed to talk to the developer of that library and the solution was to open the connection again.
https://github.com/ldab/ESP32_FTPClient/issues/26

Just adding ftp.OpenConnection(); again solved the issue.

Now onto taking a picture and sending it through!
This should work nicely!

Here is the output from the serial monitor:

12:47:13.358 -> rst:0x1 (POWERON_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
12:47:13.427 -> configsip: 0, SPIWP:0xee
12:47:13.427 -> clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
12:47:13.427 -> mode:DIO, clock div:1
12:47:13.427 -> load:0x3fff0018,len:4
12:47:13.427 -> load:0x3fff001c,len:1216
12:47:13.427 -> ho 0 tail 12 room 4
12:47:13.427 -> load:0x40078000,len:10944
12:47:13.427 -> load:0x40080400,len:6388
12:47:13.427 -> entry 0x400806b4
12:47:14.414 -> Initialize Ethernet with DHCP:
12:47:19.586 -> My IP address: 192.168.55.115
12:47:19.586 -> Connecting to: 192.168.55.200
12:47:19.586 -> Command connected
12:47:19.586 -> 220 ProFTPD Server (freenas FTP Server) [::ffff:192.168.55.200]
12:47:19.586 -> 
12:47:19.586 -> Send USER
12:47:19.586 -> 331 Password required for josai
12:47:19.586 -> 
12:47:19.586 -> Send PASSWORD
12:47:19.586 -> 230-Welcome to FreeNAS FTP Server
12:47:19.586 -> 230 User josai logged in
12:47:19.586 -> 
12:47:19.586 -> Send SYST
12:47:19.586 -> 215 UNIX Type: L8
12:47:19.586 -> 
12:47:19.586 -> Send TYPE
12:47:19.586 -> Type A
12:47:19.586 -> 200 Type set to A
12:47:19.586 -> 
12:47:19.586 -> Send PASV
12:47:19.586 -> 227 Entering Passive Mode (192,168,55,200,217,72).
12:47:19.586 -> 
12:47:19.586 -> Data port: 55624
12:47:19.586 -> Data connection established
12:47:19.586 -> Send CWD
12:47:19.586 -> 250 CWD command successful
12:47:19.586 -> 
12:47:19.586 -> Send MLSD
12:47:19.586 -> 150 Opening ASCII mode data connection for MLSD
12:47:19.586 -> 
12:47:19.586 -> Result start
12:47:19.586 -> Result: 150 Opening ASCII mode data connection for MLSD
12:47:19.586 -> Result end
12:47:19.586 -> 
12:47:19.586 -> Directory info: 
12:47:19.586 -> modify=20210707143317;perm=flcdmpe;type=cdir;unique=D249C87BUC;UNIX.group=0;UNIX.groupname=media;UNIX.mode=0755;UNIX.owner=1000;UNIX.ownername=josai; .

12:47:19.586 -> modify=20210707143147;perm=flcdmpe;type=pdir;unique=D249C87BU9;UNIX.group=0;UNIX.groupname=media;UNIX.mode=0755;UNIX.owner=1000;UNIX.ownername=josai; ..

12:47:19.586 -> modify=20210707143322;perm=adfrw;size=3;type=file;unique=D249C87BUF;UNIX.group=0;UNIX.groupname=media;UNIX.mode=0744;UNIX.owner=1000;UNIX.ownername=josai; pootis.txt

12:47:19.586 -> Connecting to: 192.168.55.200
12:47:19.586 -> Command connected
12:47:19.586 -> 220 ProFTPD Server (freenas FTP Server) [::ffff:192.168.55.200]
12:47:19.586 -> 
12:47:19.586 -> Send USER
12:47:19.586 -> 331 Password required for josai
12:47:19.586 -> 
12:47:19.586 -> Send PASSWORD
12:47:19.586 -> 230-Welcome to FreeNAS FTP Server
12:47:19.586 -> 230 User josai logged in
12:47:19.586 -> 
12:47:19.586 -> Send SYST
12:47:19.586 -> 215 UNIX Type: L8
12:47:19.586 -> 
12:47:19.586 -> Send TYPE
12:47:19.586 -> Type A
12:47:19.586 -> 200 Type set to A
12:47:19.586 -> 
12:47:19.586 -> Send PASV
12:47:19.586 -> 227 Entering Passive Mode (192,168,55,200,175,65).
12:47:19.586 -> 
12:47:19.586 -> Data port: 44865
12:47:19.586 -> Data connection established
12:47:19.586 -> Send MKD
12:47:19.586 -> 257 "/my_new_dir" - Directory successfully created
12:47:19.586 -> 
12:47:19.586 -> Send CWD
12:47:19.586 -> 250 CWD command successful
12:47:19.586 -> 
12:47:19.586 -> Send TYPE
12:47:19.586 -> Type I
12:47:19.586 -> 200 Type set to I
12:47:19.586 -> 
12:47:19.586 -> Send PASV
12:47:19.586 -> 227 Entering Passive Mode (192,168,55,200,100,89).
12:47:19.586 -> 
12:47:19.586 -> Data port: 25689
12:47:19.586 -> Data connection established
12:47:19.586 -> Send STOR
12:47:19.586 -> 150 Opening BINARY mode data connection for octocat.jpg
12:47:19.586 -> 
12:47:19.586 -> Writing
12:47:19.691 -> Close File
12:47:19.726 -> 226 Transfer complete
12:47:19.726 -> 
12:47:19.726 -> Send TYPE
12:47:19.726 -> Type A
12:47:19.726 -> 200 Type set to A
12:47:19.726 -> 
12:47:19.726 -> Send PASV
12:47:19.726 -> 227 Entering Passive Mode (192,168,55,200,201,245).
12:47:19.726 -> 
12:47:19.726 -> Data port: 51701
12:47:19.726 -> Data connection established
12:47:19.726 -> Send STOR
12:47:19.726 -> 150 Opening ASCII mode data connection for hello_world.txt
12:47:19.726 -> 
12:47:19.726 -> Write File
12:47:19.726 -> Close File
12:47:19.726 -> 226 Transfer complete
12:47:19.726 -> 
12:47:19.726 -> Connection closed