Fyzikální kabinet GymKT

Články IP Wi-Fi kamera ,,za hubičku''

IP Wi-Fi kamera doslova „za hubičku“

Motivace k dále uvedenému modulu kamery

V naší Vzdálené internetové laboratoři jsme začali postupně opouštět dosavadní vzdálené řízení pomocí řídicího PC a začali jsme tento plnohodnotný počítač nahrazovat řídicími moduly, jako je kupříkladu modul Arduino. Ukázalo se však, že i přes tuto snahu je u vzdálené úlohy potřeba klasický počítač. Důvodem není vzdálené řízení experimentu, ale zpracování obrazu z webové kamery, která úlohu sleduje. Pochopitelně jsme začali hledat nějakou možnost, jak původní USB webovou kameru nahradit IP kamerou obdobných vlastností.

V tomto případě profesionální IP kamery oplývají pro naše potřeby zbytečně rozsáhlým množstvím funkcí, jako je vzdálené ostření a otáčení směru pozorování… Navíc jejich nákup je pro nás dosti nákladný. Proč plýtvat prostředky, které se jinak dají použít na další rozvoj laboratoře v podobě tvorby dalších úloh?

Naším řešením tedy bylo vytvořit jednoduchý a levný kamerový modul, ze kterého bude možné načítat sérii obrázků (cca 10 fps) nebo obrazový stream. Tento modul by se měl dát připojit do dosavadní školní sítě – buď LAN nebo Wi-Fi a obrazový výstup by měl být přístupný přes klasický http protokol. Kamerový modul nemusí zajišťovat přenos zvuku.

Toto zadání se nám nakonec podařilo vyřešit pomocí modulu ESP32-CAM, který nás oslovil jak svými možnostmi (max. rozlišení 2 Mpx, vestavěný Wi-Fi modul, ale třeba i možnost rozpoznání tváře…), tak především cenou (200–350 Kč dle prodejce).

Představujeme modul AI Thinker modulu kamery ESP32-CAM
To je to, co si cca za 200 Kč koupíte.

Streamování videa pomocí ESP32-CAM

V následujícím článku si ukážeme základní použití modulu ESP32-CAM v roli jednoduché WI-FI kamery (IP kamery). Řešení této kamery je založené na technice streamování videa z kamery a jeho poskytování do zvolené Wi-Fi sítě prostřednictvím jednoduchého webového serveru.

První část článku přestavuje použitý modul ESP32-CAM a kameru OV2640 a především ukazuje, jak nastavit prostředí Arduino IDE pro vytváření a kompilování programu pro modul  ESP32-CAM. Stejně tak je zde ukázáno, jak propojit modul ESP32-CAM s potřebným FTDI programátorem.

Pokud tyto dovednosti již máte, přejděte rovnou až na další část článku, která je představuje námi prezentovaný program pro Wi-Fi kameru.

Představujeme ESP32-CAM

ESP32-CAM je velmi malý řídicí modul, který lze na českém trhu zakoupit společně s modulem mikroskopické kamery OV2640 v cenové relaci asi 200–300 Kč. Modul ESP32-CAM kromě speciálního konektoru pro připojení fotoaparátu OV2640 disponuje několika piny GPIO pro připojení různých periferních a také obsahuje slot pro microSD kartu. Ta se může kupříkladu použít pro ukládání snímků pořízených fotoaparátem nebo pro uložení souborů vhodných pro provoz webového serveru.

ESP32-CAM pins
Modul ESP32-CAM – rozložení pinů

Souhrn základních parametrů modulu ESP32-CAM:

  • Integrovaný malý modul Wi-Fi BT SoC 802.11b/g/n
  • úsporný 32-bitový procesor
  • rychlost taktování až 160 MHz, souhrnný výpočetní výkon až 600 DMIPS
  • integrovaná 520 KB SRAM, externí 4M PSRAM
  • podpora UART/SPI/I2C/PWM/ADC/DAC
  • podpora kamer OV2640 a OV7670 (vestavěná přisvětlovací LED pro blesk)
  • Podpora karty TF
  • Podpora více režimů spánku
  • Integrované Lwip a FreeRTOS
  • Podpora provozní režim STA/AP/ STA + AP
  • Podpora technologie Smart Config/AirKiss
  • Podpora pro místní a vzdálený upgrade firmwaru (FOTA) sériového portu

Pro napájení modulu slouží piny: buď 3,3 V nebo 5 V a GND.

Piny GPIO 1 a GPIO 3 jsou sériové piny. Tyto piny potřebujeme při nahrání kódu programu do modulu. Při programování modulu bude hrát důležitou roli i pin GPIO 0, protože určuje, zda je modul ESP32-CAM v programovacím režimu nebo ne. Když je pin GPIO 0 připojen k GND, lze modul ESP32-CAM programovat (viz dále).

Čtečka paměťových microSD karet je interně připojena na následující piny:

  • GPIO 14 – CLK
  • GPIO 15 – CMD
  • GPIO 2 – Data 0
  • GPIO 4 – Data 1
  • GPIO 12 – Data 2
  • GPIO 13 – Data 3

Kamerový modul OV2640

Výrobce OmniVision jako první na světě sestrojil 2 megapixelový senzor o průměru 0,635 mm. Výrobce dále uvádí vysokou citlivost pro použití v nízkém osvětlení, vestavěný kompresní systém, který podporuje většinu běžně používaných formátů a široké možnosti nastavení kvality obrazu.

Modul kamery OV2640
Modul kamery OV2640

Souhrn základních parametrů modulu OV2640

  • Velikost zobrazované matice: UXGA (1600×1200)
  • Průměr čočky: 0,635 mm
  • Maximální rychlost přenosu obrazu – UXGA/SXGA: 15 fps
  • SVGA: 20 fps
  • CIF: 60 fps

Deska ESP32-CAM je ke kamerovému modulu OV2640 připojena pomocí série pinů ve speciálním konektoru, což je nutné v kódu řádně definovat (viz dále). ESP32-CAM podporuje i kamerový modul OV7670, nicméně modul OV2640 je k ní rovnou standardně dodáván.

Programování modulu ESP32-CAM

Pro programování modulu ESP32-CAM budeme využívat prostředí Arduino IDE. Nyní si ukážeme, jak toto prostředí připravíme pro použití s modulem ESP32-CAM.

Příprava prostředí Arduino IDE pro použití k ESP32-CAM

Po stažení a instalaci prostředí Arduino IDE musíme do tohoto prostředí zadat adresu URL, ze které se budou stahovat potřebné informace pro kompilování zdrojového programu pro modul ESP32.

To uděláme v menu: SOUBOR → Vlastnosti, kde do pole Správce dalších desek zadáme jednu z následujících URL (popř. obě):

  • https://dl.espressif.com/dl/package_esp32_index.json
  • https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json

nastavení IDE Arduino

Poté je třeba zvolit modul ESP32-CAM v manažeru desek prostředí Arduino IDE: Nástroje → Vývojová deska → Manažér Desek…

manažer desek

V manažeru desek vyhledáme desku ESP (stačí napsat do vyhledávacího pole) a v na nabídnuté položce klikneme na tlačítko instalovat.

Instalace desky ESP32

A nyní konečně nastavíme prostředí Arduino IDE tak, aby při kompilaci byl vytvářen kód pro modul ESP32-CAM. Ve volbě Nástroje → Vývojová deska vybereme rodinu desek ESP32 a pak modul AI Thinker ESP32-CAM (viz obr.).

volba AI Thinker ESP32-CAM

Máme hotovo!

Nyní budeme psát a kompilovat kódy pro modul ESP32-CAM

Upload kódu do modulu ESP32-CAM

Určitou slabinou modulu ESP32-CAM je to, že není osazen tradičním konektorem USB, takže při jeho programování budete potřebovat FTDI programátor. Propojení programátoru s modulem ESP32-CAM vidíme na následujícím obrázku:

ESP32-Cam a FTDI programátor

ESP32-CAM FTDI programátor
GND GND
3,3 V (popř. 5 V) 3,3 V (popř. 5 V)
U0R (pin GPIO 1) TX
U0T (pin GPIO 3) RX
GPIO 0 GND

Propojení modulu ESP32-CAM a FTDI programátoru

Na internetu lze najít i zapojení, kde je programátor s modulem ESP32-CAM propojen napětím 5 V místo zde uvedených 3,3 V. Protože varianta s 3,3 V nám funguje bezproblémově vždy, zatímco při použití napájení 5 V se občas po nahrátí kódu ukázal problém, uvádíme zde toto osvědčené zapojení. Upřímně jsme nezjišťovali, zda je za tím problém s naším modulem nebo programátorem. Ale podobné problémy měli dle některých ohlasů na odborných fórech i jiní uživatelé.

Naopak, při provozu modulu ESP32-CAM již používáme pro napájení 5 V připojené na patřičný pin. Všimněme si propojovacího vodiče (na obr. šedý) mezi piny GPIO 0 a GND – toto propojení je pro upload programu nutné!

Po připojení FTDI programátoru k USB počítače již budeme programovat modul ESP32-CAM stejně, jako každý jiný modul.


Program Wi-Fi kamery

Zde uvedený kód je jedena ze starších (jednodušších) verzí naší Wi-Fi kamery původně určené pro potřeby Vzdálené internetové laboratoře. Připomínáme, že tomu také odpovídají některé nastavení – kupříkladu rozlišení kamery, pevná IP adresa a připojení kamery do existující Wi-Fi sítě.

Pokud potřebujete kameru, která vytváří svou vlastní Wi-Fi síť a tedy modul ESP funguje jako jednoduché AP, doporučujeme nastudovat článek: https://randomnerdtutorials.com/esp32-cam-access-point-ap-web-server/, podle kterého lze níže uvedený kód lehce upravit.

Námi zde prezentovaný kód umožňuje načítat obrázky s rozlišením 640×480 (ale ukážeme si, jak lze nastavit maximálních 1600×1200).  Kamera pracuje ve dvou základních režimech. Prvním režimem je klasické načtení jednotlivého obrázku ve formátu JPG a druhý režim je v podobě nekonečného MJPG streamu. Zabudovaný webový server též obsahuje základní úvodní webovou stránku, která slouží jako úvodní informační rozcestník. Kromě načítání obrázků je možné vzdáleně rozsvěcet a zhasínat přisvětlovací LED. Protože tato dioda je konstruována spíše jako fotografický blesk, zřejmě není připravena na nepřetržitý provoz, je pro ni tedy v programu řešen i časovač, který ji po pěti minutách svitu automaticky zhasne.

Kód webového serveru pro streamování videa:

/*
JPG-stream Server
verze 0.0.3_5
- rozlišení 640x480 (schvalne snizeno)
- lze nacist obrazek: /out.jpg
- lze nacitat stream: /video.mjpg
- obsahuje web-page: /
- lze rozsvitit LED: /LEDON
- lze LED zhasnout: /LEDOFF
- obsahuje TimeOUT pro přisvícení (5 minut)
- pri problemu blika cervena LED
*/

#include "esp_camera.h"
#include <WiFi.h>
#include "esp_timer.h"
#include "img_converters.h"
#include "Arduino.h"
#include "fb_gfx.h"
#include "soc/soc.h"
#include "soc/rtc_cntl_reg.h"
#include "esp_http_server.h"

// --- SITOVA CAST ---
const char* ssid = "XXXXXXXXXXXXXX";  // prihlaseni do Wi-Fi
const char* password = "XXXXXXXXXXXXXX";

IPAddress local_IP(192, 168, 8, 110);  // staticka IP addresa
IPAddress gateway(192, 168, 8, 1);    // IP brany
IPAddress subnet(255, 255, 0, 0);     // maska podsite
IPAddress primaryDNS(192,168 ,8, 1);  // primarni DNS
IPAddress secondaryDNS(8, 8, 8, 8);   // sekundarni DNS

#define PORT 80   // port serveru

// --- NASTAVENI MODULU a KAMERY ---
#define PART_BOUNDARY "123456789000000000000987654321"
// udaje platne pro AI Thinker Model
#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

#define LED_BUILTIN_FLASH 4 // zabudovane LEDky
#define LED_BUILTIN_RED 33

// ---- WEB ----
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  <title>IP CAM - WebPage</title>
</head>
<body>
  <h1>IP-CAM server is READY</h1>
  <p>JPG picture --- /out.jpg</p>
  <p>JPG stream --- /video.mjpg</p>
  <p>LED on --- LEDON</p>
  <p>LED off --- LEDOFF</p>
</body>
</html>)rawliteral";

boolean led = false;
boolean STAV = true;
long cas = 0;

// ----------------------- JPG STREAM ---------------------
static const char* _STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY;
static const char* _STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n";
static const char* _STREAM_PART = "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n";

httpd_handle_t stream_httpd = NULL;

static esp_err_t jpg_stream_httpd_handler(httpd_req_t *req){
  camera_fb_t * fb = NULL;
  esp_err_t res = ESP_OK;
  size_t _jpg_buf_len = 0;
  uint8_t * _jpg_buf = NULL;
  char * part_buf[64];

  res = httpd_resp_set_type(req, _STREAM_CONTENT_TYPE);
  if(res != ESP_OK){
    return res;
  }

  while(true){
    fb = esp_camera_fb_get();
    if (!fb) {
      Serial.println("Camera capture failed");
      res = ESP_FAIL;
    } else {
      if(fb->width > 400){
        if(fb->format != PIXFORMAT_JPEG){
          bool jpeg_converted = frame2jpg(fb, 80, &_jpg_buf, &_jpg_buf_len);
          esp_camera_fb_return(fb);
          fb = NULL;
          if(!jpeg_converted){
            Serial.println("JPEG compression failed");
            res = ESP_FAIL;
          }
        } else {
          _jpg_buf_len = fb->len;
          _jpg_buf = fb->buf;
        }
      }
    }
    if(res == ESP_OK){
      size_t hlen = snprintf((char *)part_buf, 64, _STREAM_PART, _jpg_buf_len);
      res = httpd_resp_send_chunk(req, (const char *)part_buf, hlen);
    }
    if(res == ESP_OK){
      res = httpd_resp_send_chunk(req, (const char *)_jpg_buf, _jpg_buf_len);
    }
    if(res == ESP_OK){
      res = httpd_resp_send_chunk(req, _STREAM_BOUNDARY, strlen(_STREAM_BOUNDARY));
    }
    if(fb){
      esp_camera_fb_return(fb);
      fb = NULL;
      _jpg_buf = NULL;
    } else if(_jpg_buf){
      free(_jpg_buf);
      _jpg_buf = NULL;
    }
    if(res != ESP_OK){
      break;
    }
  }
  return res;
}

// ------------------- JPG CAPTURE -------------------
typedef struct {
        httpd_req_t *req;
        size_t len;
} jpg_chunking_t;

static size_t jpg_encode_stream(void * arg, size_t index, const void* data, size_t len){
    jpg_chunking_t *j = (jpg_chunking_t *)arg;
    if(!index){
        j->len = 0;
    }
    if(httpd_resp_send_chunk(j->req, (const char *)data, len) != ESP_OK){
        return 0;
    }
    j->len += len;
    return len;
}

static esp_err_t jpg_httpd_handler(httpd_req_t *req){
    camera_fb_t * fb = NULL;
    esp_err_t res = ESP_OK;
    size_t fb_len = 0;

    fb = esp_camera_fb_get();
    if (!fb) {
        ESP_LOGE(TAG, "Camera capture failed");
        httpd_resp_send_500(req);
        return ESP_FAIL;
    }
    res = httpd_resp_set_type(req, "image/jpeg");
    if(res == ESP_OK){
        res = httpd_resp_set_hdr(req, "Content-Disposition", "inline; filename=capture.jpg");
    }

    if(res == ESP_OK){
        if(fb->format == PIXFORMAT_JPEG){
            fb_len = fb->len;
            res = httpd_resp_send(req, (const char *)fb->buf, fb->len);
        } else {
            jpg_chunking_t jchunk = {req, 0};
            res = frame2jpg_cb(fb, 80, jpg_encode_stream, &jchunk)?ESP_OK:ESP_FAIL;
            httpd_resp_send_chunk(req, NULL, 0);
            fb_len = jchunk.len;
        }
    }
    esp_camera_fb_return(fb);
    return res;
}

// ----------------- FLASH LAMP CONTROL ------------------
static esp_err_t flashON_handler(httpd_req_t *req) {
  digitalWrite(LED_BUILTIN_FLASH, HIGH);
  cas = (300000 + millis()) % 4294967;
  led = true;
  httpd_resp_set_type(req, "text/plain");
  const char resp[] = "OK";
  httpd_resp_send(req, resp, 2);
  return ESP_OK;
}

static esp_err_t flashOFF_handler(httpd_req_t *req) {
  digitalWrite(LED_BUILTIN_FLASH, LOW);
  led = true;
  httpd_resp_set_type(req, "text/plain");
  const char resp[] = "OK";
  httpd_resp_send(req, resp, 2);
  return ESP_OK;
}

// -------------------- HTML page --------------------
static esp_err_t web_handler(httpd_req_t *req) {
  httpd_resp_send(req, index_html, strlen(index_html));
  return ESP_OK;
}

// ---------------------- camera server -------------
void startCameraServer(){
  httpd_config_t config = HTTPD_DEFAULT_CONFIG();
  config.max_uri_handlers = 5;
  config.server_port = PORT;

  httpd_uri_t picture_uri = {       // zaregistrovani procedury pro JPG
    .uri       = "/out.jpg",
    .method    = HTTP_GET,
    .handler   = jpg_httpd_handler,
    .user_ctx  = NULL
  };

  httpd_uri_t stream_uri = {       // zaregistrovani procedury pro streamovani
    .uri       = "/video.mjpg",
    .method    = HTTP_GET,
    .handler   = jpg_stream_httpd_handler,
    .user_ctx  = NULL
  };

  httpd_uri_t flashON_uri = {       // zaregistrovani procedury pro zapnuti LED
    .uri       = "/LEDON",
    .method    = HTTP_GET,
    .handler   = flashON_handler,
    .user_ctx  = NULL
  };

httpd_uri_t flashOFF_uri = {       // zaregistrovani procedury pro vypnuti LED
    .uri       = "/LEDOFF",
    .method    = HTTP_GET,
    .handler   = flashOFF_handler,
    .user_ctx  = NULL
  };

httpd_uri_t web_uri = {       // zaregistrovani procedury pro webovou stranku
    .uri       = "/",
    .method    = HTTP_GET,
    .handler   = web_handler,
    .user_ctx  = NULL
  };

  if (httpd_start(&stream_httpd, &config) == ESP_OK) {  // registrace pozadavku pro server
    httpd_register_uri_handler(stream_httpd, &picture_uri);
    httpd_register_uri_handler(stream_httpd, &stream_uri);
    httpd_register_uri_handler(stream_httpd, &flashON_uri);
    httpd_register_uri_handler(stream_httpd, &flashOFF_uri);
    httpd_register_uri_handler(stream_httpd, &web_uri);
  }
}

camera_config_t config;   // globalni promenna pro parametry konfigurace kamery

void configInitCamera(){
  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; //YUV422,GRAYSCALE,RGB565,JPEG

  // Pokud kamera nepodporuje PSRAM, vyberte nastavení mensi velikosti snimku
  // QVGA (320x240), CIF (400x296), VGA (640x480), SVGA (800x600), XGA (1024x768), SXGA (1280x1024), UXGA (1600x1200)
  if(psramFound()){
    config.frame_size = FRAMESIZE_SVGA;   // FRAMESIZE_ + QVGA|CIF|VGA|SVGA|XGA|SXGA|UXGA
    config.jpeg_quality = 10;             //10-63 nizsi cislo znamena vyssi kvalitu
    config.fb_count = 2;
  } else {
    config.frame_size = FRAMESIZE_SVGA;
    config.jpeg_quality = 15;
    config.fb_count = 1;
  }

  // Inicializujte kameru
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera init failed with error 0x%x", err);
    STAV = false;
    return;
  }

  sensor_t * s = esp_camera_sensor_get();   // nastavení zobrazovacich parametru kamery
  s->set_brightness(s, 0);     // -2 to 2
  s->set_contrast(s, 1);       // -2 to 2
  s->set_saturation(s, -1);     // -2 to 2
  s->set_special_effect(s, 0); // 0 to 6 (0 - No Effect, 1 - Negative, 2 - Grayscale, 3 - Red Tint, 4 - Green Tint, 5 - Blue Tint, 6 - Sepia)
  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 - if awb_gain enabled (0 - Auto, 1 - Sunny, 2 - Cloudy, 3 - Office, 4 - Home)
  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, 300);    // 0 to 1200
  s->set_gain_ctrl(s, 1);      // 0 = disable, 1 = enable
  s->set_agc_gain(s, 0);       // 0 to 30
  s->set_gainceiling(s, (gainceiling_t) 0);  // 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, 0);          // 0 = disable, 1 = enable
  s->set_dcw(s, 1);            // 0 = disable, 1 = enable
  s->set_colorbar(s, 0);       // 0 = disable, 1 = enable
}

void setup() {
  WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); //disable brownout detector

  pinMode(LED_BUILTIN_RED, OUTPUT);  // pro cervenou LED stavu
  pinMode (LED_BUILTIN_FLASH, OUTPUT);  // pro LED prisviceni

  Serial.begin(115200);
  Serial.setDebugOutput(false);

  Serial.print("Initializing the camera module...");
  configInitCamera();
  Serial.println("Ok!");

  // nastaveni pevne konfigurace IP
  if(!WiFi.config(local_IP, gateway, subnet, primaryDNS, secondaryDNS)) {
    Serial.println("STA Failed to configure");
    STAV = false;
  }

  // pripojeni k Wi-Fi (SSID, heslo)
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected");

  Serial.print("Camera Stream Ready! Go to: http://");
  Serial.print(WiFi.localIP());

  startCameraServer();    // Spusti streamovaci webovy server

  digitalWrite(LED_BUILTIN_RED, LOW);    // vse uz bezí, tak rozsit cervenou LED
}

void loop() {
  if (!STAV) { // je problem, tak blikej
    digitalWrite(LED_BUILTIN_RED, HIGH);
    delay(100);
    digitalWrite(LED_BUILTIN_RED, LOW);
    delay(100);
  }

  if ((millis() > cas) && (led)) { // po 300 sekundach zhasni
    digitalWrite(LED_BUILTIN_FLASH, LOW);
    led = false;
  }
  delay(1);
}

Popis kódu:

První části kódu jsou obecná nastavené a dekklarace parametrů a konstant. První řádky tvoří nastavení síťové komunikace:

// --- SITOVA CAST ---
const char* ssid = "XXXXXXXXXXXXXX";  // prihlaseni do Wi-Fi
const char* password = "XXXXXXXXXXXXXX";

IPAddress local_IP(192, 168, 8, 110);  // staticka IP addresa
IPAddress gateway(192, 168, 8, 1);    // IP brany
IPAddress subnet(255, 255, 0, 0);     // maska podsite
IPAddress primaryDNS(192,168 ,8, 1);   // primarni DNS
IPAddress secondaryDNS(8, 8, 8, 8);   // sekundarni DNS

#define PORT 80   // port serveru

Konstanty ssid a password obsahují přihlašovací údaje Wi-Fi sítě, do které se má modul připojit. Před kompilací kódu a jeho zapsáním do obvodu ESP32-CAM je třeba zde zadat platné údaje místo sekvence znaků XXXXXXXXXXXXXX. (ssid je jméno Wi-Fi sítě, password je heslo do této Wi-Fi sítě)

Dále uvedené proměnné typu IPAddress, jak ukazují i jejich komentáře, slouží pro nastavení pevné IP adresy modulu ESP32-CAM v síti, zde 192.168.8.110.

Pokud nechceme, aby modul měl pevně přiřazenou IP adresu, ale naopak byla IP adresa přiřazena daným prvkem sítě, tyto řádky vynecháme. A následně tyto proměnné vynecháme i při konfiguraci Wi-Fi sítě (v proceduře setup). Tedy řádky:

  // nastaveni pevne konfigurace IP
  if(!WiFi.config(local_IP, gateway, subnet, primaryDNS, secondaryDNS)) {
   Serial.println("STA Failed to configure");

upravíme do podoby:

  if(!WiFi.config()) {
   Serial.println("STA Failed to configure");


Poslední řádek v úvodním síťovém nastavení:

#define PORT 80   // port serveru

určuje číslo portu, na kterém bude komunikovat webový server kamery.

Za úvodním nastavením síťové části programu pokračuje nastavení kamerového modulu: (zde nejde o samotné nastavení, to bude dále, zde jsou jen nastaveny hodnoty, které budou dále nataveny)

// --- NASTAVENI MODULU a KAMERY ---
#define PART_BOUNDARY "123456789000000000000987654321"
// udaje platne pro  AI Thinker Model
#define PWDN_GPIO_NUM     32
#define RESET_GPIO_NUM    -1
#define XCLK_GPIO_NUM      0

… atd. …

#define HREF_GPIO_NUM     23
#define PCLK_GPIO_NUM     22

Toto nastavení odpovídá kamerovému modulu OV2640.  Při použití jiného kamerového modulu, je třeba nastavení změnit.

#define LED_BUILTIN_FLASH 4 // zabudovane LEDky
#define LED_BUILTIN_RED 33

Tyto řádky určují čísla pinů, na kterých jsou připojeny dvě zabudované LED – na pinu číslo 4 je LED přisvětlení (blesku), pin 33 je propojen se zabudovanou červenou LED, která je na desce a nyní bude sloužit jako stavový signál – například při jakémkoliv problému s připojením do sítě, inicializací kamerového modulu atd. bude blikat.

Část kódu

// ---- WEB ----
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  <title>IP CAM - WebPage</title>
</head>
<body>

… atd. …

</body>
</html>)rawliteral";

definuje obsah webové stránky, která se bude zobrazovat jako informační stránka webového serveru, pokud nebude zvolena některá z níže uvedených variant (zobrazení obrázku, obrazového streamu, rozsvícení přisvětlovací LED…). Znakové pole index_html se při překladu jednorázově zapisuje do flash paměti, aby nezabíralo jinak omezený prostor dynamických proměnných. (viz vlastnosti struktury  PROGMEM).

Dvě logické proměnné:

boolean led = false;
boolean STAV = true;

jsou dvě logické proměnné, které budou udávat stav přisvětlovací LED a celkový stav při inicializaci kamery.

Další proměnná, kterou můžeme najít na dalším řádku kódu:

long cas = 0;

bude sloužit pro zabezpečení maximálního pětiminutového svitu přisvětlovací LED.

Dále v programu následují procedury, které se budou provádět při jednotlivých dotazech na webovém serveru. Nejdříve jsou všechny procedury deklarované a pak budou společně zaregistrovány jednotlivým webovým dotazům.

První část tvoří procedury a funkce pro snímání obrazu a videa z kamerového modulu. Procedury pro snímání video streamu MJPEG jsou odděleny komentářem:

// ----------------------- JPG STREAM --------------------

Hlavní procedurou této části je procedura jpg_stream_httpd_handler, kterou chceme volat při zadání dotazu video.mjpg na webovém serveru.

Procedury pro načtení jednoho JPG obrázku jsou odděleny komentářem:

// ------------------- JPG CAPTURE -------------------

Procedura jpg_httpd_handler je opět procedurou, která bude volána při patřičném dotazu na webovém serveru – zde dotaz na soubor out.jpg.

Obě procedury zde nebudeme podrobně rozebírat, jen se zaměříme na jednu základní část. V obou těchto případech dojde k načtení „čistých“ grafických dat z kamery do proměnné fb, pak jsou tyto data konvertována do formátu JPG. Například:

res = frame2jpg_cb(fb, 80, jpg_encode_stream, &jchunk)?ESP_OK:ESP_FAIL;

kde parametr 80 určuje kvalitu této konverze 0–100%). Jde o stejný parametr, jaký se udává například v grafických programech při nastavování kvality JPG konverze a ukládání.

Další částí programu jsou procedury pro řízení přisvětlovací LED:

// ----------------- FLASH LAMP CONTROL ------------------
static esp_err_t flashON_handler(httpd_req_t *req) {
  digitalWrite(LED_BUILTIN_FLASH, HIGH);
  cas = (300000 + millis()) % 4294967;
  led = true;
  httpd_resp_set_type(req, "text/plain");
  const char resp[] = "OK";
  httpd_resp_send(req, resp, 2);
  return ESP_OK;
}

static esp_err_t flashOFF_handler(httpd_req_t *req) {
  digitalWrite(LED_BUILTIN_FLASH, LOW);
  led = false;
  httpd_resp_set_type(req, "text/plain");
  const char resp[] = "OK";
  httpd_resp_send(req, resp, 2);
  return ESP_OK;
}

Procedura flashON_handler rozsvítí přisvětlovací LED (stav uloží do logické proměnné led) a nastaví do proměnné cas hodnotu interního časovače, při které se má LED vypnout. Celočíselné dělení hodnotou 4294967 zohledňuje pravidelné „přetečení“ funkce millis() po dosažení své maximální hodnoty. Úspěšné rozsvícení přisvětlovací LED webový server potvrdí textovým výstupem OK.

Procedura flashOFF_handler slouží obdobně, jen přisvětlovací LED zhasíná.

Procedura web_handler odpovídá za zobrazení informační webové stránky uložené v znakovém poli index_html.

// -------------------- HTML page --------------------
static esp_err_t web_handler(httpd_req_t *req) {
  httpd_resp_send(req, index_html, strlen(index_html));
  return ESP_OK;
}

Nyní je třeba všechny dříve uvedené výkonné procedury propojit s webovým serverem, zejména s danými webovými dotazy. To jo zajištěno následujícím kódem:

// ---------------------- camera server -------------
void startCameraServer(){
  httpd_config_t config = HTTPD_DEFAULT_CONFIG();
  config.max_uri_handlers = 5;
  config.server_port = PORT;

Tato procedura spustí webový server s dříve zadanými parametry a na určeném portu. Dále je zde nastaven maximální počet (zde pět) ovládacích procedur. Tyto procedury se postupně zaregistrují na následujících řádcích. Nejdříve se nastaví jednotlivé proměnné typu httpd_uri_t, které určují, typ dotazu, zadaný text dotazu a proceduru, která se má spustit. Kupříkladu:

  httpd_uri_t picture_uri = {       // zaregistrovani procedury pro JPG
    .uri       = "/out.jpg",
    .method    = HTTP_GET,
    .handler   = jpg_httpd_handler,
    .user_ctx  = NULL
  };

určuje, že po zadání dotazu out.jpg  na webový server se spustí procedura jpg_httpd_handler, která vrátí obrázek formátu jpg.

Podobně řádky

httpd_uri_t stream_uri = {       // zaregistrovani procedury pro streamovani
… atd. …

slouží k propojení dotazu video.mjpg s procedurou jpg_stream_httpd_handler.

Obdobně je i deklarováno propojení dotazu LEDON pro rozsvícení přisvětlovací LED:

httpd_uri_t flashON_uri = {       // zaregistrovani procedury pro zapnuti LED
… atd. …

A dotazu LEDOFF pro zhasnutí LED

httpd_uri_t flashOFF_uri = {       // zaregistrovani procedury pro vypnuti LED
… atd. …

Při prázdném dotazu (tedy pouhé zadání dané IP adresy webového serveru) se má zobrazit informační webová stránka.

httpd_uri_t web_uri = {       // zaregistrovani procedury pro webovou stranku
… atd. …

Je však třeba upozornit, že předešlé řádky ještě výkonné procedury („handlery“) a webové dotazy nepropojily. Zatím šlo jen o vytvoření registračních datových struktur. Registraci udělá až následující řádky, které jsou registrací předešlých datových struktur do webového serveru.

  if (httpd_start(&stream_httpd, &config) == ESP_OK) {  // registrace pozadavku pro server
    httpd_register_uri_handler(stream_httpd, &picture_uri);
    httpd_register_uri_handler(stream_httpd, &stream_uri);
    httpd_register_uri_handler(stream_httpd, &flashON_uri);
    httpd_register_uri_handler(stream_httpd, &flashOFF_uri);
    httpd_register_uri_handler(stream_httpd, &web_uri);
  }
}

Poslední částí před procedurou setup je dvojice procedur, které nastavují již stanovené parametry, jako je propojení kamerového modulu a nastavení parametrů načteného obrazu – např. rozlišení obrázku, automatická balance bílé, vypnutí efektu sepia apod. (významy jsou uvedeny v komentářích).

V těchto řádcích stojí za zmínku nastavení rozlišení – v našem příkladu 640×480 pixelů. Rozlišení se nastavuje pomocí konstanty FRAMESIZE_  za kterou následuje název zvoleného rozlišení, konkrétně QVGA (320×240), CIF (400×296), VGA (640×480), SVGA (800×600), XGA (1024×768), SXGA (1280×1024) a UXGA (1600×1200). Takže námi zvolené rozlišení 640×480 je nastaveno předdefinovanou konstantou FRAMESIZE_VGA, kterou dosazujeme do proměnné config.frame_size, zatímco maximální množné rozlišení (1600×1200) bychom nastavili dosazením konstanty FRAMESIZE_UXGA.

Jen pozor, toto dosazení probíhá na dvou místech v podmínce:

  if(psramFound()){
    config.frame_size = FRAMESIZE_SVGA;   // FRAMESIZE_ + QVGA|CIF|VGA|SVGA|XGA|SXGA|UXGA
    config.jpeg_quality = 10;             //10-63 nizsi cislo znamena vyssi kvalitu
    config.fb_count = 2;
  } else {
    config.frame_size = FRAMESIZE_SVGA;
    config.jpeg_quality = 15;
    config.fb_count = 1;
  }

kde funkce psramFound() je určována připojeným modulem zapojené kamery. Pokud se tím nechceme příliš zabývat, dosaďme námi zvolené rozlišení do obou větví podmínky.

Závěrečný start modulu, nastavení webového serveru a konfigurace kamery nastává po spuštění procedury setup. Zde se postupně spouští jednotlivé služby – průběh toho je postupně zobrazován na sériovém výstupu. Můžeme jej tedy sledovat přes FTDI programátor a spuštěný sériový monitor (nastavení rychlosti 115200) v prostředí Arduino IDE. To je výhodné především při prvotním startu a pak zejména, pokud není IP adresa stanovena napevno. Modul totiž po úspěšném přiřazení IP adresy sítí tuto adresu na sériový výstup vypíše. Aby i bez sledování výpisů na sériovém výstupu bylo jasné, že vše probíhá tak, jak má, je stav modulu signalizován pomocí červené zabudované LED. Pokud tato LED nebliká, proběhlo vše správně.

void setup() {
  WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); // disable brownout detector

  pinMode(LED_BUILTIN_RED, OUTPUT);  // pro cervenou LED stavu
  pinMode (LED_BUILTIN_FLASH, OUTPUT);  // pro LED prisviceni

  Serial.begin(115200);
  Serial.setDebugOutput(false);

  Serial.print("Initializing the camera module...");
  configInitCamera();
  Serial.println("Ok!");

  // nastaveni pevne konfigurace IP
  if(!WiFi.config(local_IP, gateway, subnet, primaryDNS, secondaryDNS)) {
    Serial.println("STA Failed to configure");
    STAV = false;
  }

  // pripojeni k Wi-Fi (SSID, heslo)
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected");

  Serial.print("Camera Stream Ready! Go to: http://");
  Serial.print(WiFi.localIP());

  startCameraServer();    // Spusti streamovaci webovy server

  digitalWrite(LED_BUILTIN_RED, LOW);    // vse uz bezí, tak rozsit cervenou LED
}

Protože webový server i se svými službami běží na pozadí hlavní výkonné smyčky, je v této hlavní části (tedy v proceduře loop), již řešeno jen blikání červené LED (pokud nastal problém při startu kamery) a „hlídání“ času svícení přisvětlovací LED.

void loop() {
  if (!STAV) { // je problem, tak blikej
    digitalWrite(LED_BUILTIN_RED, HIGH);
    delay(100);
    digitalWrite(LED_BUILTIN_RED, LOW);
    delay(100);
  }

  if ((millis() > cas) && (led)) { // po 300 sekundach zhasni
    digitalWrite(LED_BUILTIN_FLASH, LOW);
    led = false;
  }
  delay(1);
}

Načítání IP kamery

Načítání obrazu z kamery lze zajistit přímým http dotazem – zadáním odpovídající URL, kupříkladu po zadání dorazu: http:// 192.168.8.110/out.jpg se zobrazí jeden JPG obrázek. Nebo po zadání dotazu: http:// 192.168.8.110/video.mjpg začne načítat nekonečný JPG stream.

Další možností je načítání obrazů připravenou webovou stránkou, která tyto obrázky (popř. stream) načítá. Kupříkladu následující kód aktualizuje každých 100 ms načtený JPG obrázek a umožňuje  ovládat přisvětlovací LED:

<!DOCTYPE html>
<html lang="cs">
      <head>
      <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
      <title>IP CAM</title>
<style type="text/css">
* {
      padding:0;
      margin:0;
}
#camImg {
      width:100%;
      max-width:640px;
}
</style>
</head>
<body>
    <img src="" onLoad="reloadImage();" alt="WebCam" id="camImg" />
    <p>
      <input type="BUTTON" value="Zapnout sv&ecaron;tlo" onClick="LED_on();" />
      <input type="BUTTON" value="Vypnout sv&ecaron;tlo" onClick="LED_off();" />
    </p>
<script>
<!--
var baseURL = "http://192.168.8.110/";
var timerID = null;
var timerRunning = false;

var xmlHttp;

function GetXmlHttpObject() {
      var XMLHttp_=null;
      try {
            XMLHttp_=new ActiveXObject("Msxml2.XMLHTTP");
      }
      catch(e) {
            try {
                  XMLHttp_=new ActiveXObject("Microsoft.XMLHTTP");
            }catch(e){}
      }
      if (XMLHttp_==null) {
            XMLHttp_=new XMLHttpRequest();
      }
      return XMLHttp_;
}

function reloadImage() {
      var url = baseURL + "out.jpg?&time=" + Math.random();

      var _img = document.getElementById('camImg');
      var newImg = new Image;
      newImg.onload = function() {
            _img.src = this.src;
      }
      newImg.src = url;
}

function LED_on() {
      xmlHttp=GetXmlHttpObject();
      if(xmlHttp==null) {
            alert("Browser does not support HTTP Request")
            return
      }
      var url=baseURL+"LEDON";
      xmlHttp.open("GET",url,true);
      xmlHttp.send(null);
}

function LED_off() {
      xmlHttp=GetXmlHttpObject();
      if(xmlHttp==null) {
            alert("Browser does not support HTTP Request")
            return
      }
      var url=baseURL+"LEDOFF";
      xmlHttp.open("GET",url,true);
      xmlHttp.send(null);
}

function startclock() {
      stopclock();
      time();
}

function stopclock() {
      if (timerRunning) clearTimeout(timerID);
      timerRunning = false;
}

function time() {
      reloadImage();
      timerID = setTimeout("time()",100);
      timerRunning = true;
}

startclock();
// -->
</script>
</body>
</html>

Podobnou webovou stránku lze napsat i pro MJPG stream. Jen je třeba upozornit, že při načtení nekonečného MJPG streamu, je stále plně vytížen webový server, tedy nemůže přijímat další příkazy od nejen jiného uživatele, ale ani od uživatele, který je právě připojen. Aby se tomuto problému předešlo, je možné využít pro načítání MJPG streamu nějakého „prostředníka“. Takovým „prostředníkem“ může například být program YawCam, do kterého lze zadat náš modul jako IP kameru. Program YawCam bude tím jediným připojeným uživatelem a další „distribuci“ obrazu pro další uživatele bude zařizovat svým více uživatelským serverem.

Ale je již na každém z dalších uživatelů, jak bude výstup z kamerového modulu zpracovávat.

Závěrem

Výše uvedený kód lze rovnou použít jako jednoduchou IP kameru, která se připojí do předem zadané domácí Wi-Fi sítě. Toto řešení se dá použít kupříkladu pro monitoring daného prostotu. Je třeba jen ještě připomenout, že je zde řešen jen přenos obrazu a nikoliv zvuku.

Zde prezentované řešení je jakousi IP náhradou webové USB kamery připojené k počítači se spuštěným kamerovým serverem. Další případné vylepšení a rozšíření programového kódu je možné. Koneckonců modul ESP32-CAM má na to dostatek výkonu. Zajímavou možností je třeba i možnost rozpoznání tváře nebo detekce pohybu – i pro tyto aplikace lze na internetu najít dobré podklady.

Pro případné vážné zájemce o modul ESP32-CAM a jeho další možné aplikace, lze doporučit PDF knihu: „ESP32-CAM Projects“ (https://randomnerdtutorials.com/esp32-cam-projects-ebook/), která na 400 stranách popisuje řešení 20 projektů, jako je pořizování fotografií a ukládání na kartu microSD; vytvoření webového serveru pro fotografování nebo streamování videa; odesílání pořízených fotografií prostřednictvím e-mailu; připojení IP kamery k Home Assistantu; stavba dálkově ovládaného automobilového robota s kamerou; projekt vzdáleně otočného a naklápěcího stojanu s kamerou; zmíněná detekce obličeje ale i mnohem více…

Autor článku: RNDr. Miroslav Panoš, Ph.D.
UPOZORNĚNÍ:
Odmítáme ministerské rozhodnutí vyřadit Newtonovy zákony, Ohmův zákon a zákon zachování energie z učiva fyziky základních škol v České republice!