ESP32-CAM: Wi-Fi kamera doslova „za hubičku“
AKTUALIZOVÁNO: 8. 12. 2021 viz níže
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 (230–350 Kč dle prodejce).
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.
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.
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
Poté je třeba zvolit modul ESP32-CAM v manažeru desek prostředí Arduino IDE: Nástroje → Vývojová deska → Manažér 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.
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.).
Máme hotovo!
Nyní budeme psát a kompilovat kódy pro modul ESP32-CAM
Upload kódu do modulu ESP32-CAM
Nedávno se na Aliexpressu objevil modul pro přímé programování modulu ESP32-CAM. Jde o jakýsi shield, který se k modulu ESP32-CAM připojí. Tím modul ESP32-CAM rázem disponuje mikro USB konektorem (jako běžný kit ESP32), tím by tedy mohla odpadnout potřeba FTDI programátoru.
Modul pro přímé programování se jmenuje ESP32-CAM-MB a je k dostání i na českém trhu – můžete jej zakoupit například u firmy Hadex za 79 Kč.
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 | 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ání 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: Wi-Fi přístup k modulu ESP32, 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_51
- rozlišení 640x480 (schvalne snizeno)
- lze nacitat stream: /video.mjpg (spusten na portu 81)
- lze nacist obrazek: /out.jpg (port 80)
- obsahuje web-page: / }vse na portu 80)
- 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 weboveho serveru
#define ST_PORT 81 // port video streamu
// --- 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 --- :80/out.jpg</p>
<p>JPG stream --- :81/video.mjpg</p>
<p>LED on --- :80/LEDON</p>
<p>LED off --- :80/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; // budouci handler pro stream
httpd_handle_t web_httpd = NULL; // budouci handler pro web
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_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
};
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.max_uri_handlers = 4; // pocet zaregistrovanych procedur pro port 80
config.server_port = PORT;
if (httpd_start(&web_httpd, &config) == ESP_OK) { // registrace pozadavku pro web
httpd_register_uri_handler(web_httpd, &picture_uri);
httpd_register_uri_handler(web_httpd, &flashON_uri);
httpd_register_uri_handler(web_httpd, &flashOFF_uri);
httpd_register_uri_handler(web_httpd, &web_uri);
}
config.max_uri_handlers = 1; // pocet zaregistrovanych procedur pro port 81
config.server_port = ST_PORT;
config.ctrl_port += 1; // zvetseni poctu portu serveru
if (httpd_start(&stream_httpd, &config) == ESP_OK) { // registrace pozadavku pro stream
httpd_register_uri_handler(stream_httpd, &stream_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 deklarace 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ů XXXXXXXXXXXXX
. (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 weboveho serveru
#define ST_PORT 81 // port video streamu
určuje číslo portu, na kterém bude komunikovat webový server kamery a na kterém bude vysílán video stream. Načítání nekonečného MJPG streamu způsobí, že je daný port plně vytížen, takže pokud by byl webový server na stejném portu, nemohl by přijímat další příkazy nejen od jiného uživatele, ale ani od toho, který je právě připojen.
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(){
Tato procedura na svém závěru (viz dále) spustí oba porty webového serveru s dříve zadanými parametry. Dále jsou procedury, které se při přístupu na server budou vykonávat. 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.
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.max_uri_handlers = 4; // pocet zaregistrovanych procedur pro port 80
config.server_port = PORT;
if (httpd_start(&web_httpd, &config) == ESP_OK) { // registrace pozadavku pro web
httpd_register_uri_handler(web_httpd, &picture_uri);
httpd_register_uri_handler(web_httpd, &flashON_uri);
httpd_register_uri_handler(web_httpd, &flashOFF_uri);
httpd_register_uri_handler(web_httpd, &web_uri);
}
config.max_uri_handlers = 1; // pocet zaregistrovanych procedur pro port 81
config.server_port = ST_PORT;
config.ctrl_port += 1; // zvetseni poctu portu serveru
if (httpd_start(&stream_httpd, &config) == ESP_OK) { // registrace pozadavku pro stream
httpd_register_uri_handler(stream_httpd, &stream_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:81/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 - JPG</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ětlo" onClick="LED_on();" />
<input type="BUTTON" value="Vypnout svě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. Ten by mohl vypadat třeba takto:
<!DOCTYPE html>
<html lang="cs">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>IP CAM - STREAM</title>
<style type="text/css">
* {
padding:0;
margin:0;
}
#camImg {
width:100%;
max-width:640px;
}
</style>
</head>
<body>
<img src="http://192.168.8.110:81/video.mjpg" alt="WebCam" id="camImg" />
<p>
<input type="BUTTON" value="Zapnout světlo" onClick="LED_on();" />
<input type="BUTTON" value="Vypnout světlo" onClick="LED_off();" />
</p>
<script>
<!--
var baseURL = "http://192.168.8.110/";
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 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);
}
// -->
</script>
</body>
</html>
Jen je třeba upozornit, že dalším způsobem obsluhy této kamery je možnost využití pro načítání JPG obrázků a 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 se stane 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://