Система оповещения о парковке на ESP32-CAM с распознаванием машин в облаке

Перевод статьи AI-Based No Parking Alert System using ESP32-CAM — Circuit Digest.
Потребность в системах наблюдения растёт стремительно, и известных брендов с готовыми решениями хватает. Но можно ли расширить возможности модуля ESP32-CAM за $5 и собрать нечто похожее? В этом проекте мы строим систему оповещения о парковке: если кто-то встаёт перед воротами, ESP32-CAM распознаёт машину, отправляет уведомление в WhatsApp и включает зуммер со светодиодом.
Поскольку ESP32-CAM не потянет тяжёлые алгоритмы компьютерного зрения сам, все серьёзные вычисления вынесены в облако CircuitDigest Cloud.
Как это работает#

Блок-схема работы системы оповещения
Первая часть — сам ESP32-CAM, который ищет движение, сравнивая последовательные кадры. Как только движение обнаружено, он снимает кадр и отправляет его в облачный API распознавания объектов, чтобы найти в кадре автомобиль. Если движения нет, цикл повторяется.
Допустим, движение зафиксировано и снимок ушёл в облако. Там ищут в изображении транспортные средства, и при обнаружении машины срабатывает API уведомлений WhatsApp — нам приходит сообщение со снимком. Заодно зуммер и светодиод, подключённые к ESP32-CAM, включаются на две секунды для локального оповещения: это происходит благодаря JSON-ответу, который облако возвращает плате.
Обратите внимание: в отличие от привычных систем, здесь не нужны ни PIR-датчик, ни ультразвуковой, ни инфракрасный — камера сама сравнивает два соседних кадра, чтобы понять, было ли движение.
Что понадобится#

Компоненты для сборки
| № | Компонент | Назначение | Кол-во |
|---|---|---|---|
| 1 | ESP32-CAM | Основной локальный процессор | 1 |
| 2 | Зуммер | Локальное оповещение | 1 |
| 3 | Светодиод 3/5 мм | Локальное оповещение | 1 |
| 4 | Держатель светодиода 5 мм | Крепление светодиода к картону | 1 |
| 5 | Пауэрбанк / аккумулятор | Питание схемы | 1 |
| 6 | Разъём питания / модуль TP4056 | Интерфейс питания для ESP32-CAM | 1 |
| 7 | Провода-перемычки | Монтаж | по потребности |
| 8 | Лист картона | Основание для сборки | 2 |
Схема подключения#

Схема системы оповещения на ESP32-CAM
Сначала соедините все компоненты по схеме. Питать схему можно любым обычным адаптером на 5 В или даже пауэрбанком — автор использовал пауэрбанк. Разъём питания подключается к выводам 5V и GND платы ESP32-CAM. Светодиод подключён к GPIO 2, зуммер — к GPIO 4 для локальной обратной связи.
Самое интересное здесь в том, что мы не используем никаких датчиков движения вроде ультразвукового или PIR. ESP32-CAM запрограммирован так, чтобы сравнивать последовательные кадры и искать отличия в цвете пикселей — а отличия означают движение.
Сборка#

Собранное устройство на картонном основании
Все компоненты закреплены на картоне толщиной 2,5 мм. Вся схема размещается между двумя такими листами, разнесёнными четырьмя стойками M3 длиной 40 мм с соответствующим крепежом. Питание — стандартный пауэрбанк на 10 000 мА·ч; кабель Type-C — micro USB соединяет его с ESP32-CAM через модуль TP4056, который здесь работает просто разъёмом. Зуммер приклеен к картону, а светодиод 3 мм установлен в держатель, чтобы выглядело аккуратно. На переднюю грань наклеили распечатку с надписями «ESP32 SMART SURVEILLANCE» и «NO PARKING IN FRONT OF GATE».
Прошивка ESP32-CAM#

Подключение USB-TTL для прошивки ESP32-CAM
У ESP32-CAM нет собственного USB-порта для прошивки, поэтому понадобится внешний программатор. В этом руководстве использован преобразователь USB-TTL. Главное — соединить VCC и GND преобразователя с выводами 5V и GND платы, а TX и RX преобразователя — с RX и TX платы соответственно. Если перепутать TX и RX местами, код не зальётся. Кроме того, на время прошивки нужно замкнуть GPIO 0 с GND, чтобы перевести модуль в режим загрузки.
Есть и другие способы: специальный программатор-подставка для ESP32-CAM (плата просто вставляется в него) или обычная Arduino UNO с обойдённым чипом ATmega328p — для этого RESET соединяют с GND, а RX и TX платы ESP32-CAM подключают к RX и TX UNO соответственно (здесь RX идёт к RX, а TX к TX, потому что внутренний микроконтроллер обойдён).
Получаем ключ CircuitDigest Cloud#

Страница API уведомлений WhatsApp
Для проекта нужен один реквизит из CircuitDigest Cloud — ключ API. Он даёт доступ сразу к двум сервисам: API распознавания объектов и API уведомлений WhatsApp.
Шаг 1. Войдите в CircuitDigest Cloud, перейдите в раздел WhatsApp Notification, нажмите Link a Number и подтвердите свой номер WhatsApp по одноразовому коду.
Шаг 2. Пролистайте вниз: в разделе Try API виден ваш ключ API. Скопируйте его и вставьте в код Arduino. Туда же впишите подтверждённый номер WhatsApp — с кодом страны в начале. Создавать шаблоны сообщений не нужно, для распознавания объектов и уведомлений используются уже готовые.

Копируем ключ API
Разбор кода#
Подключения, реквизиты и параметры настройки#
#include "esp_camera.h"
#include "esp_tls.h"
#include "esp_crt_bundle.h"
#include "esp_sntp.h"
#include <WiFi.h>
#include <time.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
const char* WIFI_SSID = "your_ssid";
const char* WIFI_PASS = "your_password";
const char* API_KEY = "your_api_key";
const char* PHONE = "91---------";
// ─── Параметры настройки ───────────────────────────────
#define PIXEL_DIFF_THRESHOLD 30
#define MOTION_THRESHOLD 350
#define UPLOAD_COOLDOWN_MS 10000UL
#define BUZZER_PIN 4 // GPIO 4
#define LED_PIN 2 // GPIO 2
Здесь собраны все нужные подключения библиотек, реквизиты и конфигурация выводов — эти значения нужно поправить под свою сборку. Важный параметр — MOTION_THRESHOLD, сейчас равный 350: им регулируется чувствительность к движению для съёмки кадра.
setupMotionBaseline() — управление опорным кадром#
static bool setupMotionBaseline()
{
camera_fb_t *fb = esp_camera_fb_get();
if (!fb) {
Serial.println("[CAM] Warning: Failed to acquire baseline frame. Retrying...");
return false;
}
motionFrameSize = fb->len;
if (previousFrame != NULL) {
free(previousFrame);
previousFrame = NULL;
}
previousFrame = (uint8_t *)malloc(motionFrameSize);
if (previousFrame) {
memcpy(previousFrame, fb->buf, motionFrameSize);
}
esp_camera_fb_return(fb);
return (previousFrame != NULL);
}
Движение определяется по разнице кадров, которая считается в loop(). Эта функция выступает механизмом управления опорным кадром для такой задачи.
Кроме того, в проекте есть низкоуровневые помощники для потоковой передачи (tlsWriteAll, tlsReadAll, openTLS). Они оборачивают работу с сокетом так, чтобы многокилобайтные фрагменты JPEG последовательно уходили в защищённый TLS-канал без потери пакетов, а ответ читался безопасно, с таймаутами. Функция detectVehicle() обращается к облачному API распознавания объектов, передавая список искомых классов (car, truck, bus, motorcycle) и порог уверенности 0,40, а sendWhatsApp() дёргает API уведомлений.
setup() — последовательность загрузки#
Функция setup() выполняется при старте устройства: настраивает выводы зуммера и светодиода как выходы и сбрасывает их в низкий уровень, подключает устройство к домашнему Wi-Fi и синхронизирует внутреннее время по серверам SNTP. Сетевая работа вынесена в отдельную задачу FreeRTOS, закреплённую за ядром 0.
loop() — обнаружение движения и съёмка#
void loop() {
if (previousFrame == NULL || motionFrameSize == 0) {
vTaskDelay(pdMS_TO_TICKS(100));
return;
}
camera_fb_t *fb = esp_camera_fb_get();
if (!fb) { vTaskDelay(pdMS_TO_TICKS(100)); return; }
int changedPixels = 0;
for (size_t i = 0; i < motionFrameSize; i += 8) {
int diff = abs((int)fb->buf[i] - (int)previousFrame[i]);
if (diff > PIXEL_DIFF_THRESHOLD) { changedPixels++; }
}
bool coolingDown = (millis() - lastUploadTime < UPLOAD_COOLDOWN_MS);
if (!coolingDown && (changedPixels > MOTION_THRESHOLD)) {
esp_camera_fb_return(fb);
Serial.printf("\n[Alert] Tripwire activated (%d). Locking scan engine.\n", changedPixels);
Serial.println("Holding 2 seconds for target stabilization...");
vTaskDelay(pdMS_TO_TICKS(2000));
if (setCameraSensingMode(PIXFORMAT_JPEG, FRAMESIZE_VGA, 10)) {
vTaskDelay(pdMS_TO_TICKS(600));
camera_fb_t* flushFb = esp_camera_fb_get();
if (flushFb) esp_camera_fb_return(flushFb);
camera_fb_t* captureFb = esp_camera_fb_get();
if (captureFb) {
g_imgLen = captureFb->len;
g_imgBuf = (uint8_t*)malloc(g_imgLen);
if (g_imgBuf) {
memcpy(g_imgBuf, captureFb->buf, g_imgLen);
esp_camera_fb_return(captureFb);
xSemaphoreGive(g_startSem);
xSemaphoreTake(g_doneSem, portMAX_DELAY);
return;
}
esp_camera_fb_return(captureFb);
}
}
setCameraSensingMode(PIXFORMAT_GRAYSCALE, FRAMESIZE_QQVGA, 12);
while (!setupMotionBaseline()) { vTaskDelay(pdMS_TO_TICKS(200)); }
} else {
if (motionFrameSize == fb->len) {
memcpy(previousFrame, fb->buf, motionFrameSize);
}
esp_camera_fb_return(fb);
vTaskDelay(pdMS_TO_TICKS(150));
}
}
Эта функция крутится бесконечно: снимает кадры и сравнивает их с порогом 350, проверяя каждый восьмой пиксель. Шаг в восемь пикселей снижает объём вычислений и заметно ускоряет работу кода. После срабатывания порога делается пауза в 2 секунды, чтобы машина успела остановиться, — её можно подстроить под свою ситуацию: задача в том, чтобы снять автомобиль уже вставшим у ворот. Для съёмки камера переключается в режим JPEG высокого разрешения.
Если что-то не работает#
| Проблема | Вероятная причина | Решение |
|---|---|---|
| В мониторе порта «Failed to acquire baseline frame» | Плохо сидит шлейф камеры или нестабильное питание | Переустановите шлейф камеры, используйте стабильный внешний источник 5 В |
| Движение обнаружено, но ИИ не находит машину | Автомобиль движется слишком быстро, слишком далеко от объектива или плохое освещение | Понизьте PIXEL_DIFF_THRESHOLD и MOTION_THRESHOLD, улучшите освещение, особенно ночью |
Частые вопросы#
Можно ли взять обычный пассивный зуммер вместо активного? Нет, рекомендуется активный: ему достаточно постоянного высокого уровня (3,3 В) на настроенном выводе GPIO.
Почему встроенный светодиод вспышки мигает вместе с внешним? На плате ESP32-CAM белый светодиод вспышки жёстко заведён на GPIO 2. Поскольку в коде LED_PIN назначен на GPIO 2 для внешнего светодиода, оба срабатывают одновременно — так задумано.
Можно ли управлять другими внешними устройствами, кроме светодиода и зуммера? Да. К свободным выводам GPIO платы можно подключать устройства, рассчитанные на 3,3 В.
Облако CircuitDigest бесплатное? Да, бесплатно доступно 100 запросов к каждому сервису в месяц с ограничением 15 запросов в сутки.
Можно ли питать схему от адаптера, а не от батареи или пауэрбанка? Да, главное — чтобы адаптер был на 5 В, и подключать его к выводам 5V и GND платы.
Заключение#
Здесь мы разобрали ESP32-CAM — версию ESP32 со встроенной камерой. Проект хорошо показывает, как устроены современные устройства, работающие в связке с облаком: компоненты доступны новичкам и продаются повсеместно, а сочетание локальной части на ESP32-CAM с облачными сервисами даёт полноценную систему оповещения о парковке. Этот проект вполне может послужить отправной точкой для ваших будущих сборок.