Differences

This shows you the differences between two versions of the page.

Link to this comparison view

ss:laboratoare:03 [2025/02/20 16:30]
127.0.0.1 external edit
ss:laboratoare:03 [2026/03/16 19:36] (current)
ciprian.popescu0411
Line 1: Line 1:
-===== Laboratorul 03. =====+====== 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 =====
 +
 +  - Înțelegerea arhitecturii unui sistem IoT bazat pe MQTT (Publisher/​Subscriber).
 +  - Configurarea mediului de dezvoltare (PlatformIO,​ Mosquitto, Python).
 +  - Utilizarea ESP32-CAM pentru captură foto și transmisie Wi-Fi.
 +  - Controlul dispozitivului de la distanță prin comenzi MQTT.
 +
 +===== Arhitectura sistemului =====
 +
 +Sistemul este compus din trei entități principale:
 +
 +  - **ESP32-CAM (Publisher/​Subscriber)**:​
 +    * Se conectează la rețeaua Wi-Fi.
 +    * Publică imagini (JPEG) pe topicul ''​ssproject/​images''​.
 +    * Ascultă comenzi pe topicul ''​ssproject/​commands''​.
 +  - **Broker MQTT (Mosquitto)**:​
 +    * Intermediarul care gestionează mesajele între cameră și calculator.
 +    * Rulează local pe calculatorul vostru.
 +  - **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 [[https://​mosquitto.org/​download/​|Mosquitto]].
 +
 +Creați în rădăcina proiectului un fișier de configurare ''​mosquitto.conf''​ pentru a permite conexiuni externe (de la ESP32):
 +
 +<file text mosquitto.conf>​
 +listener 1883
 +allow_anonymous true
 +</​file>​
 +
 +Porniți brokerul folosind acest fișier de configurare:​
 +
 +<code bash>
 +# Opriți serviciul default dacă rulează
 +sudo systemctl stop mosquitto.service
 +
 +# Porniți brokerul cu configurarea noastră
 +/​usr/​sbin/​mosquitto -c mosquitto.conf -v
 +</​code>​
 +
 +==== 2. Clientul Python (Receiver) ====
 +
 +Aplicația de vizualizare (''​receiver.py''​) necesită Python și câteva biblioteci.
 +
 +<code bash>
 +# 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
 +</​code>​
 +
 +==== 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:
 +
 +<code bash>
 +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
 +</​code>​
 +
 +=== 3.3 Configurarea proiectului ===
 +
 +Înlocuiți conținutul fișierului ''​camera/​platformio.ini''​ cu următorul:
 +
 +<file ini 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
 +</​file>​
 +
 +**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.h''​ cu definițiile pinilor GPIO pentru modelul AI-Thinker:
 +
 +<file c camera_pins.h>​
 +#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
 +</​file>​
 +
 +=== 3.5 Structura finală ===
 +
 +După acești pași, structura proiectului ar trebui să arate astfel:
 +
 +<code text>
 +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
 +</​code>​
 +
 +=== 3.6 Compilare și upload ===
 +
 +<code bash>
 +# 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
 +</​code>​
 +
 +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.
 +===== Codul sursă =====
 +
 +==== Firmware ESP32 (''​camera/​src/​main.cpp''​) ====
 +
 +Acesta este codul care rulează pe cameră. Analizați-l cu atenție.
 +
 +<file cpp 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);​
 +  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;
 +  }
 +}
 +</​file>​
 +
 +<note warning>​Nu deschideți ''​Serial Monitor''​ în Visual Studio Code, deoarece este foarte posibil să-și dea ''​RESET''​ placa.</​note>​
 +
 +==== Receiver Python (''​receiver/​receiver.py''​) ====
 +
 +Acesta este clientul care rulează pe calculator, primește imaginile și trimite comenzi.
 +
 +<file python 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()
 +</​file>​
 +
 +===== 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:
 +
 +<code cpp>
 +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
 +</​code>​
 +
 +==== 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 =====
 +
 +  - **Configurare rețea**: Deschideți ''​camera/​src/​main.cpp''​ și setați ''​ssid'',​ ''​password''​ și ''​mqtt_server''​ (IP-ul PC-ului vostru).
 +  - **Pornire broker**: Rulați comanda de pornire Mosquitto într-un terminal.
 +  - **Pornire receiver**: Rulați ''​python receiver.py''​ într-un alt terminal.
 +  - **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''​.
 +  - **Testare**:​
 +    * După resetare, camera se va conecta la Wi-Fi.
 +    * Din fereastra receiver-ului,​ apăsați ''​b''​ pentru a porni live stream-ul.
 +    * Verificați latența și calitatea imaginii.
 +
 +<note tip>
 +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.
 +</​note>​
 +
 +===== Întrebări și exerciții =====
 +
 +  - 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?
 +  - 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.1740061822.txt.gz · Last modified: 2025/02/25 22:15 (external edit)
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