ESP8266 – CCTV still camera


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.


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.

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:

  1. Motion detection by the camera module
  2. Motion sensor connected to the ESP8266
  3. 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.




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

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

Next is to connect to a WiFi network. Usually I use here the WiFiManager library, but to keep the code simple I connect directly.

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.

Next step is to connect to the serial camera and initialize it.

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).

And here is the initialization of ArduinoOTA

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. 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.

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.



void socketServer(WiFiClient tcpClient)
The TCP socket server is a simplified version of a web server, but avoiding the additional overhead. Basically this server is listening on a defined port (in our case on port 9998) for incoming TCP connections.
In the main loop we have the code sequence

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

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.

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.

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.

Another command used mainly during debugging is “z” which restarts the device.

And the last command is “z” which reformats the SPIFFS file system. After parsing the received command we return to the main loop

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” 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

The second part of the function converts the JSON object into a string and sends it as a packet.

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.

The function takeShot() uses this library to access the camera. The camera was already initialized in the setup() function, so all we need to do is to trigger the camera shot and check if the camera was able to take the picture

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

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.

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.

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.

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.

Then we open the connection to the FTP server

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

Now we are ready to send the picture to the FTP server

After all data blocks are written, we close the connection to the FTP server.

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()
After each command sent to the FTP server the server sends a response. ftpReceive waits for this response, checks if it is an error message or an OK message and reports the result back. In case of an error the FTP connection is closed.

bool ftpConnect()
This function initializes the connection to the FTP server, sends the username and password to the server and tells the server that we want to send data in passive binary streaming mode. The username and password are stored in the wifiAPinfo.h file which I can not share. You will have to define the username and password for the FTP server as

Replace _YOURUSERNAME_ and _YOURPASSWORD_ with the username and password for your FTP server.

That’s all. Hope you can find some useful code in this project.



Leave a Reply

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

Free Link Directory