IoTLabs

Nghiên cứu, Sáng tạo và Thử nghiệm

Series ESP-WebRTC: Dự Án Video Doorbell — Chuông Cửa Video Với ESP32-P4

Dự án đầu tiên trong series: video doorbell — chuông cửa thông minh cho phép nhà chủ nhìn thấy và nói chuyện với khách từ bất cứ đâu, trực tiếp trên browser mà không cần cài app.

Tổng Quan Hệ Thống

[Camera ngoài cửa]
[Mic ngoài cửa]    → ESP32-P4 → Wi-Fi → MediaMTX → Browser (nhà chủ)
[Nút bấm chuông]                                  → Speaker (nhà chủ)
[Speaker ngoài cửa] ←─────────────────────────────────────

Khi khách nhấn nút, ESP32-P4 gửi WebRTC call → nhà chủ nhận trên browser → 2 chiều audio+video.

Danh Sách Linh Kiện

Linh KiệnModelGhi Chú
ESP32-P4 EV BoardEspressifBoard chính
CameraOV2640 DVPGóc rộng 160° nếu có
MicrophoneINMP441Chống nước nếu dùng ngoài trời
Speaker8Ω 2WGắn trong hộp
Nút bấmMomentary push buttonChống nước IP67
HộpHộp điện nhựa IP65Bảo vệ ngoài trời
Nguồn5V 2A adapterCấp từ trong nhà ra

Sơ Đồ Kết Nối

Nút bấm chuông  → GPIO_NUM_0 (INPUT_PULLUP)
LED trạng thái  → GPIO_NUM_48 (OUTPUT)
Camera DVP      → Xem bài 06
Mic INMP441     → Xem bài 06
Speaker I2S     → I2S_NUM_1 qua NS4168 amp

Code: Main Application

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "driver/gpio.h"
#include "esp_webrtc.h"
#include "esp_capture.h"
#include "av_render.h"
#include "wifi_helper.h"

#define BUTTON_PIN    GPIO_NUM_0
#define LED_PIN       GPIO_NUM_48
#define CALL_TIMEOUT  30000  // 30 giây không ai bắt máy → tắt

static esp_webrtc_handle_t g_webrtc;
static bool g_call_active = false;

// Callback khi có sự kiện WebRTC
static void on_webrtc_event(esp_webrtc_event_t *event, void *ctx) {
    switch (event->type) {
        case ESP_WEBRTC_EVENT_CONNECTED:
            g_call_active = true;
            gpio_set_level(LED_PIN, 1);  // LED sáng khi kết nối
            ESP_LOGI("doorbell", "Browser answered, streaming...");
            break;

        case ESP_WEBRTC_EVENT_DISCONNECTED:
            g_call_active = false;
            gpio_set_level(LED_PIN, 0);
            ESP_LOGI("doorbell", "Call ended");
            break;

        default:
            break;
    }
}

// Task theo dõi nút bấm chuông
static void button_task(void *arg) {
    gpio_config_t btn_cfg = {
        .pin_bit_mask = (1ULL << BUTTON_PIN),
        .mode         = GPIO_MODE_INPUT,
        .pull_up_en   = GPIO_PULLUP_ENABLE,
    };
    gpio_config(&btn_cfg);

    while (1) {
        // Nút bấm (active LOW)
        if (gpio_get_level(BUTTON_PIN) == 0 && !g_call_active) {
            ESP_LOGI("doorbell", "Button pressed — calling...");
            // Nhấp nháy LED trong khi đổ chuông
            for (int i = 0; i < 6; i++) {
                gpio_set_level(LED_PIN, i % 2);
                vTaskDelay(pdMS_TO_TICKS(300));
            }
            // Bắt đầu WebRTC call
            esp_webrtc_start_call(g_webrtc);

            // Timeout nếu không ai bắt máy
            vTaskDelay(pdMS_TO_TICKS(CALL_TIMEOUT));
            if (!g_call_active) {
                esp_webrtc_stop_call(g_webrtc);
                gpio_set_level(LED_PIN, 0);
                ESP_LOGI("doorbell", "No answer, call ended");
            }
        }
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

void app_main(void) {
    // LED output
    gpio_set_direction(LED_PIN, GPIO_MODE_OUTPUT);

    // Wi-Fi
    wifi_init_sta(CONFIG_WIFI_SSID, CONFIG_WIFI_PASS);
    wifi_wait_connected();

    // Capture pipeline (camera + mic)
    esp_capture_handle_t cap;
    init_capture(&cap);  // Hàm từ bài 06

    // AV Render (speaker + LCD nếu có)
    av_render_handle_t render;
    init_av_render(&render);

    // WebRTC
    esp_webrtc_cfg_t cfg = {
        .signaling = {
            .type     = ESP_WEBRTC_SIGNALING_WHIP,
            .whip_url = CONFIG_WHIP_URL,
        },
        .ice_servers = {
            { .url = "stun:stun.l.google.com:19302" },
            { .url = "turn:" CONFIG_TURN_SERVER, .username = CONFIG_TURN_USER, .credential = CONFIG_TURN_PASS },
        },
        .ice_server_num = 2,
        .audio_codec    = ESP_WEBRTC_AUDIO_CODEC_OPUS,
        .video_codec    = ESP_WEBRTC_VIDEO_CODEC_H264,
    };
    esp_webrtc_init(&cfg, &g_webrtc);
    esp_webrtc_set_av_source(g_webrtc, cap);
    esp_webrtc_set_av_render(g_webrtc, render);
    esp_webrtc_register_event_handler(g_webrtc, on_webrtc_event, NULL);

    // Bắt đầu giám sát nút bấm
    xTaskCreate(button_task, "button_task", 4096, NULL, 5, NULL);

    ESP_LOGI("doorbell", "Video doorbell ready");
}

Trang Web Nhận Cuộc Gọi

Nhà chủ mở trang web đơn giản trên điện thoại/laptop:

<!DOCTYPE html>
<html>
<head><title>Chuông Cửa</title></head>
<body>
<h2>Video Doorbell</h2>
<video id="remoteVideo" autoplay playsinline style="width:100%"></video>
<button onclick="answer()">Bắt Máy</button>
<button onclick="hangup()">Cúp Máy</button>

<script>
let pc;

// Kiểm tra có cuộc gọi đến không (polling đơn giản)
async function checkCall() {
  const resp = await fetch('http://SERVER/esp32-door/status');
  const { calling } = await resp.json();
  if (calling && !pc) {
    document.querySelector('button').style.animation = 'pulse 1s infinite';
    // Thông báo âm thanh
    new Audio('ring.mp3').play();
  }
}
setInterval(checkCall, 2000);

async function answer() {
  pc = new RTCPeerConnection({
    iceServers: [
      { urls: 'stun:stun.l.google.com:19302' },
      { urls: 'turn:YOUR_TURN_SERVER', username: 'esp32', credential: 'password123' }
    ]
  });

  pc.ontrack = e => document.getElementById('remoteVideo').srcObject = e.streams[0];
  pc.addTransceiver('video', { direction: 'recvonly' });
  pc.addTransceiver('audio', { direction: 'sendrecv' });  // 2 chiều audio

  const offer = await pc.createOffer();
  await pc.setLocalDescription(offer);

  const resp = await fetch('http://SERVER:8889/esp32-door/whep', {
    method: 'POST',
    headers: { 'Content-Type': 'application/sdp' },
    body: offer.sdp
  });
  const answer = await resp.text();
  await pc.setRemoteDescription({ type: 'answer', sdp: answer });
}

async function hangup() {
  pc?.close();
  pc = null;
}
</script>
</body>
</html>

📷 [Hình minh hoạ: Ảnh demo: browser hiển thị video từ camera cửa, nút Bắt Máy/Cúp Máy]

Cài Đặt Trong Hộp Chống Nước

Gợi ý lắp đặt thực tế:

  1. Camera: gắn góc cao ~2.2m, hướng xuống
  2. Mic: dùng mic có windscreen (bảo vệ gió), đặt gần nút bấm
  3. Speaker: đặt trong hộp, khoét lỗ nhỏ cho âm thanh
  4. ESP32-P4 board: trong hộp IP65, cách ẩm bằng silica gel
  5. Cấp nguồn: dây nguồn 5V từ trong nhà ra (không dùng pin)

Tiết Kiệm Điện Ở Chế Độ Chờ

Khi không có cuộc gọi, ESP32-P4 chạy ở light sleep — vẫn giữ Wi-Fi nhưng giảm CPU:

// Sau khi setup xong, vào light sleep
esp_pm_config_t pm_config = {
    .max_freq_mhz = 400,
    .min_freq_mhz = 40,   // CPU giảm xuống 40MHz khi idle
    .light_sleep_enable = true,
};
esp_pm_configure(&pm_config);

Tiêu thụ: ~30mA khi chờ (Wi-Fi connected, light sleep), ~300mA khi stream video.

Bài tiếp theo: Dự Án Two-Way Intercom — Gọi Video Hai Chiều ESP32 và Browser