Combining various sources of code, i built a cheap web camera that sends an updated image to a web server every 2 minutes. It is based on the ESP8266 wifi module, programmed with the Arduino IDE, and a serial camera (TTL level) like the PTC06 or (modified) PTC02/PTC08. The highest image resolution is 640×480, resulting in JPG files of around 45kB.

IMG_20171017_134035 - Edited

The above serial cameras come with a DSP that does the compression into JPG format, and stores the image until we read it over the serial connection. Adafruit has written an extensive Arduino library for these cameras, but i started from a different, excellent example of an ESP8266 acting as a web server with a PTC06 camera. I used a NodeMCU v1.0 and the example worked immediately. The reason i used the PTC06 camera is that it comes standard with Serial TTL levels (not RS485, not RS232). The module can be powered by the 3V3 rail of the NodeMCU (it does not need 5V).

IMG_20171017_133932 - Edited

My aim is to build a webpage with live updated images from a certain location, so i need to send the images to a web server. I could not find an FTP library for the ESP8266, so i used this Arduino FTP example.  This works after removing the F() wrappers from the client.print commands, as described here. I created an FTP account on my server, and a directory to put the images in. Below is an example of a 640×480 picture taken by the PTC06 (4.3mm lens) without modifying any image properties (after manual focus).

picture640x480

I went back to the above camera web server example, and instead of sending the file to the client over a TCP connection, i saved it as a JPG image to the ESP8266’s SPIFFS file system. Then i read the file from SPIFFS to do the FTP, appending an index number to the filename, from 0 to 9. So i will always have the 10 most recent pictures stored on my server, overwriting the oldest one with the newest one.

Maybe it is possible to skip saving the file, and read from the camera while writing over FTP; i did not try that.

The PTC06 is only available with a rather wide lens (4.3mm), and without enclosure. I got a PTC02 with a 25mm lens, in a waterproof IP67 casing. It is available from PUTAL, the manufacturer (links above) in RS485 and RS232 versions, not TTL. Adafruit does sell a waterproof TTL version. I got the RS232 version and followed these instructions to remove the RS232 chip to modify it to TTL level output (not really easy).

This waterproof PTC02 actually has a PTC08 pcb inside. It also has an extra pcb with IR LEDs to enable night vision. These IR LEDs switch on based on an LDR sensor on the same PCB. The only connection with the camera PCB are the 2 power wires. As i don’t need night vision, i removed this IR LED pcb. On the left the original with IR LEDs and on the right without, and with the green PTC08 pcb visible.

IMG-20171014-WA0001 - Edited IMG_20171017_141636 - Edited

The PTC02/PTC08 can be tested also via their video output on the CVBS terminal, that is useful for adjusting the focus. Details available at Adafruit.

IMG_20171016_102758 - Edited

My initial pictures of the PTC02/PTC08 were very grey, not much colour. I installed CommTool on a Windows laptop as described by Adafruit to be able to change the increase the saturation; for some reason it was set quite low, maybe for the night vision. Below picture is taken with the PCT02 25mm lens, modified saturation, max resolution of 640×480. For reference, it is the same house as in the centre of the above picture (4.3mm lens).

picture640x480-ptc02-house3

The instructions to be sent to the camera over Serial are almost identical for the PTC06 vs PTC02/PTC08; only the SendReadDataCmd() has a different end byte (0x0a vs 0xff). And the default baud rate is different: the PTC06 is 115200, and the PTC02/PTC08 38400. It can be changed but Adafruit warns against it so i did not bother; speed is not a major concern in my application.

The 25mm lens was a bit too narrow, so i decided to use the housing of the PTC02 to put an ESP8266 (ESP-01 package) inside, with the PTC06 camera. So the whole project fits inside this nice waterproof box, with only power wires coming out. The perf board has an MCP1826 voltage regulator from 5V to 3.3V, so the project can be powered with 5V USB. The ESP-01 can be taken out of the header for re-programming. Th blue LED indicates capture, save, ftp.

IMG_20171022_151101 - Edited IMG_20171022_151145 - Edited

IMG_20171022_154911 - Edited

The resulting quality is quite good (640×480):

picture3 (1)

Here is my code, as used on a NodeMCU v1.0 – it assume it will work on any ESP8266 module. Sorry for the bad formatting, blame WordPress. The two #include should read: #include <ESP8266WiFi.h> and
#include <FS.h>

 


/*
   ESP8266 with serial camera (PTC06 TTL and PTC02/PTC08 TTL) to upload pictures to cloud server:
   > take picture
   > write jpg to SPIFFS
   > FTP upload to server
   > wait
   LED on D1 = GPIO5 to indicate snap, saving, ftp

   CAMERA CODE:
   based on chynehome.com/web/wp-content/uploads/2015/08/Wifi_Cam_13.txt
   chynehome.com/web/camera-ip-wifi-avec-une-camera-serie-jpeg-et-un-module-wifi-esp8266/
   modified by Tom:    removed webserver
                       writing picture to SPIFFS
                       include timeout
                       add PTC02/PTC08 support
   PTC02 is a waterproof version with PTC08 pcb inside
   PTC02/PTC08 serial camera RS232 modified to TTL (removed RS232 IC - see datasheet)
   powered by 3.3V
   RX-TX
   TX-RX

   FTP CODE:
   Original Arduino: FTP passive client
   http://playground.arduino.cc/Code/FTP
   Modified 6 June 2015 by SurferTim
   You can pass flash-memory based strings to Serial.print() by wrapping them with F().
   2015-12-09 Rudolf Reuter, adapted to ESP8266 NodeMCU, with the help of Markus.
   https://github.com/esp8266/Arduino/issues/1183
   modified by TOM;   remove F() from doFTP() - otherwise password does not work
                      use hostname instead of IP
                      include timeout

   Tom Tobback Oct 2017
   NodeMCU version
*/

#include 
#include 

///////////////////////////////////  WIFI  //////////////////////////////////
const char *ssid = "xxx";
const char *password = "xxx";
WiFiClient client;
WiFiClient dclient;
///////////////////////////////////  FTP  //////////////////////////////////
File fh;   // SPIFFS file handle
char FTPserver[] = "xxx"; // name of FTP server
String fileName = "picture.jpg";
String  path = "/picture.jpg";
const boolean debug = false;  // true = messages about FTP on Serial (conflicts with camera on Serial)
char outBuf[128];
char outCount;
int counter = 0;   // for FTP filename
const int ftp_timeout = 10;   // timeout in seconds for FTP replies
//////////////////////////////////  CAMERA  ////////////////////////////////
const boolean PTC06 = false;  // choice between PTC06 and PTC02/PTC08
byte incomingbyte;
int a = 0x0000, //Read Starting address
    j = 0,
    k = 0,
    count = 0,
    sendcount = 0;
uint8_t MH, ML;
boolean EndFlag = 0;
const int cmd_delay = 1000;
const int capture_timeout = 60;   // timeout in seconds -- should be enough to read/save large picture (45 sec at baud 38400)
////////////////////////////////  GENERAL //////////////////////////////////
const int LED_PIN = 5;          // use 2 for ESP-01
const unsigned long main_interval = 60;  // seconds between pics
unsigned long main_timestamp;
boolean first = true;   // to take picture in first loop, not waiting for interval

///////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////

void setup() {
  pinMode(LED_PIN, OUTPUT);

  WiFi.begin ( ssid, password );
  while ( WiFi.status() != WL_CONNECTED ) {  // blink LED while connecting to wifi
    delay (500);
    digitalWrite(LED_PIN, HIGH);
    delay(100);
    digitalWrite(LED_PIN, LOW);
  }

  if (PTC06) {
    Serial.begin(115200);  // default for PTC06
  } else
  {
    Serial.begin (38400);  // default for PTC02/PTC08   -- can be changed in theory, but Adafruit warns against it
  }
  SPIFFS.begin();

  delay(3000);   // camera needs time to start up
}

///////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////

void loop() {

  yield();  // for wifi stack

  if (millis() - main_timestamp > main_interval * 1000L || first) {     // make sure first loop takes picture

    first = false;    // switch flag off after first picture
    picture();   // take picture and save to SPIFFS, LED will flash

    digitalWrite(LED_PIN, HIGH);                          // LED on during FTP
    if (!doFTP()) {                                   // send over FTP to cloud server, LED full on, try a second time if first fails
      delay(100);
      doFTP();
    }
    digitalWrite(LED_PIN, LOW);                          // LED off when FTP finished

    main_timestamp = millis();
    counter++;                      // for picture index
    if (counter == 10) counter = 0;
  }
}


///////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////  CAMERA  ////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////

void picture() {

  picture_hd();    // set highest resolution 640x480   -- options: ld, md, hd

  SPIFFS.remove(path);         // delete picture file
  fh = SPIFFS.open(path, "w");

  digitalWrite(LED_PIN, HIGH);                          // LED on while taking pic = short flash
  SendTakePhotoCmd();
  delay(100);
  digitalWrite(LED_PIN, LOW);                          // LED off

  while (Serial.available() > 0) {
    incomingbyte = Serial.read();         // flush reply from camera
  }

  String out = "";
  byte b[32];
  unsigned long capture_timestamp = millis();                           // timeout
  while (!EndFlag && (millis() - capture_timestamp < capture_timeout * 1000L)) { // 60sec j = 0; k = 0; count = 0; sendcount = 0; SendReadDataCmd(); delay(35); //try going up digitalWrite(LED_PIN, HIGH); // LED on when reading camera part while (Serial.available() > 0) {
      incomingbyte = Serial.read();
      k++;
      if ((k > 5) && (j < 32) && (!EndFlag)) {
        b[j] = incomingbyte;
        out += (char)incomingbyte;
        if ((b[j - 1] == 0xFF) && (b[j] == 0xD9))
          EndFlag = 1;
        j++;
        count++;
      }
    }

    fh.print(out);
    digitalWrite(LED_PIN, LOW);                          // LED off when part saved to SPIFFS

    out = "";
  }

  fh.close();

  delay(3000);    // was 3000

  StopTakePhotoCmd(); //stop this picture so another one can be taken
  EndFlag = 0; //reset flag to allow another picture to be read

}

void picture_ld() {    // reset camera for low definition picture
  SendResetCmd();
  delay(cmd_delay);
  ChangeSizeSmall();
  delay(cmd_delay);
  SendResetCmd();
  delay(cmd_delay);
}

void picture_md() {
  SendResetCmd();
  delay(cmd_delay);
  ChangeSizeMedium();
  delay(cmd_delay);
  SendResetCmd();
  delay(cmd_delay);
}

void picture_hd() {
  SendResetCmd();
  delay(cmd_delay);
  ChangeSizeBig();
  delay(cmd_delay);
  SendResetCmd();
  delay(cmd_delay);
}

//*****************************************CAMERA COMMANDS**************************************//

//Send Reset command
void SendResetCmd() {             // same for PTC6, PTC2/PTC8
  Serial.write((byte)0x56);
  Serial.write((byte)0x00);
  Serial.write((byte)0x26);
  Serial.write((byte)0x00);
}

//Send take picture command
void SendTakePhotoCmd() {         // same for PTC6, PTC2/PTC8
  Serial.write((byte)0x56);
  Serial.write((byte)0x00);
  Serial.write((byte)0x36);
  Serial.write((byte)0x01);
  Serial.write((byte)0x00);

  a = 0x0000; //reset so that another picture can taken
}

//Read data
void SendReadDataCmd() {
  MH = a / 0x100;
  ML = a % 0x100;

  Serial.write((byte)0x56);
  Serial.write((byte)0x00);
  Serial.write((byte)0x32);
  Serial.write((byte)0x0c);
  Serial.write((byte)0x00);
  Serial.write((byte)0x0a);
  Serial.write((byte)0x00);
  Serial.write((byte)0x00);
  Serial.write((byte)MH);
  Serial.write((byte)ML);
  Serial.write((byte)0x00);
  Serial.write((byte)0x00);
  Serial.write((byte)0x00);
  Serial.write((byte)0x20);
  Serial.write((byte)0x00);
  if (PTC06) {
    Serial.write((byte)0x0a);  // PTC06
  } else
  {
    Serial.write((byte)0xff);   // PTC02/PTC08
  }
  a += 0x20;
}

void StopTakePhotoCmd() {       // same for PTC6, PTC2/PTC8
  Serial.write((byte)0x56);
  Serial.write((byte)0x00);
  Serial.write((byte)0x36);
  Serial.write((byte)0x01);
  Serial.write((byte)0x02);  
}

void ChangeSizeSmall() {        // same for PTC6, PTC2/PTC8
  Serial.write((byte)0x56);
  Serial.write((byte)0x00);
  Serial.write((byte)0x31);
  Serial.write((byte)0x05);
  Serial.write((byte)0x04);
  Serial.write((byte)0x01);
  Serial.write((byte)0x00);
  Serial.write((byte)0x19);
  Serial.write((byte)0x22);
}

void ChangeSizeMedium()
{
  Serial.write((byte)0x56);
  Serial.write((byte)0x00);
  Serial.write((byte)0x31);
  Serial.write((byte)0x05);
  Serial.write((byte)0x04);
  Serial.write((byte)0x01);
  Serial.write((byte)0x00);
  Serial.write((byte)0x19);
  Serial.write((byte)0x11);
}

void ChangeSizeBig()
{
  Serial.write((byte)0x56);
  Serial.write((byte)0x00);
  Serial.write((byte)0x31);
  Serial.write((byte)0x05);
  Serial.write((byte)0x04);
  Serial.write((byte)0x01);
  Serial.write((byte)0x00);
  Serial.write((byte)0x19);
  Serial.write((byte)0x00);
}


///////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////   FTP  ////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////

///////////////////////////////////////////////////////////////////////////////////////////
//----------------- FTP fail
void efail() {
  byte thisByte = 0;

  client.println("QUIT");

  unsigned long ftp_timestamp = millis();
  while (!client.available() && (millis() - ftp_timestamp < ftp_timeout * 1000L)) delay(1); // add timeout otherwise stuck waiting from FTP reply while (client.available()) { thisByte = client.read(); if (debug) Serial.write(thisByte); } client.stop(); if (debug) Serial.println(F("Command disconnected")); fh.close(); if (debug) Serial.println(F("SD closed")); } // efail /////////////////////////////////////////////////////////////////////////////////////////// //-------------- FTP receive reply byte eRcv() { byte respCode; byte thisByte; unsigned long ftp_timestamp = millis(); while (!client.available()) { if (millis() - ftp_timestamp > ftp_timeout * 1000L) return 0;         // add timeout otherwise stuck waiting from FTP reply
    delay(1);
  }

  respCode = client.peek();

  outCount = 0;

  while (client.available()) {
    thisByte = client.read();
    if (debug) Serial.write(thisByte);
    if (outCount < 127) { outBuf[outCount] = thisByte; outCount++; outBuf[outCount] = 0; } } if (respCode >= '4') {
    efail();
    return 0;
  }
  return 1;
}  // eRcv()

///////////////////////////////////////////////////////////////////////////////////////////
//--------------- FTP handling
byte doFTP() {

  fh = SPIFFS.open(path, "r");

  if (!fh) {
    //    Serial.println(F("SPIFFS open fail"));
    return 0;
  }

  if (!fh.seek((uint32_t)0, SeekSet)) {
    //    Serial.println(F("Rewind fail"));
    fh.close();
    return 0;
  }

  if (debug) Serial.println(F("SPIFFS opened"));

  if (client.connect(FTPserver, 21)) {  // 21 = FTP server
    //    Serial.println(F("Command connected"));
  } else {
    fh.close();
    //    Serial.println(F("Command connection failed"));
    return 0;
  }

  if (!eRcv()) return 0;
  if (debug) Serial.println("Send USER");
  client.println("USER xxx");

  if (!eRcv()) return 0;
  if (debug) Serial.println("Send PASSWORD");
  client.println("PASS xxx");

  if (debug) Serial.println("Sent PASSWORD");

  if (!eRcv()) return 0;
  if (debug) Serial.println("Send SYST");
  client.println("SYST");

  if (!eRcv()) return 0;
  if (debug) Serial.println("Send Type I");
  client.println("Type I");

  if (!eRcv()) return 0;
  if (debug) Serial.println("Send PASV");
  client.println("PASV");

  if (!eRcv()) return 0;

  char *tStr = strtok(outBuf, "(,");
  int array_pasv[6];
  for ( int i = 0; i < 6; i++) {
    tStr = strtok(NULL, "(,");
    array_pasv[i] = atoi(tStr);
    if (tStr == NULL) {
      //      Serial.println(F("Bad PASV Answer"));
    }
  }
  unsigned int hiPort, loPort;
  hiPort = array_pasv[4] << 8; loPort = array_pasv[5] & 255; if (debug) Serial.print(F("Data port: ")); hiPort = hiPort | loPort; if (debug) Serial.println(hiPort); if (dclient.connect(FTPserver, hiPort)) { // Serial.println(F("Data connected")); } else { // Serial.println(F("Data connection failed")); client.stop(); fh.close(); return 0; } String fileNameFTP = "picture"; fileNameFTP += counter; fileNameFTP += ".jpg"; if (debug) Serial.println("Send STOR filename"); client.print("STOR "); client.println(fileNameFTP); if (!eRcv()) { dclient.stop(); return 0; } if (debug) Serial.println(F("Writing")); // for faster upload increase buffer size to 1460 //#define bufSizeFTP 64 #define bufSizeFTP 1460 uint8_t clientBuf[bufSizeFTP]; //unsigned int clientCount = 0; size_t clientCount = 0; while (fh.available()) { clientBuf[clientCount] = fh.read(); clientCount++; if (clientCount > (bufSizeFTP - 1)) {
      dclient.write((const uint8_t *) &clientBuf[0], bufSizeFTP);
      clientCount = 0;
      delay(1);
    }
  }
  if (clientCount > 0) dclient.write((const uint8_t *) &clientBuf[0], clientCount);

  dclient.stop();
  //  Serial.println(F("Data disconnected"));

  if (!eRcv()) return 0;

  client.println("QUIT");

  if (!eRcv()) return 0;

  client.stop();
  //  Serial.println(F("Command disconnected"));

  fh.close();
  if (debug) Serial.println(F("SPIFS closed"));
  return 1;
}  // doFTP()


///////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////  DEBUG SPIFFS  ////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////

void listSPIFFS() {                     // only for debugging
  Serial.println("SPIFFS have:");
  String fileNameDir;
  Dir dir = SPIFFS.openDir("/");
  while (dir.next()) {
    fileNameDir = dir.fileName();
    size_t fileSize = dir.fileSize();
    Serial.printf("FS File: %s, size: %s\n", fileNameDir.c_str(), formatBytes(fileSize).c_str());
  }
}

//format bytes
String formatBytes(size_t bytes) {
  if (bytes < 1024) {
    return String(bytes) + "B";
  } else if (bytes < (1024 * 1024)) {
    return String(bytes / 1024.0) + "KB";
  } else if (bytes < (1024 * 1024 * 1024)) {
    return String(bytes / 1024.0 / 1024.0) + "MB";
  } else {
    return String(bytes / 1024.0 / 1024.0 / 1024.0) + "GB";
  }
}

Leave a Reply

Your email address will not be published. Required fields are marked *