Taula de continguts

Placa de desenvolupament ESP32

El xip ESP32 de Espressif és molt popular per a integrar en dispositius IoT i sistemes «embedded» (incrustats o embeguts). Disposa de funcions de comunicacions necessàries com Wifi, Bluetooth, BLE (Bluetooth Low Emission), NFC i una capacitat de còmput moderada. I se li poden connectar dispositius com càmeres o d'altres.

L'exemple que treballarem farà una captura de foto amb la càmera i l'enviarà via BLE (Bluetooth Low Emission).

Aquest codi d'exemple està pensat per a enviar dades i que una aplicació Android la rebi, la podeu trobar a Android Bluetooth.


Placa de desenvolupament

Emprarem una placa de desenvolupament Freenove ESP32-S3-WROOM-1 (hi ha moltes variants).

Per fer-la funcionar es necessita Arduino IDE, i configurar-la amb el perfil «ESP32S3 Dev Module» i els paràmetres següents:


Codi C : captura amb càmera i enviament BLE

El pinatge de connexionat amb la càmera que hi ha al codi està configurat per a la placa Freenove esmentada. Encara que sigui el mateix processador, cada placa de desenvolupament té la seva pròpia implementació del hardware extern com la càmera.

esp32-cam-ble.ino
/*
 * ESP32-S3 FREENOVE: Captura foto amb botó BOOT i envia per BLE
 * Necessita la llibreria ESP32-Camera
 */
 
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>
#include <esp_camera.h>
#include <base64.h>  // Necessitaràs instal·lar aquesta llibreria
 
 
// Configuració dels pins de la càmera (ajusta segons la teva placa)
// Configuració típica per ESP32-S3 amb càmera OV2640
#define PWDN_GPIO_NUM     -1
#define RESET_GPIO_NUM    -1
 
#define SIOD_GPIO_NUM     4
#define SIOC_GPIO_NUM     5
#define VSYNC_GPIO_NUM    6
#define HREF_GPIO_NUM     7
#define XCLK_GPIO_NUM     15
#define PCLK_GPIO_NUM     13
 
#define Y9_GPIO_NUM       16
#define Y8_GPIO_NUM       17
#define Y7_GPIO_NUM       18
#define Y6_GPIO_NUM       12
#define Y5_GPIO_NUM       10
#define Y4_GPIO_NUM       8
#define Y3_GPIO_NUM       9
#define Y2_GPIO_NUM       11
 
#define LED_PIN           43     // LED per indicar captura
#define BOOT_BUTTON       0      // Botó BOOT
 
// UUIDs del servei BLE
#define SERVICE_UUID        "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"
 
 
// Variables globals
String deviceName = "ESP32-Camera-9";
BLEServer *pServer = NULL;
BLECharacteristic *pCharacteristic = NULL;
bool deviceConnected = false;
bool oldDeviceConnected = false;
bool isCapturing = false;
 
// Configuració de la càmera
camera_config_t config;
 
// Variables per al botó
bool lastButtonState = HIGH;
unsigned long lastDebounceTime = 0;
const unsigned long debounceDelay = 50;
 
// Callbacks BLE
class MyServerCallbacks: public BLEServerCallbacks {
    void onConnect(BLEServer* pServer) {
      deviceConnected = true;
      Serial.println("Dispositiu BLE connectat");
    };
 
    void onDisconnect(BLEServer* pServer) {
      deviceConnected = false;
      Serial.println("Dispositiu BLE desconnectat");
      // Reiniciem la publicitat per permetre noves connexions
      BLEDevice::startAdvertising();
    }
};
 
/*
 * Versió simplificada - Envia foto per BLE com a text base64
 */
 
void setup() {
  Serial.begin(115200);
  Serial.println("\n=== ESP32-S3 FREENOVE Camera BLE (Base64) ===");
 
  pinMode(LED_PIN, OUTPUT);
  pinMode(BOOT_BUTTON, INPUT_PULLUP);
 
  initBLE();
 
  if (!initCamera()) {
    Serial.println("Error camera!");
    while(1) delay(1000);
  }
 
  Serial.println("Sistema llest. Prem BOOT per foto!");
}
 
void loop() {
  // Gestió connexió BLE
  if (!deviceConnected && oldDeviceConnected) {
    delay(500);
    pServer->startAdvertising();
    oldDeviceConnected = deviceConnected;
  }
 
  if (deviceConnected && !oldDeviceConnected) {
    oldDeviceConnected = deviceConnected;
  }
 
  // Botó BOOT
  if (digitalRead(BOOT_BUTTON) == LOW && !isCapturing) {
    delay(50); // Debounce simple
    if (digitalRead(BOOT_BUTTON) == LOW) {
      takeAndSendPhotoBase64();
    }
  }
 
  delay(10);
}
 
// Funció per inicialitzar BLE
void initBLE() {
  BLEDevice::init(deviceName);
 
  pServer = BLEDevice::createServer();
  pServer->setCallbacks(new MyServerCallbacks());
 
  BLEService *pService = pServer->createService(SERVICE_UUID);
 
  pCharacteristic = pService->createCharacteristic(
                      CHARACTERISTIC_UUID,
                      BLECharacteristic::PROPERTY_READ |
                      BLECharacteristic::PROPERTY_WRITE |
                      BLECharacteristic::PROPERTY_NOTIFY |
                      BLECharacteristic::PROPERTY_INDICATE
                    );
 
  pService->start();
 
  // Configurar publicitat
  BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
  pAdvertising->addServiceUUID(SERVICE_UUID);
  pAdvertising->setScanResponse(true);
  pAdvertising->setMinPreferred(0x06);  // funcions que ajuden amb iPhone
  pAdvertising->setMinPreferred(0x12);
  BLEDevice::startAdvertising();
 
  Serial.println("BLE inicialitzat. Nom del dispositiu: ESP32-Camera");
  Serial.println("Connecta't des del teu dispositiu BLE");
}
 
// Funció per inicialitzar la càmera
bool initCamera() {
  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_sscb_sda = SIOD_GPIO_NUM;
  config.pin_sscb_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;
 
  // Qualitat de la imatge (ajusta segons la memòria disponible)
  config.frame_size = FRAMESIZE_SVGA;      // 800x600
  config.jpeg_quality = 10;                // 0-63 (més baix = millor qualitat)
  config.fb_count = 1;
 
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Error inicialitzant càmera: 0x%x", err);
    return false;
  }
 
  // Configuració addicional de la càmera
  sensor_t *s = esp_camera_sensor_get();
  if (s != NULL) {
    // Ajustos automàtics
    s->set_brightness(s, 0);     // -2 to 2
    s->set_contrast(s, 0);       // -2 to 2
    s->set_saturation(s, 0);     // -2 to 2
    s->set_special_effect(s, 0); // 0 to 6 (0 - No Effect)
    s->set_whitebal(s, 1);       // 0 = disable, 1 = enable
    s->set_awb_gain(s, 1);       // 0 = disable, 1 = enable
    s->set_wb_mode(s, 0);        // 0 to 4
    s->set_exposure_ctrl(s, 1);  // 0 = disable, 1 = enable
    s->set_aec2(s, 0);           // 0 = disable, 1 = enable
    s->set_ae_level(s, 0);       // -2 to 2
    s->set_aec_value(s, 1000);    // 0 to 1200
    s->set_gain_ctrl(s, 1);      // 0 = disable, 1 = enable
    s->set_agc_gain(s, 5);       // 0 to 30
    s->set_gainceiling(s, (gainceiling_t)6);  // 0 to 6
    s->set_bpc(s, 0);            // 0 = disable, 1 = enable
    s->set_wpc(s, 1);            // 0 = disable, 1 = enable
    s->set_raw_gma(s, 1);        // 0 = disable, 1 = enable
    s->set_lenc(s, 1);           // 0 = disable, 1 = enable
    s->set_hmirror(s, 0);        // 0 = disable, 1 = enable
    s->set_vflip(s, 1);          // 0 = disable, 1 = enable
    s->set_dcw(s, 1);            // 0 = disable, 1 = enable
    s->set_colorbar(s, 0);       // 0 = disable, 1 = enable
  }
 
  Serial.println("Càmera inicialitzada correctament");
  return true;
}
 
// Funció per capturar i enviar com base64
void takeAndSendPhotoBase64() {
  if (!deviceConnected) {
    Serial.println("Connecta primer un dispositiu BLE");
    return;
  }
 
  Serial.println("Capturant foto...");
  isCapturing = true;
  digitalWrite(LED_PIN, HIGH);
 
  // Capturar
  camera_fb_t *fb = esp_camera_fb_get();
  if (!fb) {
    Serial.println("Error captura");
    digitalWrite(LED_PIN, LOW);
    isCapturing = false;
    return;
  }
 
  Serial.printf("Foto: %d bytes\n", fb->len);
 
  // Convertir a base64
  String base64Image = base64::encode(fb->buf, fb->len);
  Serial.printf("Base64: %d caràcters\n", base64Image.length());
 
  // Enviar per BLE (fragmentat)
  sendBase64BLE(base64Image);
 
  // Alliberar
  esp_camera_fb_return(fb);
  digitalWrite(LED_PIN, LOW);
  isCapturing = false;
 
  Serial.println("Foto enviada!");
}
 
// Enviar base64 per BLE fragmentat
void sendBase64BLE(String &base64Data) {
  const int CHUNK_SIZE = 100;  // Caràcters per paquet
 
  // Enviar mida total
  uint32_t totalSize = base64Data.length();
  pCharacteristic->setValue((uint8_t*)&totalSize, sizeof(totalSize));
  pCharacteristic->notify();
  delay(20);
 
  // Enviar fragmentat
  for (size_t i = 0; i < base64Data.length(); i += CHUNK_SIZE) {
    String chunk = base64Data.substring(i, min(i + CHUNK_SIZE, base64Data.length()));
 
    pCharacteristic->setValue(chunk.c_str());
    pCharacteristic->notify();
 
    delay(20);  // Pausa per evitar sobrecàrrega
 
    if (i % 1000 == 0 || i + CHUNK_SIZE >= base64Data.length()) {
      int percent = (i * 100) / base64Data.length();
      Serial.printf("Enviant BLE: %d%%\n", percent);
    }
  }
 
  // Marcar final
  //pCharacteristic->setValue("END");
  //pCharacteristic->notify();
}