This is an old revision of the document!


Laborator 3: Captură și transmisie de imagini prin MQTT cu ESP32-CAM

În acest laborator ne vom familiariza cu platforma de dezvoltare ESP32-CAM și vom implementa un sistem de supraveghere simplu care transmite imagini către un calculator prin protocolul MQTT.

Obiective

  1. Înțelegerea arhitecturii unui sistem IoT bazat pe MQTT (Publisher/Subscriber).
  2. Configurarea mediului de dezvoltare (PlatformIO, Mosquitto, Python).
  3. Utilizarea ESP32-CAM pentru captură foto și transmisie WiFi.
  4. Controlul dispozitivului de la distanță prin comenzi MQTT.

Arhitectura sistemului

Sistemul este compus din trei entități principale:

  1. ESP32-CAM (Publisher/Subscriber):
    • Se conectează la rețeaua WiFi.
    • Publică imagini (JPEG) pe topicul ssproject/images.
    • Ascultă comenzi pe topicul ssproject/commands.
  2. Broker MQTT (Mosquitto):
    • Intermediarul care gestionează mesajele între cameră și calculator.
    • Rulează local pe calculatorul vostru.
  3. Client Python (Subscriber/Publisher):
    • Afișează imaginile primite în timp real.
    • Trimite comenzi către cameră (START-LIVE, STOP-LIVE, CAPTURE).

Configurare mediu de lucru

Pentru a rula proiectul, trebuie să pregătim infrastructura software.

1. Brokerul MQTT (Mosquitto)

Dacă nu aveți un broker instalat, descărcați Mosquitto.

Creați în rădăcina proiectului un fișier de configurare mosquitto.conf pentru a permite conexiuni externe (de la ESP32):

mosquitto.conf
listener 1883
allow_anonymous true

Porniți brokerul folosind acest fișier de configurare:

# Opriți serviciul default dacă rulează
sudo systemctl stop mosquitto.service
 
# Porniți brokerul cu configurarea noastră
/usr/sbin/mosquitto -c mosquitto.conf -v

2. Clientul Python (Receiver)

Aplicația de vizualizare (receiver.py) necesită Python și câteva biblioteci.

# Creați mediul virtual (dacă nu există)
python3 -m venv .venv
source .venv/bin/activate
 
# Instalați dependențele
pip install paho-mqtt opencv-python numpy

3. Crearea proiectului PlatformIO (Firmware ESP32-CAM)

Vom crea un proiect PlatformIO de la zero pentru plăcuța ESP32-CAM (AI-Thinker).

3.1 Instalare PlatformIO

Instalați extensia PlatformIO IDE din Visual Studio Code (Extensions → căutați “PlatformIO IDE” → Install).

3.2 Crearea proiectului

Puteți crea proiectul din interfața PlatformIO sau din terminal:

mkdir camera && cd camera
 
# Creați un mediu virtual Python și instalați PlatformIO CLI
python3 -m venv .venv
source .venv/bin/activate
pip install platformio
 
# Inițializați proiectul
pio project init --board esp32cam

3.3 Configurarea proiectului

Înlocuiți conținutul fișierului camera/platformio.ini cu următorul:

platformio.ini
; PlatformIO Project Configuration File
 
[env]
platform = espressif32
framework = arduino
lib_deps = 
    espressif/esp32-camera@^2.0.4
    WiFi @ ^2.0.0
    knolleary/PubSubClient @ ^2.8
monitor_speed = 115200
 
[env:esp32cam]
board = esp32cam
build_flags = -D CAMERA_MODEL_AI_THINKER
              -D MQTT_MAX_PACKET_SIZE=65000

Explicații:

  • espressif/esp32-camera — biblioteca oficială pentru modulul camerei.
  • knolleary/PubSubClient — client MQTT pentru Arduino.
  • CAMERA_MODEL_AI_THINKER — definește pinii GPIO corespunzători modelului AI-Thinker.
  • MQTT_MAX_PACKET_SIZE=65000 — mărește dimensiunea maximă a unui pachet MQTT (necesar pentru trimiterea imaginilor JPEG).

3.4 Definirea pinilor GPIO pentru cameră

Creați fișierul camera/include/camera_pins.hpp cu definițiile pinilor GPIO pentru modelul AI-Thinker:

camera_pins.hpp
#if defined(CAMERA_MODEL_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
 
#else
#error "Camera model not selected"
#endif

3.5 Structura finală

După acești pași, structura proiectului ar trebui să arate astfel:

camera/
├── include/
│   └── camera_pins.hpp     # Definițiile pinilor GPIO
├── src/
│   └── main.cpp            # Codul principal (de mai jos)
├── platformio.ini          # Configurarea proiectului
└── .venv/                  # Mediul virtual Python

3.6 Compilare și upload

# Compilare
pio run
 
# Upload pe placă (asigurați-vă că aveți permisiuni pe portul serial)
sudo chmod 666 /dev/ttyACM0   # sau /dev/ttyUSB0, depinde de placă
pio run -t upload
 
# Monitor serial (pentru debug)
pio device monitor -b 115200

Codul sursă

Firmware ESP32 (''camera/src/main.cpp'')

Acesta este codul care rulează pe cameră. Analizați-l cu atenție.

main.cpp
/**********************************************************************
  Filename    : Camera MQTT Client
  Description : ESP32-CAM MQTT Image Transfer
**********************************************************************/
#include "esp_camera.h"
#include <WiFi.h>
#include <PubSubClient.h>
// CAMERA_MODEL is defined in platformio.ini
#include "camera_pins.h"
 
// ===========================
// Configuration
// ===========================
const char* ssid     = "";       // TODO: Modificați cu SSID-ul rețelei voastre
const char* password = "";     // TODO: Modificați cu parola rețelei voastre
const char* mqtt_server = "10.10.10.10"; // TODO: Modificați cu IP-ul calculatorului (ip addr / ipconfig)
const int mqtt_port = 1883;
 
// Topics
const char* TOPIC_COMMAND = "ssproject/commands";
const char* TOPIC_IMAGE   = "ssproject/images";
 
WiFiClient espClient;
PubSubClient client(espClient);
 
// State variables
bool streaming = false;
bool take_one_picture = false;
unsigned long last_capture_time = 0;
const unsigned long STREAM_INTERVAL = 100; // ms
 
void setup_camera() {
  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_sccb_sda    = SIOD_GPIO_NUM;
  config.pin_sccb_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;
 
  if (psramFound()) {
    Serial.println("PSRAM found!");
    config.frame_size    = FRAMESIZE_VGA;
    config.jpeg_quality  = 12; 
    config.fb_count      = 2;
  } else {
    Serial.println("No PSRAM found, using DRAM");
    config.frame_size    = FRAMESIZE_SVGA;
    config.jpeg_quality  = 12; 
    config.fb_count      = 1;
    config.fb_location   = CAMERA_FB_IN_DRAM;
  }
 
  Serial.println("Initializing camera...");
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera init failed with error 0x%x\n", err);
    return;
  }
  Serial.println("Camera Ready!");
}
 
void callback(char* topic, byte* payload, unsigned int length) {
  Serial.println(">>> CALLBACK FIRED <<<");
  String message;
  for (int i = 0; i < length; i++) {
    message += (char)payload[i];
  }
  Serial.printf("Topic: %s\n", topic);
  Serial.printf("Message: [%s] (len=%u)\n", message.c_str(), length);
 
  if (String(topic) == TOPIC_COMMAND) {
    if (message == "CAPTURE") {
      take_one_picture = true;
      Serial.println("=> Action: take_one_picture = true");
    } else if (message == "START-LIVE") {
      streaming = true;
      Serial.println("=> Action: Streaming Started");
    } else if (message == "STOP-LIVE") {
      streaming = false;
      Serial.println("=> Action: Streaming Stopped");
    } else {
      Serial.println("=> Unknown command, ignoring");
    }
  } else {
    Serial.println("=> Wrong topic, ignoring");
  }
}
 
void reconnect() {
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    String clientId = "ESP32CamClient-";
    clientId += String(random(0xffff), HEX);
    Serial.printf(" (clientId=%s)\n", clientId.c_str());
 
    if (client.connect(clientId.c_str())) {
      Serial.println("MQTT connected!");
      bool subOk = client.subscribe(TOPIC_COMMAND);
      Serial.printf("Subscribe to '%s': %s\n", TOPIC_COMMAND, subOk ? "OK" : "FAILED");
      Serial.printf("Buffer size: %d\n", client.getBufferSize());
      Serial.printf("Free heap: %u bytes\n", ESP.getFreeHeap());
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      delay(5000);
    }
  }
}
 
void setup() {
  Serial.begin(115200);
  delay(1000);  // Give serial monitor time to connect
  Serial.println();
  Serial.println("============================");
  Serial.println("  ESP32-CAM MQTT Client");
  Serial.println("============================");
  Serial.printf("Free heap at start: %u bytes\n", ESP.getFreeHeap());
  Serial.printf("PSRAM size: %u bytes\n", ESP.getPsramSize());
 
  setup_camera();
 
  Serial.printf("Connecting to WiFi: %s\n", ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.printf("\nWiFi connected! IP: %s\n", WiFi.localIP().toString().c_str());
 
  client.setServer(mqtt_server, mqtt_port);
  client.setCallback(callback);
  client.setBufferSize(65000); 
}
 
void captureAndPublish() {
    camera_fb_t * fb = esp_camera_fb_get();
    if (!fb) {
        Serial.println("Camera capture failed");
        return;
    }
 
    if (client.publish(TOPIC_IMAGE, (const uint8_t*)fb->buf, fb->len)) {
        Serial.printf("Image published: %u bytes\n", fb->len);
    } else {
        Serial.println("Publish failed");
    }
 
    esp_camera_fb_return(fb);
}
 
unsigned long last_heartbeat = 0;
 
void loop() {
  if (!client.connected()) {
    Serial.println("MQTT disconnected, reconnecting...");
    reconnect();
  }
  client.loop();
 
  unsigned long now = millis();
 
  // Print a heartbeat every 5 seconds so you know the loop is running
  if (now - last_heartbeat > 5000) {
    Serial.printf("[heartbeat] millis=%lu connected=%d streaming=%d free_heap=%u\n",
                  now, client.connected(), streaming, ESP.getFreeHeap());
    last_heartbeat = now;
  }
 
  if (take_one_picture) {
    Serial.println("Taking single picture...");
    captureAndPublish();
    take_one_picture = false;
  }
 
  if (streaming && (now - last_capture_time > STREAM_INTERVAL)) {
    captureAndPublish();
    last_capture_time = now;
  }
}

Receiver Python (''receiver/receiver.py'')

Acesta este clientul care rulează pe calculator, primește imaginile și trimite comenzi.

receiver.py
import paho.mqtt.client as mqtt
import cv2
import numpy as np
import threading
 
# Configuration
BROKER = "192.168.50.239"  # TODO: Modificați cu IP-ul brokerului vostru
PORT = 1883
TOPIC_IMAGE = "ssproject/images"
TOPIC_COMMAND = "ssproject/commands"
 
# Global flags
running = True
 
# Shared frame storage (thread-safe via lock)
latest_frame = None
frame_lock = threading.Lock()
 
def on_connect(client, userdata, flags, rc):
    if rc == 0:
        print("Connected to MQTT Broker!")
        client.subscribe(TOPIC_IMAGE)
    else:
        print(f"Failed to connect, return code {rc}")
 
def on_message(client, userdata, msg):
    global latest_frame
    try:
        nparr = np.frombuffer(msg.payload, np.uint8)
        img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
 
        if img is not None:
            # Store the frame — do NOT call cv2.imshow here (wrong thread!)
            with frame_lock:
                latest_frame = img
        else:
            print("Failed to decode image")
    except Exception as e:
        print(f"Error processing image: {e}")
 
def main():
    global running, latest_frame
    client = mqtt.Client()
    client.on_connect = on_connect
    client.on_message = on_message
 
    try:
        client.connect(BROKER, PORT, 60)
        client.loop_start()
 
        print("\n--- ESP32 Camera Controller ---")
        print("Controls:")
        print(" 's' : Take Single Picture")
        print(" 'b' : Begin Stream")
        print(" 'e' : End Stream")
        print(" 'q' : Quit")
        print("-------------------------------\n")
 
        cv2.namedWindow("ESP32-CAM Stream", cv2.WINDOW_AUTOSIZE)
 
        while running:
            # Display the latest frame if available (main thread only)
            with frame_lock:
                frame = latest_frame
 
            if frame is not None:
                cv2.imshow("ESP32-CAM Stream", frame)
 
            # Handle GUI events and keyboard input (main thread only)
            key = cv2.waitKey(30) & 0xFF
 
            if key == ord('q'):
                running = False
            elif key == ord('s'):
                print("Command: Capture")
                client.publish(TOPIC_COMMAND, "CAPTURE")
            elif key == ord('b'):
                print("Command: Start Stream")
                client.publish(TOPIC_COMMAND, "START-LIVE")
            elif key == ord('e'):
                print("Command: Stop Stream")
                client.publish(TOPIC_COMMAND, "STOP-LIVE")
 
    except KeyboardInterrupt:
        print("\nExiting...")
    except Exception as e:
        print(f"Error: {e}")
    finally:
        client.loop_stop()
        client.disconnect()
        cv2.destroyAllWindows()
 
if __name__ == "__main__":
    main()

Structura codului

Firmware (''main.cpp'')

Codul de pe cameră îndeplinește următoarele funcții:

  • setup_camera(): Inițializează senzorul OV2640.
  • callback(): Funcția apelată când se primește un mesaj pe ssproject/commands. Interpretează:
    • CAPTURE → Face o poză.
    • START-LIVE → Activează flag-ul streaming.
    • STOP-LIVE → Dezactivează flag-ul streaming.
  • loop(): Verifică starea streaming. Dacă este activ, capturează și trimite o imagine la fiecare 100ms.

Configurare Wi-Fi și MQTT

În main.cpp, trebuie să modificați următoarele constante pentru a corespunde rețelei voastre:

const char* ssid     = "NUME_RETEA";
const char* password = "PAROLA_RETEA";
const char* mqtt_server = "IP_CALCULATOR"; // Rulați `ip addr` sau `ipconfig` pentru a-l afla

Receiver (''receiver.py'')

Scriptul Python se conectează la broker și afișează imaginile folosind OpenCV. De asemenea, ascultă tastatura pentru a trimite comenzi:

  • b: Begin Stream (START-LIVE)
  • e: End Stream (STOP-LIVE)
  • s: Single Capture (CAPTURE)
  • q: Quit

Desfășurarea laboratorului

  1. Configurare rețea: Deschideți camera/src/main.cpp și setați ssid, password și mqtt_server (IP-ul PC-ului vostru).
  2. Pornire broker: Rulați comanda de pornire Mosquitto într-un terminal.
  3. Pornire receiver: Rulați python receiver.py într-un alt terminal.
  4. Upload:
    • Conectați ESP32-CAM la USB (prin programator, dacă e cazul).
    • Asigurați-vă că aveți permisiuni pe portul serial: sudo chmod 666 /dev/ttyACM0.
    • Folosiți comanda Upload din PlatformIO sau terminal: pio run -t upload.
  5. Testare:
    • După resetare, camera se va conecta la WiFi (urmăriți Serial Monitor).
    • Din fereastra receiver-ului, apăsați b pentru a porni live stream-ul.
    • Verificați latența și calitatea imaginii.

Dacă ați configurat totul corect (broker-ul este pornit, camera publică pe ssproject/images, și receiver-ul sau aplicația web este conectată la același broker), ar trebui să puteți vizualiza stream-ul și prin intermediul interfaței web a aplicației (serverul web care face subscribe la același topic MQTT). Aceasta confirmă faptul că sistemul funcționează end-to-end.

Întrebări și exerciții

  1. Ce se întâmplă dacă modificați STREAM_INTERVAL în main.cpp la o valoare mai mică (ex: 20ms)? Cum afectează asta calitatea imaginii și latența?
  2. Analizați funcția callback din main.cpp. Adăugați o comandă nouă FLASH-ON care să aprindă LED-ul flash al camerei (GPIO 4).
ss/laboratoare/03.1773681434.txt.gz · Last modified: 2026/03/16 19:17 by ciprian.popescu0411
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0