Подключаем камеру 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>
)=====";
Ссылки
Файлы и библиотеки
- Библиотека arduinoWebSockets Библиотека GitHub
- OV7670 Архив