ESP8266 – CCTV still camera

Introduction
When I saw this cheap Serial JPEG Camera I got the idea to convert a Fake CCTV camera into a “real” CCTV. Well, not a fully functional CCTV, because the serial camera is too slow to be able to stream video (and so is the ESP8266). But at least to get pictures from it.
The idea is to trigger the camera by motion detection, capture one image and store it in the internal memory. Then the image is transferred to a FTP server from where it can be accessed by a PC or Android phone or tablet.
Hardware
The serial connection between the camera module and the ESP8266 is very simple. I used a NodeMCU ESP8266 module. There is only one serial port and it is occupied by the USB connection. So I decided to go for a software serial solution. The camera RX pin is connected to GPIO14 and the camera TX pin is connected to GPIO12.
Important here is to check the voltage level of the TX and RX lines of the camera. The GPIO’s of the ESP8266 are only compatible to 3.3V max. If you feed them with 5V or more, you will most likely fry your board. If this is something that is alien to you, don’t risk it. Contact the experts, like those from AC Electrical (https://acelectrical.ca/), and ensure it is done properly.
I was lucky with the camera module I bought. Even the module is powered by 5V, the TX line showed only 3.3V level. So I was able to connect the camera directly to the ESP8266 without the need of a level shifter. As the connection between the ESP and the camera is really very simple (only 4 wires), I never made a schematic for it. So I will skip here the hardware details.
Trigger the camera
To trigger the camera I found three possibilities:
- Motion detection by the camera module
- Motion sensor connected to the ESP8266
- Trigger over WiFi by another device
I tested the motion detection of the camera itself and I found it over sensitive. It basically triggers on every small change in the view of the camera. And there was no possibility to change the sensitivity.
So I looked into the other two possibilities. I would have gone for #2 and attached one of these PIR sensors to the ESP8266 for a stand-alone system. But as I had already a DIY security alert systems build and installed, the solution #3 is the one I implemented. The details of this security alert system will be covered by another post here. As a summary, it is an ESP8266 with a light sensor to detect day/night, a PIR sensor to detect movements in my front yard, a siren for audible alarm and a relay to automatically switch on the lights in the front yard. The security alert system is part of my home control system. I use the capabilities of the system to communicate directly to each other device to trigger the camera whenever a movement in the front yard is detected.
Software
The software was completely written with Visual Studio Code, using the PlatformIO plug-in. To make life easier, I stick on the Arduino-Framework, because most of the available Arduino libraries work fine with the ESP8266.
The software parts I show here are not complete. To keep it simple, I left out some parts that needs deeper explanations or are better to explain in the context of my whole Home Control System. Parts I left out
- Connecting to WiFi using WiFiManager, a library that opens a hotspot to configure the WiFi settings if required (like on the first boot).
- Parts of the communication packets that are only of interest together with my Home Control System
- Debug messages over TCP connections
However the whole code with everything inside can be found in my Github repo.
Structure of the software
Different to Arduino IDE, programming with Visual Studio Code (or Atom as an alternative) gives the possibility to split the code into several parts and header files, which makes it easier to overview and maintain it. In this project I have
- Setup.h – Includes all necessary libraries and header files
- declarations.h – Definition of global variables and structures
- globals.h – Declaration of global variables and structures, needed by the compiler and linker during generation of the code
- functions.h – Declaration of functions, needed by the compiler and linker during generation of the code
- Setup.cpp – The void setup(void) function to initialize the system
- Main.cpp – The void loop(void) function that is repeated over and over
- Camera.cpp – The functions to access the serial camera
- Ftp.cpp – A FTP client helper that connects to a FTP server and stores the picture on this server
- LanSubs.cpp – The communication functions
- Subs.cpp – Other functions
Required libraries
In setup.h we include the required libraries by including their header files
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#include <Arduino.h> #include <Ticker.h> #include <ESP8266WiFi.h> #include <ArduinoJson.h> #include <WiFiClient.h> #include <FS.h> #include <TimeLib.h> #include <Adafruit_VC0706.h> #include <SoftwareSerial.h> /* Common private libraries */ #include <ntp.h> #include <leds.h> #include <wifi.h> #include <wifiAPinfo.h> #include <spiffs.h> /* globals.h contains defines and global variables */ #include "globals.h" /* functions.h contains all function declarations */ #include "functions.h" |
Most of these libraries are standard Arduino/ESP8266 libraries. For a few functions I need additional libraries.
ArduinoJson is used to easy create and parse JSON objects. I use JSON objects for the status messages
TimeLib is used to create time stamps for the picture names.
Adafruit_VC0706 is the library that communicates with the camera.
There are as well some “private” libraries that I created for my self and that contains functions I use in all of my ESP8266 projects. These libraries are available as well from one of my Github repos.
ntp connects to a NTP server to update the RTC and has some functions to create time-date strings used in status messages and file names
leds has some standardized functions to access the build-in LEDs
wifi has functions to setup, connect and reconnect to the local WiFI network
wifiAPinfo are the definitions of local IP’s, ports, passwords and usernames e.g. for my local network and for my MQTT server (of course I don’t share this file)
spiffs functions to initialize the SPIFFS file system and write, read or erase files.
void setup()
In setup the initialization of the different communication channels, the SPIFFS file system, setup the device time by syncing with a NTP server and initialize the ArduinoOTA for updates of the software.
First the definition of a ticker to send out a “heart beat” of the device every 1 minute, then initialization of the serial port and the SPIFFS file system
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
#include <Setup.h> #include <declarations.h> /** Timer for heart beat */ Ticker heartBeatTimer; void setup() { inSetup = true; initLeds(blinkLED,flashLED); // COM LED -- ACT LED digitalWrite(flashLED,LOW); digitalWrite(blinkLED,LOW); Serial.begin(115200); Serial.setDebugOutput(false); Serial.println(""); Serial.println("Hello from ESP8266 home security camera"); Serial.print("SW build: "); Serial.println(compileDate); // Initialize file system. bool spiffsOK = false; if (!SPIFFS.begin()) { if (SPIFFS.format()){ spiffsOK = true; } } else { // SPIFFS ready to use spiffsOK = true; } |
Next is to connect to a WiFi network. Usually I use here the WiFiManager library, but to keep the code simple I connect directly.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// Connect to WiFi WiFi.mode(WIFI_STA); WiFi.begin(ssidMHC, password); // ssidMHC and password are defined in wifiAPinfo.h uint32_t startTime = millis(); while (WiFi.waitForConnectResult() != WL_CONNECTED) { if (millis()-startTime > 30000) { // wait maximum 30 seconds for a connection Serial.println("Failed to connect to WiFI"); Serial.println("Rebooting in 30 seconds"); delay(30000); ESP.reset(); } } // WiFi connection successfull Serial.println("Connected to "); Serial.println(WiFi.SSID()); Serial.println("with IP address "); Serial.println(WiFi.localIP()); |
Once we have successfully connected, we start the TCP server, which will be listening for commands send by other devices and we sync the device time with a NTP server.
1 2 3 4 5 6 7 8 |
// Start the tcp socket server to listen on port tcpComPort tcpServer.begin(); // Set initial time tryGetTime(false); // part of my "private" library => ntp.h // Prepare NTP time update timer lastSyncTime = now(); |
Next step is to connect to the serial camera and initialize it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
// Start camera connection // Try to get the baud rate of the serial connection uint32_t foundBaud = cam.autoDetectBaudRate(); delay(1000); if (foundBaud != 0) { if (cam.begin(foundBaud)) { Serial.println("Camera found!"); // Set the picture size - you can choose one of 640x480, 320x240 or 160x120 // Remember that bigger pictures take longer to transmit! cam.setImageSize(VC0706_640x480); // biggest // cam.setImageSize(VC0706_320x240); // medium // cam.setImageSize(VC0706_160x120); // small // Reset is necessary only if resolution other than 640x480 is selected cam.reset(); delay(500); Serial.println("Image size = " + String(cam.getImageSize())); cam.setCompression(255); Serial.println("Compression = " + String(cam.getCompression())); // Motion detection system can alert you when the camera 'sees' motion! // cam.setMotionDetect(true); // turn it on // Serial.print("Motionstatus = "); Serial.println(cam.getMotionDetect()); cam.resumeVideo(); digitalWrite(flashLED,LOW); } } else { Serial.println("No camera found?"); doubleLedFlashStart(1); hasCamera = false; } |
Last thing to do is to initialize ArduinoOTA. Once the device is mounted at its dedicated position it is usually difficult to update the software without unmounting the device. Therefor I use the OTA function on all my devices. To make it easy to find my devices with mDNS, all devices get a unique name. The name is created by the device type and part of the MAC address (in case there are several devices of the same type).
1 2 3 4 5 6 7 8 9 10 11 |
char hostApName[] = "MHC-Cam-xxxxxxxx"; // Create device ID from MAC address String macAddress = WiFi.macAddress(); hostApName[8] = macAddress[0]; hostApName[9] = macAddress[1]; hostApName[10] = macAddress[9]; hostApName[11] = macAddress[10]; hostApName[12] = macAddress[12]; hostApName[13] = macAddress[13]; hostApName[14] = macAddress[15]; hostApName[15] = macAddress[16]; |
And here is the initialization of ArduinoOTA
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// Prepare OTA update listener ArduinoOTA.onStart([]() { wdt_disable(); String debugMsg = "OTA start"; Serial.println(debugMsg); doubleLedFlashStart(0.1); WiFiUDP::stopAll(); WiFiClient::stopAll(); tcpServer.close(); otaRunning = true; }); // Start OTA server. ArduinoOTA.setHostname(hostApName); ArduinoOTA.begin(); MDNS.addServiceTxt("arduino", "tcp", "board", "ESP8266"); MDNS.addServiceTxt("arduino", "tcp", "type", camDevice); MDNS.addServiceTxt("arduino", "tcp", "id", String(hostApName)); MDNS.addServiceTxt("arduino", "tcp", "service", mhcIdTag); MDNS.addServiceTxt("arduino", "tcp", "loc", String(devLoc)); |
As you can see, I add some additional service text to the mDNS service. This makes it easier to identify the device.
Last thing to do in setup is to initialize the ticker which will send out every minute a status message as UDP broadcast. This is an important thing, because my home control system is setup to work without a dedicated server. I could consider looking at specific servers, as there is no denying that my home control system would benefit from a dedicated server, but a shared server is the cheaper option which is why it appeals to me. Each device within the system can find and communicate with any other device. Read my posts about My Home Control System to learn more about it.
1 2 3 4 5 6 |
// Start heart beat sending every 1 minutes heartBeatTimer.attach(60, triggerHeartBeat); sendBroadCast(false); inSetup = false; wdt_enable(WDTO_8S); } |
In these last steps you can see that I enable the integrated watchdog of the ESP8266. For a security device it is important that it can survive software problems like infinite loops and go back to work. The watchdog needs to be reset every 8 seconds, otherwise the ESP8266 will automatically reset. This ensures that in case of a fatal error or crash, the device will be reset after latest 8 seconds.
void loop(void)
There is not much happening in the main loop. Basically we run in circles here, waiting for a incoming command over TCP, checking if an OTA update has started, blink a LED to show that the code is still running. Once every minute the heartbeat flag will be set. Then we check if we still have WiFi connection and try to reconnect in case we lost it. There is as well a check if the device time was synced with a NTP server. In case that failed before, a retry is started. And of course the status message will be broadcasted over UDP, so that other device know we are still alive, where we are, what we can do and what the IP address is.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
#include <Setup.h> /** Counter for "I am alive" red LED blinking in loop() */ long liveCnt = 0; void loop() { wdt_reset(); // Handle OTA updates ArduinoOTA.handle(); if (otaRunning) { // If the OTA update is active we do nothing else here in the main loop return; } wdt_reset(); // Handle new request on tcp socket server if available WiFiClient tcpClient = tcpServer.available(); if (tcpClient) { socketServer(tcpClient); digitalWrite(flashLED, LOW); } // // Check if motion was detected // if (hasCamera) { // if (cam.motionDetected()) { // cam.setMotionDetect(false); // takeShot(); // cam.setMotionDetect(true); // } // } wdt_reset(); // Give a "I am alive" signal liveCnt++; if (liveCnt >= 100000) { // 100000 digitalWrite(blinkLED, !digitalRead(blinkLED)); liveCnt = 0; } wdt_reset(); if (heartBeatTriggered) { heartBeatTriggered = false; if (!WiFi.isConnected()) { wdt_disable(); WiFi.reconnect(); wdt_enable(WDTO_8S); } // In case we don't have a time from NTP or local server, retry if (!gotTime) { wdt_disable(); tryGetTime(false); wdt_enable(WDTO_8S); } // Stop the tcp socket server tcpServer.stop(); // Restart the tcp socket server to listen on port tcpComPort tcpServer.begin(); // Give a "I am alive" signal sendBroadCast(false); } } |
void socketServer(WiFiClient tcpClient)
1 2 3 4 5 6 |
// Handle new request on tcp socket server if available WiFiClient tcpClient = tcpServer.available(); if (tcpClient) { socketServer(tcpClient); digitalWrite(flashLED, LOW); } |
which checks if a client seeks a connection to our device. If a client is trying to connect, socketServer() is called with the link to the WiFiClient.
Inside socketServer() we then read all the bytes the client sends us into a buffer. Here I chose a quite small buffer, only 128 bytes, but this would be enough as the command that are received are quite short. We stop the reading if either
- all bytes are read (buffer is empty)
- our input buffer is full (should never happen)
- the connection was interrupted and we didn’t receive any more data for 3 seconds
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
/** socketServer answer request on tcp socket server returns status to client @param tcpClient Instance of WiFiClient that has connected * Commands: * t take a picture * d toggles debug output over serial port * x to reset the device * y=YYYY,MM,DD,HH,mm,ss to set time and date * z to format SPIFFS */ void socketServer(WiFiClient tcpClient) { comLedFlashStart(0.4); // Get data from client until he stops the connection or timeout occurs long timeoutStart = now(); char rcvd[128]; String cmd; byte index = 0; while (tcpClient.connected()) { if (tcpClient.available()) { rcvd[index] = tcpClient.read(); index++; if (index >= 128) break; // prevent buffer overflow } if (now() > timeoutStart + 3000) { // Wait a maximum of 3 seconds break; // End the while loop because of timeout } } rcvd[index] = 0; tcpClient.flush(); tcpClient.stop(); |
As the received commands are simple strings, I just convert the char array into a string. That makes the parsing of the command simpler.
If no data was received, we just flush the buffer and exit the function.
Otherwise we start parsing the received command. First we check if the command is “t”, which is the request to take a picture.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// Copy received buffer into a string for easier handling String req(rcvd); if (req.length() < 1) { // No data received comLedFlashStop(); tcpClient.flush(); tcpClient.stop(); return; } // Take a picture if (req.substring(0, 1) == "t"){ digitalWrite(flashLED, HIGH); bool picResult = takeShot(); // call takeShot to capture a picture if (picResult) { sendBroadCast(picResult); } |
During debugging of the app I used the Serial port for sending debug messages. To avoid sending debug messages all the time, I use a global flag bool debugOn to check if debug output is required or not. This flag is false (no debug output) by default, but can be toggled with the command “d” to enable or disable debug output.
1 2 3 4 5 6 7 8 |
// Switch on/off debug output } else if (req.substring(0, 1) == "d") { debugOn = !debugOn; if (debugOn) { Serial.println("Debug over TCP is on"); } else { Serial.println("Debug over TCP is off"); } |
Next command is to set the internal date and time of the device. Beside of using a NTP time sync, I wanted to have the option to manually set the date and time of a device in case there is no internet connection available.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
// Date/time received } else if (req.substring(0, 2) == "y=") { // initialize the variables int nowYear = 0; int nowMonth = 0; int nowDay = 0; int nowHour = 0; int nowMinute = 0; int nowSecond = 0; // Check the format of the received data if it is a valid date/time if (isDigit(req.charAt(2)) && isDigit(req.charAt(3)) && isDigit(req.charAt(4)) && isDigit(req.charAt(5)) && isDigit(req.charAt(7)) && isDigit(req.charAt(8)) && isDigit(req.charAt(10)) && isDigit(req.charAt(11)) && isDigit(req.charAt(13)) && isDigit(req.charAt(14)) && isDigit(req.charAt(16)) && isDigit(req.charAt(17)) && isDigit(req.charAt(19)) && isDigit(req.charAt(20))) { cmd = req.substring(2, 6); nowYear = cmd.toInt(); cmd = req.substring(7, 9); nowMonth = cmd.toInt(); cmd = req.substring(10, 12); nowDay = cmd.toInt(); cmd = req.substring(13, 15); nowHour = cmd.toInt(); cmd = req.substring(16, 18); nowMinute = cmd.toInt(); cmd = req.substring(19, 21); nowSecond = cmd.toInt(); if (debugOn) { String debugMsg = "Changed time to " + String(nowYear) + "-" + String(nowMonth) + "-" + String(nowDay) + " " + String(nowHour) + ":" + String(nowMinute) + ":" + String(nowSecond); Serial.println(debugMsg); } // Update the internal clock setTime(nowHour,nowMinute,nowSecond,nowDay,nowMonth,nowYear); gotTime = true; } else { String debugMsg = "Received wrong time format: " + req; Serial.println(debugMsg); } |
Another command used mainly during debugging is “z” which restarts the device.
1 2 3 4 5 6 7 8 9 |
// Reset device } else if (req.substring(0, 1) == "x") { Serial.println("Reset device"); tcpClient.flush(); tcpClient.stop(); // Reset the ESP delay(3000); ESP.reset(); delay(5000); |
And the last command is “z” which reformats the SPIFFS file system. After parsing the received command we return to the main loop
1 2 3 4 5 6 |
// Format SPIFFS } else if (req.substring(0, 1) == "z") { formatSPIFFS(OTA_HOST); } comLedFlashStop(); } |
The comLedFlashStart() and comLedFlashStop() commands will set one of the leds of the module to a fast flashing mode to indicate that data was received and is processed.
void sendBroadCast(boolean shotResult)
Every device in my home control system sends out a standard status message every one minute. This is to indicate that the device is still alive and to inform other devices about the location, IP address and status.
The status message is send as an UDP broadcast message over port 9997. UDP broadcast means that the message is not send to a specific IP address in the network, but that any device that listens on this port will receive the message.
The sendBroadCast() function can be split into 2 main parts.
The first part is the creation of the JSON object that is containing the status and will be sent out. The JSON object looks like
{“de”:”cm1″,”bo”:0,”tm”:”18:00″,”pi”:0}
- “de” indicates the device id, for the first camera in the system this is “cm1”
- “bo” indicates if this status is the first message sent after a restart of the device
- “tm” is the local time of the device
- “pi” indicates if a picture is available. 0 = no picture, 1 = picture available
- “fi” is only added if a picture is available and contains the filename of the picture
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
/** WiFiUDP class for creating UDP communication */ WiFiUDP udpClientServer; /** sendBroadCast send updated status over LAN by UTP broadcast over local lan @param shotResult Flag if taking an image was successful */ void sendBroadCast(boolean shotResult) { comLedFlashStart(0.4); DynamicJsonBuffer jsonBuffer; // Prepare json object for the response JsonObject& root = jsonBuffer.createObject(); root["de"] = DEVICE_ID; // Set flag for restart if (inSetup) { root["bo"] = 1; } else { root["bo"] = 0; } String nowTime = String(hour()) + ":"; if (minute() < 10) { nowTime += "0"; } nowTime += String(minute()); root["tm"] = nowTime; if (shotResult) { root["pi"] = 1; root["fi"] = String(filename); } else { root["pi"] = 0; } |
The second part of the function converts the JSON object into a string and sends it as a packet.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// Convert JSON object into a string String broadCast; root.printTo(broadCast); // Broadcast per UDP to LAN if (udpClientServer.beginPacketMulticast(multiIP, udpBcPort, ipAddr) == 0) { Serial.println("Start UDP broadcast failed. Maybe WiFi connection is lost"); } else { if (udpClientServer.print(broadCast) == 0) { Serial.println("Sending UDP broadcast failed. Maybe WiFi connection is lost"); } udpClientServer.endPacket(); udpClientServer.stop(); } comLedFlashStop(); } |
boolean takeShot()
Now its time to dig into the code that actually reads a picture from the camera, stores it in the local SPIFFS file system and sends it to the external FTP server.
Luckily I found a library and a good tutorial on Adafruit that made it easy to understand how the communication to the camera works, how a picture is taken and read from the camera.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
/** Take camera shot and save to FTP & SPIFFS @return <code>boolean true if photo was taken and saved to FTP & SPIFFS false if error occured */ boolean takeShot() { String debugMsg; wdt_reset(); if (debugOn) { Serial.println("takeShot started"); } comLedFlashStart(0.1); digitalWrite(flashLED, HIGH); wdt_reset(); if (!cam.takePicture()) { Serial.println("Failed to take picture!"); wdt_reset(); cam.resumeVideo(); comLedFlashStop(); digitalWrite(flashLED, LOW); return false; } |
Next we get the size of the picture and check if it has a reasonable length. Unfortunately the serial camera is not a very reliable device and on top the serial interface has no dataflow control. So it is always possible that something went wrong
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// Get the size of the image (frame) taken uint16_t jpglen = cam.frameLength(); if (debugOn) { debugMsg = "Got image with size " + String(jpglen); Serial.println(debugMsg); } wdt_reset(); if (jpglen <= 5000) { debugMsg = "Image size wrong: " + String(jpglen); Serial.println(debugMsg); cam.resumeVideo(); comLedFlashStop(); digitalWrite(flashLED, LOW); return false; } |
If everything seems to be ok, we create a filename for the camera which contains the date and time the picture was taken. For example the filename 02-17-18-12-45.JPG means the picture was taken on February 17th at 18:12 o’clock and 45 seconds. This filename is used when the picture is transfered to the FTP server.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// Create an image with the name MM-DD-hh-mm-ss.JPG String dateTime = getDigits(month()); dateTime += "-" + getDigits(day()); dateTime += "-" + getDigits(hour()); dateTime += "-" + getDigits(minute()); dateTime += "-" + getDigits(second()); for (int index=0; index < 14; index ++) { filename[index] = dateTime[index]; } filename[14] = '.'; filename[15] = 'j'; filename[16] = 'p'; filename[17] = 'g'; filename[18] = 0; if (debugOn) { debugMsg = "Saving " + String(filename) + " Image size: " + String(jpglen); Serial.println(debugMsg); } wdt_reset(); |
To save the picture in the local SPIFFS file system, we always use the filename last.jpg. The file system is small and cannot keep many files of the size of a picture. So we just overwrite the file every time a picture is shot.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// Prepare file to save image bool fileOpen = true; File imgFile = SPIFFS.open("/last.jpg", "w"); if (imgFile == 0) { fileOpen = false; Seral.println("Failed to open file /last.jpg" ); return false; // Without a file there is nothing to do here } // Switch off the flash light digitalWrite(flashLED, LOW); uint32_t bytesWrittenFS = 0; |
Now it is time to read the picture from the camera. The camera sends the picture data not in one continues string, but in chunks of 32 bytes. So we read in a loop all 32 byte chunks from the camera and write each chunk immediately into the file until we have the complete picture stored. Then we close the file and restart the camera to be ready for the next picture shot.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
// Read all the data up to jpglen # bytes! uint32_t startTime = millis(); uint32_t timeOut = millis(); // timout counter uint8_t bytesToRead; uint8_t readFailures = 0; while (jpglen > 0) { uint8_t *buffer; if (jpglen < 32) { bytesToRead = jpglen; } else { bytesToRead = 32; } wdt_reset(); buffer = cam.readPicture(bytesToRead); if (buffer == 0) { readFailures++; if (readFailures > 1000) { // Too many read errors, better to stop Serial.println("Read from camera failed"); jpglen = 0; if (fileOpen) { wdt_reset(); imgFile.close(); } return false; break; } } else { wdt_reset(); bytesWrittenFS += imgFile.write((const uint8_t *) buffer, bytesToRead); jpglen -= bytesToRead; } } if (fileOpen) { wdt_reset(); imgFile.close(); } uint32_t endTime = millis(); digitalWrite(flashLED, LOW); if (debugOn) { float transRate = ((float)bytesWrittenFS/1024)/((float)(endTime-startTime)/1000.0); debugMsg = "Read from camera finished: " + String(bytesWrittenFS) + " bytes in " + String(endTime-startTime) + " ms -> " + String(transRate) + " kB/s"; Serial.println(debugMsg); } // Restart camera wdt_reset(); cam.resumeVideo(); |
Now that we have the picture in the local file system we prepare to send it to the FTP server. First we open the file for reading, then we define the size of the packets we send to the FTP server. Her I use the MTU size that is defined in my router to avoid splitting of the packets on the router side.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
if (fileOpen) { // Prepare file to read image wdt_reset(); bool fileOpen = true; File imgFile = SPIFFS.open("/last.jpg", "r"); if (imgFile == 0) { fileOpen = false; Serial.println("Failed to open file /last.jpg"); } else { jpglen = imgFile.size(); uint32_t bytesReadFS = 0; #define bufSizeFTP 1440 // depends on available space uint8_t clientBuf[bufSizeFTP]; uint32_t bytesWrittenFTP = 0; |
Then we open the connection to the FTP server
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// Prepare FTP connection bool ftpConnected = true; if (debugOn) { Serial.println("Connecting to FTP"); } wdt_reset(); if (WiFi.isConnected()) { if (!ftpConnect()) { ftpConnected = false; Serial.println("Connecting to FTP failed"); } } else { ftpConnected = false; } |
Then we change the path to the folder on the server where the pictures should be stored and tell the FTP server that we want to send a file
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
if (ftpConnected) { // Prepare data upload ftpClient.println(F("CWD /var/www/html/1s")); // Check result wdt_reset(); if (!ftpReceive()) { debugMsg = "FTP: CD failed: " + String(ftpBuf); Serial.println(debugMsg); ftpDataClient.stop(); ftpClient.stop(); ftpConnected = false; } else { ftpClient.print(F("STOR ")); ftpClient.println(filename); // Check result wdt_reset(); if (!ftpReceive()) { debugMsg = "FTP: Passive mode not available: " + String(ftpBuf); Serial.println(debugMsg); ftpDataClient.stop(); ftpClient.stop(); ftpConnected = false; } } } |
Now we are ready to send the picture to the FTP server
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
if (ftpConnected) { startTime = millis(); // Get data from file and send to FTP for (int blocks = 0; blocks < ((jpglen/1440)+1); blocks++) { bytesReadFS = imgFile.read(clientBuf, 1440); bytesWrittenFTP += ftpDataClient.write((const uint8_t *) clientBuf, bytesReadFS); wdt_reset(); } endTime = millis(); imgFile.close(); if (debugOn) { float transRate = ((float)bytesWrittenFTP/1024)/((float)(endTime-startTime)/1000.0); debugMsg = "Save to FTP finished: " + String(bytesWrittenFTP) + " bytes in " + String(endTime-startTime) + " ms -> " + String(transRate) + " kB/s"; Serial.println(debugMsg); } |
After all data blocks are written, we close the connection to the FTP server.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// Close FTP connection wdt_reset(); ftpDataClient.stop(); ftpClient.println("QUIT"); // Check result wdt_reset(); if (!ftpReceive()) { debugMsg = "FTP: Disconnect failed: " + String(ftpBuf); Serial.println(debugMsg); } if (debugOn) { Serial.println("STOP FTP"); } ftpClient.stop(); } } } comLedFlashStop(); return true; } |
In the camera code we use two functions that I put into a separate file for easier maintenance, bool ftpConnect() and byte ftpReceive().
byte ftpReceive()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
//-------------- FTP receive /** Receive response from FTP server @return <code>byte Saves received data in outBuf returns 0 on fail on failure, 1 on success */ byte ftpReceive() { byte respCode; byte thisByte; while (!ftpClient.available()) delay(1); respCode = ftpClient.peek(); ftpCount = 0; while (ftpClient.available()) { thisByte = ftpClient.read(); if (ftpCount < 127) { ftpBuf[ftpCount] = thisByte; ftpCount++; ftpBuf[ftpCount] = 0; } } if (respCode >= '4') { ftpClient.println("QUIT"); while (!ftpClient.available()) delay(1); while (ftpClient.available()) { thisByte = ftpClient.read(); Serial.write(thisByte); } ftpClient.stop(); return 0; } return 1; } |
bool ftpConnect()
1 2 3 4 |
/** Username command for FTP server */ static const char* ftpUser = "USER _YOURUSERNAME_"; /** Password command for FTP server */ static const char* ftpPwd = "PASS _YOURPASSWORD_"; |
Replace _YOURUSERNAME_ and _YOURPASSWORD_ with the username and password for your FTP server.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
//-------------- FTP connect /** connect to FTP server @return <code>bool Saves received data in outBuf returns false on fail on failure, true on success */ bool ftpConnect() { // Open connection to FTP server if (!ftpClient.connect(ftpDataServer,ftpDataPort)) { return false; } // Check result if (!ftpReceive()) { ftpClient.stop(); return false; } // Send user name ftpClient.println(ftpUser); // Check result if (!ftpReceive()) { if (debugOn) { String debugMsg = "FTP: Wrong username: " + String(ftpBuf); Serial.println(debugMsg); } ftpClient.stop(); return false; } // Send password ftpClient.println(ftpPwd); // Check result if (!ftpReceive()) { if (debugOn) { String debugMsg = "FTP: Wrong password: " + String(ftpBuf); Serial.println(debugMsg); } ftpClient.stop(); return false; } // Set binary file mode ftpClient.println("TYPE I"); // Check result if (!ftpReceive()) { if (debugOn) { String debugMsg = "FTP: Binary file mode not available: " + String(ftpBuf); Serial.println(debugMsg); } ftpClient.stop(); return false; } // Set streaming mode transfer ftpClient.println("MODE S"); // Check result if (!ftpReceive()) { if (debugOn) { String debugMsg = "FTP: Streaming mode not available: " + String(ftpBuf); Serial.println(debugMsg); } ftpClient.stop(); return false; } // Request passive mode ftpClient.println("PASV"); // Check result if (!ftpReceive()) { if (debugOn) { String debugMsg = "FTP: Passive mode not available: " + String(ftpBuf); Serial.println(debugMsg); } ftpClient.stop(); return false; } // Check passive request response char *tStr = strtok(ftpBuf, "(,"); int array_pasv[6]; for ( int i = 0; i < 6; i++) { tStr = strtok(NULL, "(,"); array_pasv[i] = atoi(tStr); } // Get passive mode data port unsigned int hiPort, loPort; hiPort = array_pasv[4] << 8; loPort = array_pasv[5] & 255; hiPort = hiPort | loPort; if (!ftpDataClient.connect(ftpDataServer, hiPort)) { if (debugOn) { Serial.println("FTP: Data connection failed"); } ftpClient.stop(); return false; } return true; } |
That’s all. Hope you can find some useful code in this project.






