This is an old revision of the document!
Î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.
Sistemul este compus din trei entități principale:
ssproject/images.ssproject/commands.START-LIVE, STOP-LIVE, CAPTURE).Pentru a rula proiectul, trebuie să pregătim infrastructura software.
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):
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
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
Vom crea un proiect PlatformIO de la zero pentru plăcuța ESP32-CAM (AI-Thinker).
Instalați extensia PlatformIO IDE din Visual Studio Code (Extensions → căutați “PlatformIO IDE” → Install).
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
Înlocuiți conținutul fișierului camera/platformio.ini cu următorul:
; 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).
Creați fișierul camera/include/camera_pins.h cu definițiile pinilor GPIO pentru modelul AI-Thinker:
#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
După acești pași, structura proiectului ar trebui să arate astfel:
camera/ ├── include/ │ └── camera_pins.h # Definițiile pinilor GPIO ├── src/ │ └── main.cpp # Codul principal (de mai jos) ├── platformio.ini # Configurarea proiectului └── .venv/ # Mediul virtual Python
# 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
De asemenea, în Visual Studio Code (cu extensia PlatformIO) aceleași acțiuni se pot face și din tastatură:
Ctrl + Alt + B – build (compilează proiectul)Ctrl + Alt + U – upload pe placăAstfel nu mai este necesar să rulați comenzile manual în terminal.
Acesta este codul care rulează pe cameră. Analizați-l cu atenție.
/********************************************************************** 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; } }
Serial Monitor în Visual Studio Code, deoarece este foarte posibil să-și dea RESET placa.
Acesta este clientul care rulează pe calculator, primește imaginile și trimite comenzi.
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()
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.
Î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
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: Quitcamera/src/main.cpp și setați ssid, password și mqtt_server (IP-ul PC-ului vostru).python receiver.py într-un alt terminal.sudo chmod 666 /dev/ttyACM0.Upload din PlatformIO sau terminal: pio run -t upload.b pentru a porni live stream-ul.
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.
STREAM_INTERVAL în main.cpp la o valoare mai mică (ex: 20ms)? Cum afectează asta calitatea imaginii și latența?callback din main.cpp. Adăugați o comandă nouă FLASH-ON care să aprindă LED-ul flash al camerei (GPIO 4).