feat: implement WebRTC signaling and UDP-based video streaming and control service

This commit is contained in:
2026-07-16 14:55:14 +04:00
parent 53288ea670
commit 41eaf49bb2
11 changed files with 614 additions and 108 deletions
+94 -37
View File
@@ -1,6 +1,16 @@
#include "esp_camera.h"
#include <WiFi.h>
#include <WiFiUdp.h>
#include <ESP32Servo.h>
// === НАСТРОЙКА РЕЖИМА ЗАГЛУШКИ ===
const bool USE_MOCK_IF_NO_CAMERA = true; // true - слать черный экран, false - ничего не слать
Servo myServo;
const int servoPin = 13; // ПРИОРИТЕТ: Пин жестко закреплен за сервоприводом
// !!! УНИКАЛЬНЫЙ ID ДЛЯ КАЖДОЙ ПЛАТЫ
const uint8_t DEVICE_ID = 1;
// Конфигурация пинов (пример для Seeed Studio XIAO ESP32S3 Sense)
#define PWDN_GPIO_NUM -1
@@ -10,7 +20,7 @@
#define SIOC_GPIO_NUM 39
#define Y9_GPIO_NUM 48
#define Y8_GPIO_NUM 47
#define Y7_GPIO_NUM 13
#define Y7_GPIO_NUM 18 // ИЗМЕНЕНО: Отключаем этот пин для камеры, отдаем под Серво
#define Y6_GPIO_NUM 14
#define Y5_GPIO_NUM 12
#define Y4_GPIO_NUM 11
@@ -20,24 +30,45 @@
#define HREF_GPIO_NUM 38
#define PCLK_GPIO_NUM 37
const char* ssid = "RT-WiFi-1712";
const char* password = "yt3xihY2Ci";
const char* serverIP = "192.168.0.11";
const char* serverIP = "192.168.0.10";
const int serverPort = 9999;
const int localPort = 8888;
// !!! УНИКАЛЬНЫЙ ID ДЛЯ КАЖДОЙ ПЛАТЫ (например, 1 для первой, 2 для второй и т.д.)
const uint8_t DEVICE_ID = 1;
WiFiUDP udpVideo;
WiFiUDP udpCmd;
camera_config_t config;
// Флаг успешной инициализации камеры
bool isCameraInitialized = false;
// Моковый минимальный валидный JPEG черного цвета
const uint8_t mockBlackJpeg[] = {
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x01, 0x00, 0x60,
0x00, 0x60, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x0B, 0x08, 0x00, 0x08,
0x00, 0x08, 0x01, 0x01, 0x11, 0x00, 0xFF, 0xC4, 0x00, 0x14, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0xFF, 0xC4, 0x00, 0x14,
0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x3F, 0xFF, 0xD9
};
const size_t mockSize = sizeof(mockBlackJpeg);
void setup() {
Serial.begin(115200);
// Настройка камеры (QVGA для стабильности)
// Инициализируем сервопривод в первую очередь
myServo.attach(servoPin, 500, 2500);
Serial.println("Сервопривод инициализирован на пине 13");
// Настройка конфигурации камеры
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = Y2_GPIO_NUM;
@@ -45,7 +76,7 @@ void setup() {
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_d5 = Y7_GPIO_NUM; // Передаст -1, библиотека не тронет 13 пин
config.pin_d6 = Y8_GPIO_NUM;
config.pin_d7 = Y9_GPIO_NUM;
config.pin_xclk = XCLK_GPIO_NUM;
@@ -62,7 +93,15 @@ void setup() {
config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
config.fb_count = 2;
esp_camera_init(&config);
// Проверяем статус инициализации камеры
esp_err_t err = esp_camera_init(&config);
if (err == ESP_OK) {
isCameraInitialized = true;
Serial.println("Камера успешно инициализирована!");
} else {
isCameraInitialized = false;
Serial.printf("Камера не найдена (код 0x%x). Режим мок-данных: %s\n", err, USE_MOCK_IF_NO_CAMERA ? "ВКЛ" : "ВЫКЛ");
}
// Подключение к Wi-Fi
WiFi.begin(ssid, password);
@@ -79,43 +118,58 @@ void setup() {
void checkIncomingCommands() {
int packetSize = udpCmd.parsePacket();
if (packetSize > 0) {
char packetBuffer[255];
int len = udpCmd.read(packetBuffer, 255);
if (len > 0) { packetBuffer[len] = 0; }
String command = String(packetBuffer);
command.trim();
if (packetSize == 5) {
uint8_t packetBuffer[5];
int len = udpCmd.read(packetBuffer, 5);
if (len == 5) {
uint8_t steer = packetBuffer[0];
uint8_t throttle = packetBuffer[1];
uint8_t brake = packetBuffer[2];
uint8_t gear = packetBuffer[3];
uint8_t flags = packetBuffer[4];
Serial.print("Получена команда: ");
Serial.println(command);
// ИСПРАВЛЕНО: Инвертирован диапазон (180, 0 вместо 0, 180) для исправления направления
int servoAngle = map(steer, 0, 200, 180, 0);
myServo.write(servoAngle);
Serial.print("Получена команда: ");
Serial.println(command);
// Логика управления
if (command == "left") {
// Ваш код поворота налево
} else if (command == "right") {
// Ваш код поворота направо
} else if (command == "start") {
// Ваш код запуска моторов / логики
} else if (command == "stop") {
// Ваш код остановки
Serial.printf("Got control packet: Steer=%d (Angle=%d), Throttle=%d%%, Brake=%d%%, Gear=%d, Flags=0x%02X\n",
steer, servoAngle, throttle, brake, gear, flags);
}
} else if (packetSize > 0) {
char garbage[255];
udpCmd.read(garbage, sizeof(garbage));
}
}
void loop() {
checkIncomingCommands();
camera_fb_t * fb = esp_camera_fb_get();
if (!fb) return;
uint8_t * buf = nullptr;
size_t bufferSize = 0;
camera_fb_t * fb = nullptr;
size_t bufferSize = fb->len;
uint8_t * buf = fb->buf;
// Пытаемся получить кадр, если камера запущена
if (isCameraInitialized) {
fb = esp_camera_fb_get();
if (fb) {
bufferSize = fb->len;
buf = fb->buf;
}
}
// Если кадра с камеры нет
if (!buf) {
// Если отправка заглушки отключена — выходим из цикла, продолжая слушать команды
if (!USE_MOCK_IF_NO_CAMERA) {
delay(10);
return;
}
// Если включена — подставляем моковый черный кадр
bufferSize = mockSize;
buf = (uint8_t *)mockBlackJpeg;
}
// Размер куска данных уменьшаем до 1023, чтобы вместе с 1 байтом ID пакет был 1024 байта
// Нарезка и отправка по UDP (оригинальная логика)
size_t chunkSize = 1023;
uint8_t packetBuffer[1024];
@@ -125,15 +179,18 @@ void loop() {
sendSize = bufferSize - i;
}
// Формируем пакет: первый байт — ID, дальше — видеоданные
packetBuffer[0] = DEVICE_ID;
memcpy(packetBuffer + 1, buf + i, sendSize);
udpVideo.beginPacket(serverIP, serverPort);
udpVideo.write(packetBuffer, sendSize + 1); // Отправляем данные вместе с ID
udpVideo.write(packetBuffer, sendSize + 1);
udpVideo.endPacket();
}
esp_camera_fb_return(fb);
// Возвращаем буфер только если он реальный
if (fb) {
esp_camera_fb_return(fb);
}
delay(10);
}