Подключаем камеру OV7670 к ESP32

Общие сведения:

В этой статье рассматривается способ вывода картинки из камеры OV7670 подключённой к Wemos ESP32 OLED на холст веб-страницы.

В данном примерах будут использоваться сторонние библиотеки WebSocketClient и OV7670. Их необходимо скачать и установить. О том как это сделать можно прочитать в этой статье

Для компиляции скетча используются два файла: index.h и сам скетч. Файл index.h должен находиться в папке скетча. Текст файла index.h - это веб страница, на которой запускается протокол WebSocket при подключении нового клиента. При помощи этого протокола браузер клиента и скетч обмениваются данными.

После запуска скетча в монитор последовательного порта выводится ip-адрес страницы камеры. Необходимо открыть браузер на устройстве, подключенным к той же локальной сети, что и ESP32 и ввести этот адрес в адресной строке браузера.

В результате откроется веб-страница следующего вида с видеопотоком с камеры:






Подключение

Вывод камеры Вывод WEMOS ESP32
SDA 0 (должен быть отключён во время загрузки скетча)
SCL 13
VS 26
HS 25
XCLK 15
PCLK 36
D0 39
D1 16
D2 5
D3 4
D4 3
D5 2
D6 14
D7 12

Сектч

// Подключаем библиотеки
#include <OV7670.h>
#include <WebSockets.h>
#include <WebSocketsClient.h>
#include <WebSocketsServer.h>
#include <WiFi.h>
#include <WiFiClient.h>

// Подключаем файл страницы
#include "index.h"

// Данные точки доступа WiFi
const char *ssid = "название точки доступа";
const char *password = "пароль точки доступа";

// Определяем выводы, к которым подключена камера
const uint8_t camSDA = 0;
const uint8_t camSCL = 13;

const uint8_t VSYNC = 26;
const uint8_t HREF = 25;

const uint8_t XCLK = 15;
const uint8_t PCLK = 36;

const uint8_t D0 = 39;
const uint8_t D1 = 16;
const uint8_t D2 = 5;
const uint8_t D3 = 4;
const uint8_t D4 = 3;
const uint8_t D5 = 2;
const uint8_t D6 = 14;
const uint8_t D7 = 12;

// Создаём объект камеры
OV7670 camera(
        QQVGA_RGB565,
        camSDA, camSCL, VSYNC, HREF, XCLK,
        PCLK, D0, D1, D2, D3, D4, D5, D6, D7
         );

// Создаём объект веб сервера
WiFiServer server(80);

// Создаём константы обозначений начала и конца кадра
const unsigned char header = 0xAA;
const unsigned char footer = 0xFF;

// Флаг пакета с ip адресом камеры
unsigned char ip_flag = 0x11;

// Создаём сервер WebSocket
WebSocketsServer webSocket(81);

// Callback функция события WebSocket
void webSocketEvent(
        // номер канала
        uint8_t num,
        // тип события
        WStype_t type,
        // данные
        uint8_t *payload,
        // размер данных
        size_t payloadlength
            ) {

    int blk_count = 0;

    // Строки для переключения режима работы камеры
    static const char canvas_VGA[] = "canvas-VGA";
    static const char canvas_Q_VGA[] = "canvas-Q-VGA";
    static const char canvas_QQ_VGA[] = "canvas-QQ-VGA";
    static const char canvas_QQQ_VGA[] = "canvas-QQQ-VGA";

    // Строка для отправки ip адреса
    char ipaddr[26] ;
    IPAddress localip;

    // Если событие - отключение клиента
    if (type == WStype_DISCONNECTED) {
        // Выводим сообщение в последовательный порт
        Serial.printf("[%u] Disconnected!\n", num);
    }
    // Если событие - подключение клиента
    else if (type == WStype_CONNECTED) {
        // Создаём объект ip адреса
        IPAddress ip = webSocket.remoteIP(num);
        Serial.printf("[%u] Connected from %d.%d.%d.%d url: %s\n", num, ip[0], ip[1], ip[2], ip[3], payload);
        // Отправляем флаг начала передачи ip адреса камеры
        webSocket.sendBIN(0, &ip_flag, 1);
        // Записываем ip адрес камеры
        localip = WiFi.localIP();
        // Форматируем строку для отправки
        sprintf(ipaddr, "%d.%d.%d.%d", localip[0], localip[1], localip[2], localip[3]);
        // Отправляем строку
        webSocket.sendTXT(0, (const char *)ipaddr);

    }
    // Если событие - передача в текстовом режиме
    else if (type == WStype_TEXT) {
        // Преобразуем полученные данные в объект строки
        String query = String((char*)payload);

        // Если запрос QQQVGA
        if (query == canvas_QQQ_VGA) {
            Serial.println("canvas_QQQ_VGA");
            // Отправляем флаг конца передачи
            webSocket.sendBIN(0, &footer, 1);
            // Переключаем камеру в соответствующий режим
            camera.QQQVGARGB565();
        }
        // Если запрос QQVGA
        else if (query == canvas_QQ_VGA) {
            Serial.println("canvas_QQ_VGA");
            // Отправляем флаг конца передачи
            webSocket.sendBIN(0, &footer, 1);
            // Переключаем камеру в соответствующий режим
            camera.QQVGARGB565();
        }
        // Если запрос QVGA
        else if (query == canvas_Q_VGA) {
            Serial.println("canvas_Q_VGA");
            // Отправляем флаг конца передачи
            webSocket.sendBIN(0, &footer, 1);
            // Переключаем камеру в соответствующий режим
            camera.QVGARGB565();
        }
        // Если запрос VGA
        else if (query == canvas_VGA) {
            Serial.printf("canvas_VGA");
            // Отправляем флаг конца передачи
            webSocket.sendBIN(0, &footer, 1);
            // Переключаем камеру в соответствующий режим
            camera.VGARGB565();
        }


        // Вычисляем количество блоков
        blk_count = camera.yres/I2SCamera::blockSlice;

        // Проходим по всем блокам
        for (int i = 0; i < blk_count; i++) {

            // Если это первый блок
            if (i == 0) {
                // Устанавливаем соответствующие поля объекта камеры
                camera.startBlock = 1;
                camera.endBlock = I2SCamera::blockSlice;
                // Отправляем флаг начала передачи кадра
                webSocket.sendBIN(0, &header, 1);
            }

            // Если это последний блок
            if (i == blk_count - 1) {
                // Отправляем флаг конца передачи кадра
                webSocket.sendBIN(0, &footer, 1);
            }

            // Снимаем кадр
            camera.oneFrame();
            // Отправляем кадр
            webSocket.sendBIN(
                    0, camera.frame,
                    camera.xres * I2SCamera::blockSlice * 2
                    );
            // Вычисляем следующий блок
            camera.startBlock += I2SCamera::blockSlice;
            camera.endBlock   += I2SCamera::blockSlice;
        }
    }

    // Иначе
    else
        // Выводим сообщение в последовательный порт
        Serial.printf("WStype %x not handled \n", type);
}


void setup() {

    // Инициируем последовательный порт
    Serial.begin(115200);

    // Инициируем WiFi
    WiFi.begin(ssid, password);
    Serial.print("Connecting to WiFi");
    while (WiFi.status() != WL_CONNECTED) {
        delay(5000);
        Serial.print(".");
    }
    Serial.println();

    // Выводим адрес страницы
    Serial.print("Адрес страницы камеры: ");
    Serial.println(WiFi.localIP());

    // Инициируем WebSocket
    webSocket.begin();
    webSocket.onEvent(webSocketEvent);
    Serial.println("Запущен сервер WebSocket.");

    // Инициируем веб сервер
    server.begin();
    Serial.println("Запущен веб сервер.");
}

void loop()
{
    // Цикл WebSocket
    webSocket.loop();

    // Создаём объект клиента
    WiFiClient client = server.available();

    // Если есть клиенты
    if (client) {

        // Создаём пустую строку
        String currentLine = "";

        // Пока клиент подключён
        while (client.connected()) {

            // Если есть данные в буфере
            if (client.available()) {

                // Читаем следующий байт
                char c = client.read();

                // Если символ - новая строка
                if (c == '\n') {

                    // Если длина текущей строки нулевая - запрос закончен
                    if (currentLine.length() == 0) {

                        // Отвечаем клиенту
                        client.println("HTTP/1.1 200 OK");
                        client.println("Content-type:text/html");
                        client.println();

                        // Отправляем текст страницы камеры
                        client.print(indexhtml);
                        client.println();

                        // Выходим из цикла
                        break;
                    }
                    // Иначе, обнулим строку
                    else {
                        currentLine = "";
                    }
                }

                // Иначе, если не символ возврата каретки
                else if (c != '\r') {

                    // Добавляем символ к строке
                    currentLine += c;
                }

            }
        }
        // Закрываем соединение
        client.stop();
    }
}

Файл index.h

Должен находиться в одной папке со скетчем.

static const char indexhtml[] PROGMEM = R"=====(
<!-- Based on example made by Mudassar Tamboli -->

<!DOCTYPE html>
<html>
<title> ESP32-OV7670 - Demo</title>

<script type = "text/javascript">
    var ws = null;
    var r = 0;
    var capturecount = 1;
    var ln = 0;
    var flag = 0;
    var xres = 0;
    var yres = 0;
    var canvas;
    var ctx;
    var imgData;
    var gcanvasid = "canvas-QQ-VGA";
    var camera_ip = "192.168.4.1";

    function initWebSocket() {

        if ("WebSocket" in window) {
            if (ws != null) {
                ws.close();
            }

            ws = new WebSocket('ws://' +  camera_ip + ':81/', ['arduino']);
            if (ws == null) {
                document.getElementById("connecting").innerText = "Failed to connect to camera [ " + camera_ip + " ]";
                return;
            }
            ws.binaryType = 'arraybuffer';


            // open websocket
            ws.onopen = function() {
                document.getElementById("canvas7670").style.visibility = "visible";
                document.getElementById("connecting").style.visibility = "hidden";
                document.getElementById("constatus").innerText = "Connected to " + ws.url;
                if (gcanvasid != null && gcanvasid != "") {
                    capture(gcanvasid);
                }
            };//ws.onopen

            // receive message
            ws.onmessage = function (evt) {
                var arraybuffer = evt.data;
                if (arraybuffer.byteLength == 1) {
                    flag  = new Uint8Array(evt.data); // Start Flag
                    if (flag == 0xAA) {
                        ln = 0;
                    }
                    if (flag == 0xFF) {
                       //alert("Last Block");
                    }

                    if (flag == 0x11) {
                        //alert("Camera IP");
                    }

                } else {

                    if (flag == 0x11) {
                        //alert("Camera IP " + evt.data);
                        camera_ip = evt.data;
                        document.getElementById("wifi-ip").innerText = camera_ip;
                        flag = 0;
                    } else {
                        var bytearray = new Uint8Array(evt.data);
                        display(bytearray, arraybuffer.byteLength, flag);
                    }
                 }

               }; //ws.onmessage

            // close websocket
               ws.onclose = function() {
                   document.getElementById("canvas7670").style.visibility = "hidden";
                   document.getElementById("connecting").style.visibility = "visible";
               }; //ws.onclose

               // websocket error handling
               ws.onerror = function(evt) {
                   document.getElementById("canvas7670").style.visibility = "hidden";
                   document.getElementById("connecting").style.visibility = "visible";
                   document.getElementById("connecting").innerText = "Error " + evt.data;
                   document.getElementById("constatus").innerText = "";
               };

            } else {
            // The browser doesn't support WebSocket
                alert("WebSocket NOT supported by your Browser!");
            }
        } // WebSocketCamera


function onloadinit() {
   camera_ip = location.hostname;
   init();
}

function init() {
    document.getElementById("canvas7670").style.visibility = "hidden";
    document.getElementById("connecting").style.visibility = "visible";
    initCanvas();
    initWebSocket();
}

function initCanvas() {
    var canvas = document.getElementById("canvas-QQQ-VGA");
    var ctx = canvas.getContext("2d");
    ctx.font = "10px Comic Sans MS";
    ctx.fillStyle = "#FF0000";
    ctx.textAlign = "center";
    ctx.fillText("80 x 60", canvas.width/2, canvas.height/2);

    canvas = document.getElementById("canvas-QQ-VGA");
    ctx = canvas.getContext("2d");
    ctx.font = "10px Comic Sans MS";
    ctx.fillStyle = "#FF0000";
    ctx.textAlign = "center";
    ctx.fillText("160 x 120", canvas.width/2, canvas.height/2);

    canvas = document.getElementById("canvas-Q-VGA");
    ctx = canvas.getContext("2d");
    ctx.font = "10px Comic Sans MS";
    ctx.fillStyle = "#FF0000";
    ctx.textAlign = "center";
    ctx.fillText("320 x 240", canvas.width/2, canvas.height/2);

/*
    canvas = document.getElementById("canvas-VGA");
    ctx = canvas.getContext("2d");
    ctx.font = "10px Comic Sans MS";
    ctx.fillStyle = "#FF0000";
    ctx.textAlign = "center";
    ctx.fillText("640 x 480", canvas.width/2, canvas.height/2);
*/

}

//https://github.com/ThingPulse/minigrafx/issues/8
function display(pixels, pixelcount, flag) {
    var i = 0;
    for(y=0; y < yres; y++) {
       for(x=0; x < xres; x++)
       {
           i = (y * xres + x) << 1;
           pixel16 = (0xffff & pixels[i]) | ((0xffff & pixels[i+1]) << 8);
           imgData.data[ln+0] = ((((pixel16 >> 11) & 0x1F) * 527) + 23) >> 6;
           imgData.data[ln+1] = ((((pixel16 >> 5) & 0x3F) * 259) + 33) >> 6;
           imgData.data[ln+2] = (((pixel16 & 0x1F) * 527) + 23) >> 6;
           imgData.data[ln+3] = (0xFFFFFFFF) & 255;
           ln += 4;
       }
    }

    if (flag == 0xFF) { // last block
       ln = 0;
       ctx.putImageData(imgData,0,0);
       ws.send("next-frame");
    }

 }

function reset()
{
   r = 0;
   capturecount = 1;
   ln = 0;
   flag = 0;
   xres = 0;
   yres = 0;
   initCanvas();
}

function setip(whichip) {
    if (gcanvasid != null && gcanvasid != "") {
       camera_ip = document.getElementById(whichip).innerText;
       init();
       initCanvas();
       capture(gcanvasid);
    }
}

function capture(canvasid)
{
    if (ws.readyState != 1) {
    //    alert("ws.readyState " + ws.readyState);
        return;
    }

    reset();
    gcanvasid = canvasid;
    canvas = document.getElementById(canvasid);
    ctx = canvas.getContext('2d');
    if (canvasid.indexOf("canvas-VGA", 0) != -1) {
        xres = 640;
        yres = 60;
    } else if (canvasid.indexOf("canvas-Q-VGA", 0) != -1) {
        xres = 320;
        yres = 120;
    } else if (canvasid.indexOf("canvas-QQ-VGA", 0) != -1) {
        xres = 160;
        yres = 120;
    } else if (canvasid.indexOf("canvas-QQQ-VGA", 0) != -1) {
        xres = 80;
        yres = 60;
    }

    imgData = ctx.createImageData(canvas.width, canvas.height);
    for (var i=0;i<imgData.data.length;i+=4)
    {
        imgData.data[i+0]=0xCC;
        imgData.data[i+1]=0xCC;
        imgData.data[i+2]=0xCC;
        imgData.data[i+3]=255;
    }
    ctx.putImageData(imgData, canvas.width, canvas.height);
    ws.send(canvasid);
}

</script>

<body onload="onloadinit()">

<div valign=center align=center style="
    display: inline-block;
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    width: 200px;
    height: 100px;
    margin: auto;
    background-color: #FFFFFF;" id="connecting">Connecting Camera ...
</div>

<center> <h1> ESP32-OV7670 Websocket Video Camera </h1> </center>


<table align=center valign=center id="canvas7670">
    <tr>
        <td valign="bottom">
<canvas id="canvas-QQQ-VGA" width="80" height="60" style="cursor:crosshair;border:1px solid #FFFF00;" onclick="capture('canvas-QQQ-VGA')">
Your browser does not support the HTML5 canvas tag.
</canvas>
        </td>

        <td valign="bottom">
<canvas id="canvas-QQ-VGA" width="160" height="120" style="cursor:crosshair;border:1px solid #00FF00;" onclick="capture('canvas-QQ-VGA')">
Your browser does not support the HTML5 canvas tag.
</canvas>
        </td>

        <td valign="bottom">
<canvas id="canvas-Q-VGA" width="320" height="240" style="cursor:crosshair;border:1px solid #0000FF;" onclick="capture('canvas-Q-VGA')">
Your browser does not support the HTML5 canvas tag.
</canvas>
        </td>
<!--
        <td valign="bottom">
<canvas id="canvas-VGA" width="640" height="480" style="cursor:crosshair;border:1px solid #FF00FF;" onclick="capture('canvas-VGA')">
Your browser does not support the HTML5 canvas tag.
</canvas>
        </td>
-->
    </tr>

    <tr>
        <td align=center>
            QQQ-VGA     <BR>
        </td>

        <td align=center>
            QQ-VGA     <BR>
        </td>

        <td align=center>
            Q-VGA     <BR>
        </td>

<!--
        <td align=center>
            VGA     <BR>
        </td>
-->
    </tr>


</table>
<BR><BR>
<table width=30% align=center bgcolor="#FFFF00" >
 <tr align=center style="color: #fff; background: black;">
   <td id="constatus" colspan=2>Websocket not connected</td>
 </tr>

 <tr align=center style="color: #fff; background: black;">
   <!-- <td>AP IP </td> -->
   <td>WiFi IP</td>
   <td id="wifi-ip" style="cursor:pointer" onclick="setip('wifi-ip')"></td>
 </tr>
<!--
 <tr align=center bgcolor="#FFF">
   <td id="ap-ip" style="cursor:pointer" onclick="setip('ap-ip')">192.168.4.1</td>
 </tr>
-->
</table>


</body>
</html>
)=====";

Ссылки

Файлы и библиотеки