Эта статья является частью проекта «Гусеничная платформа с камерой на ESP32-CAM»
Представляю вашему вниманию уникальное приложение для ESP32-CAM, которое открывает новые возможности видеонаблюдения и управления. После долгих поисков идеального решения для вывода видео с камеры ESP32-CAM AI-Thinker на смартфон и одновременного управления гусеничной или колёсной платформой, я наконец создал такое приложение и с радостью делюсь им с вами.

Скетч для ESP32-CAM создаёт на микроконтроллере WiFi точку доступа с именем carv. Для начала работы достаточно запустить приложение на смартфоне, нажать кнопку меню (в правом верхнем углу) и подключиться к WiFi сети микроконтроллера через настройки телефона.
Интерфейс приложения интуитивно понятен: при нажатии на кнопку "Видео" под элементами управления появляется потоковое видео с камеры. Управление осуществляется простыми командами, которые отправляются на сервер:
| Команда | Описание | Ссылка | |
|---|---|---|---|
| Left | Кнопка правого джойстика | http://host/left | |
| Right | Кнопка правого джойстика | http://host/right | |
| Up | Кнопка левого джойстика | http://host/up | |
| Down | Кнопка левого джойстика | http://host/down | |
| Fara | Управление фарами | http://host/fara | |
| Avar | Аварийная сигнализация | http://host/avar | |
| Sign | Сигнал | http://host/sign | |
| Dop1 | Дополнительная функция 1 | http://host/dop1 | |
| Dop2 | Дополнительная функция 2 | http://host/dop2 | |
| Reset1 | Центральная кнопка левого джойстика | http://host/reset1 | |
| Reset2 | Центральная кнопка правого джойстика | http://host/reset2 |
Видеопоток в реальном времени
| Назначение | Ссылка |
|---|---|
| Видеопоток | http://host:81/stream |
Микроконтроллер ESP32-CAM запускает два сервера: видеопоток на порту 81 и сервер приёма команд на стандартном порту 80. Представленный ниже тестовый скетч позволяет:
- Выводить видео на экран Android-смартфона
- Отображать команды в мониторе порта
- Управлять светодиодами микроконтроллера
#include <WiFi.h>
#include <WebServer.h>
#include "esp_camera.h"
// ==== Пины камеры для AI Thinker ====
#define PWDN_GPIO_NUM 32
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 0
#define SIOD_GPIO_NUM 26
#define SIOC_GPIO_NUM 27
#define Y9_GPIO_NUM 35
#define Y8_GPIO_NUM 34
#define Y7_GPIO_NUM 39
#define Y6_GPIO_NUM 36
#define Y5_GPIO_NUM 21
#define Y4_GPIO_NUM 19
#define Y3_GPIO_NUM 18
#define Y2_GPIO_NUM 5
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 23
#define PCLK_GPIO_NUM 22
#define LED1_GPIO 33
#define LED2_GPIO 4
WebServer commandServer(80); // Сервер команд
WiFiServer videoServer(81); // Сервер видеопотока
TaskHandle_t videoTaskHandle; // Задача видеопотока
bool led1_state = false;
bool led2_state = false;
// === Универсальный обработчик команд ===
void handleCommandGeneric(const String& name) {
Serial.println("Получена команда: " + name);
commandServer.send(200, "text/plain", "OK " + name);
}
// === Спец. команды с управлением светодиодами ===
void handleCommand1() {
led1_state = !led1_state;
digitalWrite(LED1_GPIO, led1_state);
Serial.println("Команда dop1: переключили GPIO 33");
commandServer.send(200, "text/plain", "OK dop1");
}
void handleCommand2() {
led2_state = !led2_state;
digitalWrite(LED2_GPIO, led2_state);
Serial.println("Команда dop2: переключили GPIO 4");
commandServer.send(200, "text/plain", "OK dop2");
}
// === Задача видеопотока на ядре 0 ===
void videoStreamTask(void *parameter) {
videoServer.begin();
Serial.println("Сервер видеопотока запущен на порту 81");
while (true) {
WiFiClient client = videoServer.available();
if (client) {
Serial.println("Клиент подключен к видеопотоку");
String response = "HTTP/1.1 200 OK\r\n";
response += "Content-Type: multipart/x-mixed-replace; boundary=frame\r\n\r\n";
client.print(response);
while (client.connected()) {
camera_fb_t * fb = esp_camera_fb_get();
if (!fb) {
Serial.println("Ошибка получения кадра");
break;
}
client.print("--frame\r\n");
client.print("Content-Type: image/jpeg\r\n\r\n");
client.write(fb->buf, fb->len);
client.print("\r\n");
esp_camera_fb_return(fb);
delay(50);
yield();
}
client.stop();
Serial.println("Клиент отключился от видеопотока");
}
delay(10);
}
}
// === Инициализация камеры ===
void setupCamera() {
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = Y2_GPIO_NUM;
config.pin_d1 = Y3_GPIO_NUM;
config.pin_d2 = Y4_GPIO_NUM;
config.pin_d3 = Y5_GPIO_NUM;
config.pin_d4 = Y6_GPIO_NUM;
config.pin_d5 = Y7_GPIO_NUM;
config.pin_d6 = Y8_GPIO_NUM;
config.pin_d7 = Y9_GPIO_NUM;
config.pin_xclk = XCLK_GPIO_NUM;
config.pin_pclk = PCLK_GPIO_NUM;
config.pin_vsync = VSYNC_GPIO_NUM;
config.pin_href = HREF_GPIO_NUM;
config.pin_sscb_sda = SIOD_GPIO_NUM;
config.pin_sscb_scl = SIOC_GPIO_NUM;
config.pin_pwdn = PWDN_GPIO_NUM;
config.pin_reset = RESET_GPIO_NUM;
config.xclk_freq_hz = 20000000;
config.pixel_format = PIXFORMAT_JPEG;
config.frame_size = FRAMESIZE_VGA;
config.jpeg_quality = 12;
config.fb_count = 1;
esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK) {
Serial.printf("Ошибка камеры: 0x%x\n", err);
}
}
// === Настройка ===
void setup() {
Serial.begin(115200);
pinMode(LED1_GPIO, OUTPUT);
pinMode(LED2_GPIO, OUTPUT);
digitalWrite(LED1_GPIO, LOW);
digitalWrite(LED2_GPIO, LOW);
IPAddress ip(192, 168, 4, 1);
IPAddress gateway(192, 168, 4, 1);
IPAddress subnet(255, 255, 255, 0);
WiFi.softAPConfig(ip, gateway, subnet);
WiFi.softAP("carv");
Serial.println("Точка доступа: carv");
Serial.print("IP: ");
Serial.println(WiFi.softAPIP());
setupCamera();
// Главная страница управления
commandServer.on("/", HTTP_GET, []() {
String html = "<html><body><h2>Управление ESP32-CAM</h2>";
html += "<p><a href='http://192.168.4.1:81/stream' target='_blank'>▶ Смотреть видео</a></p>";
const char* commands[] = {
"left", "right", "up", "down", "fara", "avar", "sign", "dop1", "dop2", "reset1", "reset2"
};
for (auto& cmd : commands) {
html += "<p><a href='/" + String(cmd) + "'>" + String(cmd) + "</a></p>";
}
html += "</body></html>";
commandServer.send(200, "text/html", html);
});
// Обработка команд
commandServer.on("/dop1", HTTP_GET, handleCommand1);
commandServer.on("/dop2", HTTP_GET, handleCommand2);
commandServer.on("/left", HTTP_GET, []() { handleCommandGeneric("left"); });
commandServer.on("/right", HTTP_GET, []() { handleCommandGeneric("right"); });
commandServer.on("/up", HTTP_GET, []() { handleCommandGeneric("up"); });
commandServer.on("/down", HTTP_GET, []() { handleCommandGeneric("down"); });
commandServer.on("/fara", HTTP_GET, []() { handleCommandGeneric("fara"); });
commandServer.on("/avar", HTTP_GET, []() { handleCommandGeneric("avar"); });
commandServer.on("/sign", HTTP_GET, []() { handleCommandGeneric("sign"); });
commandServer.on("/reset1", HTTP_GET, []() { handleCommandGeneric("reset1"); });
commandServer.on("/reset2", HTTP_GET, []() { handleCommandGeneric("reset2"); });
commandServer.begin();
Serial.println("Командный сервер запущен (порт 80)");
// Запуск видеопотока в отдельной задаче
xTaskCreatePinnedToCore(
videoStreamTask,
"VideoStream",
8192,
NULL,
1,
&videoTaskHandle,
0
);
}
// === Главный цикл ===
void loop() {
commandServer.handleClient();
}
Это базовая реализация, которую вы можете адаптировать под свои задачи. В ближайшее время я представлю доработанную версию для управления гусеничной платформой, включая STL-модели и электрическую схему. Следите за обновлениями!