Перевод статьи AI-Based No Parking Alert System using ESP32-CAMCircuit Digest.

Потребность в системах наблюдения растёт стремительно, и известных брендов с готовыми решениями хватает. Но можно ли расширить возможности модуля ESP32-CAM за $5 и собрать нечто похожее? В этом проекте мы строим систему оповещения о парковке: если кто-то встаёт перед воротами, ESP32-CAM распознаёт машину, отправляет уведомление в WhatsApp и включает зуммер со светодиодом.

Поскольку ESP32-CAM не потянет тяжёлые алгоритмы компьютерного зрения сам, все серьёзные вычисления вынесены в облако CircuitDigest Cloud.

Как это работает#

Блок-схема работы системы

Блок-схема работы системы оповещения

Первая часть — сам ESP32-CAM, который ищет движение, сравнивая последовательные кадры. Как только движение обнаружено, он снимает кадр и отправляет его в облачный API распознавания объектов, чтобы найти в кадре автомобиль. Если движения нет, цикл повторяется.

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

Обратите внимание: в отличие от привычных систем, здесь не нужны ни PIR-датчик, ни ультразвуковой, ни инфракрасный — камера сама сравнивает два соседних кадра, чтобы понять, было ли движение.

Что понадобится#

Компоненты проекта

Компоненты для сборки

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

Схема подключения#

Схема системы на ESP32-CAM

Схема системы оповещения на 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 для прошивки

Подключение 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

Страница API уведомлений WhatsApp

Для проекта нужен один реквизит из CircuitDigest Cloud — ключ API. Он даёт доступ сразу к двум сервисам: API распознавания объектов и API уведомлений WhatsApp.

Шаг 1. Войдите в CircuitDigest Cloud, перейдите в раздел WhatsApp Notification, нажмите Link a Number и подтвердите свой номер WhatsApp по одноразовому коду.

Шаг 2. Пролистайте вниз: в разделе Try API виден ваш ключ API. Скопируйте его и вставьте в код Arduino. Туда же впишите подтверждённый номер WhatsApp — с кодом страны в начале. Создавать шаблоны сообщений не нужно, для распознавания объектов и уведомлений используются уже готовые.

Копирование ключа API

Копируем ключ 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 с облачными сервисами даёт полноценную систему оповещения о парковке. Этот проект вполне может послужить отправной точкой для ваших будущих сборок.