Tech Tip — ESP8266 + VC0706

Jan 2018.  Send JPG over WiFi with the ESP8266 and a VC0706 protocol camera
With careful programming and some fine tuning, even the lowly ESP8266 can send captured JPG images over WiFi. The ESP8266 is a relatively restricted architecture to try to pull off gathering 640x480 jpg image data while simultaneously transmitting that data outward through WiFi. There is not enough memory to store the entire image, so simultaneous transmission is the only option. Following are guidelines that allow this to be done.

Change the communication rate to 230400. This will come as a shocker because not only is it widely reported that you cannot change the communication rate of a VC0706 protocol camera without bricking the device, but by extrapolating the VC0706 baud rate control parameters, doubling the documented 115200 maximum to 230400 is achievable! This allows a 640x480 image to be transferred every 5 seconds or so. Faster transmission than this seems to overload the ESP8266 WiFi capabilities, so this is a balanced setting.

Read a carefully selected number of bytes at a time and transmit using UDP. A cloud server on the receiving side assembles the UDP packets. UDP is not a guaranteed protocol; however this is mitigated by sending multiple images if and when required. It has been found that with a packet length of 992, a reliable connection, and well-programmed cloud servers, on the order of about 15% of the images fail because of a missing packet. This is impressive considering each image is comprised of hundreds of packets.

Control the camera motion sensitivity. Not a critical functional detail, but contrary to reports (again), careful experimentation has proven you can control the motion sensitivity.

Bounce the camera if communication is erroneous. If the ESP8266/camera communication breaks down, power down the camera and power it up again using an IO pin to a MOSFET supplying camera ground. While at it, bounce yourself (the ESP8266) with a diode connected from IO#16 to the RESET pin and a low applied to IO#16. Starting over is easy.

Following is pertinent code snippets demonstrating these key components.


// camera commands 					
const byte REQ_RESET[] = {0x56,0x00,0x26,0x00};
const byte REQ_VERSION[] = {0x56,0x00,0x11,0x00};
const byte REQ_RES_READ_EEP[] = {0x56,0x00,0x30,0x04,0x04,0x01,0x00,0x19};
const byte REQ_RES_320_240_EEP[] = {0x56,0x00,0x31,0x05,0x04,0x01,0x00,0x19,0x11};
const byte REQ_RES_640_480_EEP[] = {0x56,0x00,0x31,0x05,0x04,0x01,0x00,0x19,0x00};
const byte REQ_COMPRESS_READ[] = {0x56,0x00,0x30,0x04,0x01,0x01,0x12,0x04};
const byte REQ_COMPRESS_28[] = {0x56,0x00,0x31,0x05,0x01,0x01,0x12,0x04,0x28};
const byte REQ_JPG_STOP_BUFFER[] = {0x56,0x00,0x36,0x01,0x00};
const byte REQ_JPG_LEN[] = {0x56,0x00,0x34,0x01,0x00};
const byte REQ_JPG_FIRST_10[] = {0x56,0x00,0x32,0x0c,0x00,0x0A,0x00,0x00,0x00,0x00};
const byte REQ_JPG_START_BUFFER[] = {0x56,0x00,0x36,0x01,0x03};
const byte REQ_MOT_DET_ENABLE[] = {0x56,0x00,0x42,0x03,0x00,0x01,0x01};
const byte REQ_MOT_DET_DISABLE[] = {0x56,0x00,0x42,0x03,0x00,0x01,0x00};
const byte REQ_MOT_DET_START[] = {0x56,0x00,0x37,0x01,0x01};
const byte REQ_MOT_DET_STOP[] = {0x56,0x00,0x37,0x01,0x00};
const byte REQ_MOT_SENS_READ[] = {0x56,0x00,0x30,0x04,0x01,0x01,0x1A,0x6E};
const byte REQ_BAUD_UART_115200[] = {0x56,0x00,0x24,0x03,0x01,0x0D,0xA6};
const byte REQ_BAUD_UART_230400[] = {0x56,0x00,0x24,0x03,0x01,0x06,0x53};
const byte REQ_MOT_SENS_HIGH[] = {0x56,0x00,0x31,0x05,0x01,0x01,0x1A,0x6E,0x01};
const byte REQ_MOT_SENS_MID[] = {0x56,0x00,0x31,0x05,0x01,0x01,0x1A,0x6E,0x02};
const byte REQ_MOT_SENS_LOW[] = {0x56,0x00,0x31,0x05,0x01,0x01,0x1A,0x6E,0x03};
const byte REQ_MOT_SENS_OFF[] = {0x56,0x00,0x31,0x05,0x01,0x01,0x1A,0x6E,0xff}; 	

#define AES_BASIC_BYTES           16  // iv and key length
#define HEADER_BYTES              16  // main header and data header length
				        		
			


  
#include <ESP8266WiFiMulti.h>
#include <WiFiClient.h>
#include <WiFiClientSecure.h>
#include <WiFiServer.h>
#include <WiFiUdp.h>

#define RECEIVED_CMD_LEN          21
#define UDP_PACKET_LEN            992 // divisible by 16 for aes matching

#define SER_DEFAULT_BAUD          115200  // change this to the default baud rate your camera comes with
#define SER_TARGET_BAUD           230400
#define CMD_DELAY_MS              100     // 50 works. 10 works for commands but not for EEPROM writes
#define CMD_MOT_DETECTED          (0x39)  // asynchronously received when motion is detected

byte receivedCmd[RECEIVED_CMD_LEN];
byte udpPacket[UDP_PACKET_LEN];
int jpgNum;

// call from ESP8266 setup()
void camSetup() {
  Serial.begin(SER_DEFAULT_BAUD);
  delay( 100 ); 

  // camera EEPROM modifies requiring camera reset
  bool resetCam = false;
  camSendAndReceive( REQ_RES_READ_EEP );
  if( receivedCmd[5] != 0x00 ) {
    camSendAndReceive( REQ_RES_640_480_EEP );
    resetCam = true;
  } 
  if( resetCam ) {
    // send a software request to the camera to reset itself    
    camSendAndReceive( REQ_RESET ); 
    delay( 1000 );
  }
  
  // camera registry modifies
  
  // compression ratio - default 0x35
  // going below 0x020 or so causes the camera to freeze on one image
  camSendAndReceive( REQ_COMPRESS_READ );
  if( receivedCmd[5] != 0x28 )
    camSendAndReceive( REQ_COMPRESS_28 );
  
  // set the motion sensitivity
  camSendAndReceive( REQ_MOT_SENS_HIGH );
  //camSendAndReceive( REQ_MOT_SENS_MID );
  //camSendAndReceive( REQ_MOT_SENS_LOW );
  //camSendAndReceive( REQ_MOT_SENS_OFF );
       
  if( SER_DEFAULT_BAUD != SER_TARGET_BAUD ) {
    if( SER_TARGET_BAUD == 115200 )
      camSendAndReceive( REQ_BAUD_UART_115200 );
    else if( SER_TARGET_BAUD == 230400 )
      camSendAndReceive( REQ_BAUD_UART_230400 );
    Serial.begin( SER_TARGET_BAUD ); 
    delay( 100 ); 
  }
  
  camSendAndReceive( REQ_MOT_DET_ENABLE );
}

// returns on success
void camSendAndReceive( const byte data[] ) {
  camSerialDrain();
  camReceive( camSend( data ) );
}

// returns the sent cmd
byte camSend( const byte data[] ) {
  byte pos = 0;
  Serial.write(data[pos++]);
  Serial.write(data[pos++]);
  byte cmd = data[pos++];
  Serial.write(cmd);
  byte len = data[pos++];
  Serial.write(len);
  for( byte ix = 0; ix < len; ix++ )
    Serial.write(data[pos++]);
  delay( CMD_DELAY_MS );
  return cmd;
}

void camReceive( byte expectedCmd ) {
  byte receivedCmd = camReadResponse();
  // ignore any asynchronous and unwanted "motion detected" message 
  // which could get in the way
  while( receivedCmd != expectedCmd && receivedCmd == CMD_MOT_DETECTED )
    receivedCmd = camReadResponse();
  if( receivedCmd != expectedCmd )
    ledBlink( BLINKS_ERR_CAM_BADRSP_8 ); // resets everything and does not return
}

// returns success:cmd (and populates RECEIVED_CMD) fail:bounce
byte camReadResponse() {
  memset( receivedCmd, 0, RECEIVED_CMD_LEN);
  byte pos = 0;
  byte got;

  // protocol = 0x76
  got = receivedCmd[pos++] = camSerialRead();
  if( got != (byte)0x76 )
    ledBlink( BLINKS_ERR_CAM_BADRSP_8 ); // resets everything and does not return
    
  // serial number = 0x00
  got = receivedCmd[pos++] = camSerialRead();
  if( got != (byte)0x00 )
    ledBlink( BLINKS_ERR_CAM_BADRSP_8 ); // resets everything and does not return
  
  byte cmd = receivedCmd[pos++] = camSerialRead();
  byte statuz = receivedCmd[pos++] = camSerialRead();
  if( statuz != (byte)0x00 )
    ledBlink( BLINKS_ERR_CAM_BADRSP_8 ); // resets everything and does not return

  byte len = receivedCmd[pos++] = camSerialRead();
  if( len + pos > RECEIVED_CMD_LEN )
    ledBlink( BLINKS_ERR_CAM_BADRSP_8 ); // resets everything and does not return
  for( byte ix = 0; ix < len; ix++ )
    receivedCmd[pos++] = camSerialRead();

  return cmd;
}

// returns success:byte fail:bounce
byte camSerialRead() {
  int millisWaiting = 0;
  while( !Serial.available() ) {
    delay(1);
    millisWaiting += 1;
    if( millisWaiting > 2000 )
      ledBlink( BLINKS_ERR_CAM_NORSP_7 ); // resets everything and does not return
  }  
  return Serial.read();
}

void camSerialDrain() {
  while( true ) {
    delay(1);
    if( !Serial.available() )
      break;
    while( Serial.available() )
      Serial.read();
  }
}

// NOTE: This routine demonstrates a format of data specific to an implementation.
// NOTE: The format of your data will vary and must match the assembling server code.
// NOTE: AES encryption is also used. Those routines are not shown here. 
void camJpgToService() {
  camSendAndReceive( REQ_JPG_STOP_BUFFER );
  camSendAndReceive( REQ_JPG_LEN );
  byte lenComingBytes[4];
  lenComingBytes[0] = receivedCmd[5];
  lenComingBytes[1] = receivedCmd[6];
  lenComingBytes[2] = receivedCmd[7];
  lenComingBytes[3] = receivedCmd[8];
  int lenComing = ((receivedCmd[5] & 0xFF) << 24) + ((receivedCmd[6] & 0xFF) << 16) +
  	 ((receivedCmd[7] & 0xFF) << 8) + (receivedCmd[8] & 0xFF);

  jpgNum++;
  if( jpgNum == 1000 )
    jpgNum = 1;
  
  // the primary header is HEADER_BYTES=16 long
  // Not shown - fill the primary header with your applicaton specific data, including lenComing
  sprintf( (char*)udpPacket, "TODO", TODO );
  serviceSend( udpPacket, HEADER_BYTES );

  // the iv packet is HEADER_BYTES + AES_BASIC_BYTES = 32 long
  // Not shown - fill initial packet with your applicaton specific data
  sprintf( (char*)udpPacket, "TODO", TODO ); 
  for( int ix = 0; ix < 4; ix++ ) {
    long rand = RANDOM_REG32;
    memcpy( udpPacket + HEADER_BYTES + 4*ix, &rand, 4 );    
  }    
  aesSetIv( udpPacket + HEADER_BYTES );
  serviceSend( udpPacket, HEADER_BYTES + AES_BASIC_BYTES );  

  camReceive( camSendJpgRequest( lenComingBytes ) );
  
  int millisWaiting = 0, lenReceived = 0;
  int packetNum = 0;
  // each data header is HEADER_BYTES=16 long: 8 + 1 + 3 + 1 + 3
  // Not shown - fill each packet header with your applicaton specific data, including packetNum for reassembly
  sprintf( (char*)udpPacket, "TODO", TODO ); 
  int lenPacket = HEADER_BYTES;

  while( lenReceived < lenComing ) {
    if( Serial.available() ) {
      millisWaiting = 0;
      udpPacket[ lenPacket ] = Serial.read();
      lenReceived++;
      lenPacket++;
      if( lenPacket == UDP_PACKET_LEN ) {
        serviceSend( udpPacket, lenPacket );
        // each data header is HEADER_BYTES=16 long: 8 + 1 + 3 + 1 + 3
        // Not shown - fill each packet header with your applicaton specific data, including packetNum for reassembly
        sprintf( (char*)udpPacket, "TODO", TODO ); 
        lenPacket = HEADER_BYTES;
      }
    } else {
      delay(1);
      millisWaiting += 1;
      if( millisWaiting > 2000 )
        ledBlink( BLINKS_ERR_CAM_NORSP_7 ); // resets everything and does not return
    }
  }
  if( lenPacket > HEADER_BYTES ) {
    serviceSend( udpPacket, lenPacket );
  }
  
  camReceive( 0x32 ); // another identical jpg response comes from the camera
  camSendAndReceive( REQ_JPG_START_BUFFER );
}

// returns cmd=0x32
byte camSendJpgRequest( byte len[] ) {
  for(byte ix = 0; ix < 10; ix++ ) 
    Serial.write( REQ_JPG_FIRST_10[ix] );
  Serial.write( len[0] );
  Serial.write( len[1] );
  Serial.write( len[2] );
  Serial.write( len[3] );
  // num x 0.01ms = delay before and after jpg data
  // 1000 = 10ms
  Serial.write( (byte)0x03 );
  Serial.write( (byte)0xE8 );
  // no delay
  return 0x32;
}

void camReceiveMotionDetected() {
  camReceive( CMD_MOT_DETECTED );
}
			


        		
#include <ESP8266WiFiMulti.h>
#include <WiFiClient.h>
#include <WiFiClientSecure.h>
#include <WiFiServer.h>
#include <WiFiUdp.h>

WiFiUDP udp = WiFiUDP(); 

void serviceSend( byte * data, int len ) {
  udp.beginPacket( *serverIpCurrent, 444 ); // port 444 (for example)
  // The header (16 bytes) is not encrypted as well as any non 16 byte data
  // multiples. Only the last data packet can be a non 16 byte data multiple.
  if( !(len % AES_BASIC_BYTES) && len > HEADER_BYTES )
    aesEncrypt( data + HEADER_BYTES, len - HEADER_BYTES );
  udp.write( data, len );
  udp.endPacket();
}
			

© 2017 PixelTwenty LLC
info@pixeltwenty.com