Je continue de bien m'amuser avec l'ESP32-CAM. Cette fois, j'ai installé la caméra sur un servomoteur afin qu'elle puisse effectuer un mouvement panoramique horizontal. J'ai ainsi obtenu une caméra de surveillance qui balaie un champ de vision beaucoup plus large.
À cette étape du projet, le servomoteur tourne en continu aussitôt qu'un utilisateur visionne l'image vidéo diffusée par le web server de l'ESP32-CAM. Il sera certainement intéressant, comme développement ultérieur, d'ajouter un deuxième servomoteur ainsi que des boutons de contrôle sur la page web, ce qui permettrait à l'utilisateur de positionner la caméra à distance (pan & tilt).
Connexions
J'ai branché le servomoteur à la broche GPIO 2 de l'ESP32-CAM.
Pour l'exécution d'un sketch simple dans lequel l'ESP32-CAM ne faisait rien d'autre que faire tourner le servomoteur, j'ai pu alimenter le servomoteur en le branchant à la même alimentation 5 V que l'ESP32-CAM. Mais aussitôt que j'essayais de brancher l'ESP32-CAM au réseau WiFi, le "brownout detector" interrompait le programme au démarrage. Pour cette raison, j'ai alimenté le servomoteur avec une alimentation 5 V distincte.
Test du servomoteur
Il y a quelques mois, j'avais exploré la production d'un signal PWM avec un ESP32; ça m'a facilité la tâche pour produire ce sketch qui ne fait rien d'autre que faire tourner le servomoteur dans un sens, puis dans l'autre. Dans mon cas, le servomoteur atteignait ses positions extrêmes pour un rapport cyclique d'environ 3550 et 8000, mais ces valeurs peuvent varier un peu d'un servomoteur à l'autre.
-
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
L'ESP32-CAM fait tourner un servomoteur dans | |
un sens, puis dans l'autre. Le servomoteur est | |
branché à la broche GPIO 2. | |
Plus de détails: | |
https://electroniqueamateur.blogspot.com/2020/02/mouvement-panoramique-avec-esp32-cam-et.html | |
*/ | |
void setup() { | |
Serial.begin(115200); | |
ledcSetup(2, 50, 16); // canal, fréquence, résolution | |
ledcAttachPin(2, 2); // pin, channel | |
} | |
void loop() { | |
// les positions varient de 3550 à 8000. | |
for (int i = 3550; i <= 8000; i++) { | |
Serial.println(i); | |
ledcWrite(2, i); | |
delay(5); | |
} | |
for (int i = 8000; i >= 3550; i--) { | |
ledcWrite(2, i); | |
delay(5); | |
} | |
} |
Sketch: web server avec mouvement panoramique
Voici finalement le sketch qui permet d'observer l'image vidéo sur un navigateur web. Il s'agit essentiellement de mon sketch de web server de base, auquel j'ai ajouté quelques lignes pour gérer le servomoteur.
Aux lignes 29 à 36, je définis quelques constantes (broche à laquelle le servomoteur est connecté, position minimale désirée, position maximale désirée, vitesse désirée) et deux variables globales: la position du servomoteur et la direction dans laquelle il tourne.
Aux lignes 223 à 225, j'initialise le signal PWM et l'assigne à la broche à laquelle j'ai branché le servomoteur.
La position du servomoteur est réglée à la ligne 103. Les lignes suivantes calculent la prochaine position et vérifient s'il est temps d'inverser le sens de rotation..
La vidéo ci-dessous montre le résultat. Le mouvement du servomoteur n'est pas parfaitement régulier (il semble ralentir à certains moments) et le support en papier que j'ai bricolé pour ma ESP32-CAM manque de rigidité (il se déforme un peu pendant la rotation, ce qui ajoute une oscillation verticale à l'image). Mais comme prototype, ça me semble assez bien.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Affichage dans une page web de l'image vidéo | |
d'une ESP32-CAM, avec lent panoramique horizontal | |
obtenu grâce à un servomoteur. | |
Pour plus de détails: | |
https://electroniqueamateur.blogspot.com/2020/02/mouvement-panoramique-avec-esp32-cam-et.html | |
*/ | |
#include "esp_camera.h" | |
#include <WiFi.h> | |
#include "esp_http_server.h" | |
// paramètres de votre réseau WiFi | |
const char* ssid = "**********"; | |
const char* password = "***********"; | |
#define PART_BOUNDARY "123456789000000000000987654321" | |
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; | |
httpd_handle_t camera_httpd = NULL; | |
int numero_port; // numéro du port du stream server | |
const int broche_servo = 2; // servomoteur branché à GPIO 2 | |
// rapport cyclique correspondant à la position minimale du servomoteur | |
const int position_min = 3700; | |
// rapport cyclique correspondant à la position maximale du servomoteur | |
const int position_max = 7000; | |
const int vitesse_servo = 10; | |
int position_servo = position_min; | |
bool direction_servo = 1; // 1 à l'allée, 0 au retour | |
// ******************************************************** | |
// stream_handler: routine qui gère le streaming vidéo | |
static esp_err_t stream_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]; | |
static int64_t last_frame = 0; | |
if (!last_frame) { | |
last_frame = esp_timer_get_time(); | |
} | |
res = httpd_resp_set_type(req, _STREAM_CONTENT_TYPE); | |
if (res != ESP_OK) { | |
return res; | |
} | |
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); | |
while (true) { | |
fb = esp_camera_fb_get(); | |
if (!fb) { | |
Serial.println("Echec de la capture de camera"); | |
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("Echec de la compression JPEG"); | |
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; | |
} | |
// ------- gestion du mouvement du servomoteur ---------------- | |
ledcWrite(broche_servo, position_servo); | |
if (direction_servo){ | |
position_servo = position_servo + vitesse_servo; | |
if (position_servo > position_max){ | |
direction_servo = 0; | |
} | |
} | |
else{ | |
position_servo = position_servo - vitesse_servo; | |
if (position_servo < position_min){ | |
direction_servo = 1; | |
} | |
} | |
} | |
last_frame = 0; | |
return res; | |
} | |
// ******************************************************** | |
// web_handler: construction de la page web | |
static esp_err_t web_handler(httpd_req_t *req) { | |
httpd_resp_set_type(req, "text/html"); | |
httpd_resp_set_hdr(req, "Content-Encoding", "identity"); | |
sensor_t * s = esp_camera_sensor_get(); | |
char pageWeb[175] = ""; | |
strcat(pageWeb, "<!doctype html> <html> <head> <title id='title'>ESP32-CAM</title> </head> <body> <img id='stream' src='http://"); | |
// l'adresse du stream server (exemple: 192.168.0.145:81): | |
char adresse[20] = ""; | |
sprintf (adresse, "%d.%d.%d.%d:%d", WiFi.localIP()[0], WiFi.localIP()[1], WiFi.localIP()[2], WiFi.localIP()[3], numero_port); | |
strcat(pageWeb, adresse); | |
strcat(pageWeb, "/stream'> </body> </html>"); | |
int taillePage = strlen(pageWeb); | |
return httpd_resp_send(req, (const char *)pageWeb, taillePage); | |
} | |
// ******************************************************** | |
// startCameraServer: démarrage du web server et du stream server | |
void startCameraServer() { | |
httpd_config_t config = HTTPD_DEFAULT_CONFIG(); | |
httpd_uri_t index_uri = { | |
.uri = "/", | |
.method = HTTP_GET, | |
.handler = web_handler, | |
.user_ctx = NULL | |
}; | |
httpd_uri_t stream_uri = { | |
.uri = "/stream", | |
.method = HTTP_GET, | |
.handler = stream_handler, | |
.user_ctx = NULL | |
}; | |
Serial.printf("Demarrage du web server sur le port: '%d'\n", config.server_port); | |
if (httpd_start(&camera_httpd, &config) == ESP_OK) { | |
httpd_register_uri_handler(camera_httpd, &index_uri); | |
} | |
config.server_port += 1; | |
config.ctrl_port += 1; | |
Serial.printf("Demarrage du stream server sur le port: '%d'\n", config.server_port); | |
if (httpd_start(&stream_httpd, &config) == ESP_OK) { | |
httpd_register_uri_handler(stream_httpd, &stream_uri); | |
} | |
numero_port = config.server_port; | |
} | |
// ******************************************************** | |
// initialisation de la caméra, connexion au réseau WiFi. | |
void setup() { | |
Serial.begin(115200); | |
Serial.println(); | |
Serial.println("===="); | |
// définition des broches pour le modèle AI Thinker - ESP32-CAM | |
camera_config_t config; | |
config.ledc_channel = LEDC_CHANNEL_0; | |
config.ledc_timer = LEDC_TIMER_0; | |
config.pin_d0 = 5; | |
config.pin_d1 = 18; | |
config.pin_d2 = 19; | |
config.pin_d3 = 21; | |
config.pin_d4 = 36; | |
config.pin_d5 = 39; | |
config.pin_d6 = 34; | |
config.pin_d7 = 35; | |
config.pin_xclk = 0; | |
config.pin_pclk = 22; | |
config.pin_vsync = 25; | |
config.pin_href = 23; | |
config.pin_sscb_sda = 26; | |
config.pin_sscb_scl = 27; | |
config.pin_pwdn = 32; | |
config.pin_reset = -1; | |
config.xclk_freq_hz = 20000000; | |
config.pixel_format = PIXFORMAT_JPEG; //YUV422|GRAYSCALE|RGB565|JPEG | |
config.frame_size = FRAMESIZE_VGA; // QVGA|CIF|VGA|SVGA|XGA|SXGA|UXGA | |
config.jpeg_quality = 10; // 0-63 ; plus bas = meilleure qualité | |
config.fb_count = 2; // nombre de frame buffers | |
// initialisation de la caméra | |
esp_err_t err = esp_camera_init(&config); | |
if (err != ESP_OK) { | |
Serial.printf("Echec de l'initialisation de la camera, erreur 0x%x", err); | |
return; | |
} | |
sensor_t * s = esp_camera_sensor_get(); | |
// initialisation du servo moteur | |
ledcSetup(2, 50, 16); // canal, fréquence, résolution | |
ledcAttachPin(broche_servo, 2); // broche, canal | |
Serial.println(""); | |
Serial.print("Connexion au reseau WiFi: "); | |
Serial.println(ssid); | |
delay(100); | |
WiFi.begin(ssid, password); | |
while (WiFi.status() != WL_CONNECTED) { | |
delay(250); | |
} | |
Serial.println("WiFi connecte"); | |
Serial.println(""); | |
delay(100); | |
startCameraServer(); | |
Serial.print("La camera est prete. Utilisez 'http://"); | |
Serial.print(WiFi.localIP()); | |
Serial.println("' pour vous connecter."); | |
} | |
// ******************************************************** | |
// loop ne fait rien! | |
void loop() { | |
delay(10000); | |
} |
À lire également:
Première utilisation de l'ESP32-CAM avec l'IDE Arduino , un web server minimaliste , enregistrer des photos sur la carte microSD, les LEDs de l'ESP32-CAM, time-lapse avec l'ESP32-CAM.
Yves Pelletier (Twitter, Facebook)
Bonsoir, je trouve votre article très intéressant, cependant, j'aurais quelques questions :
RépondreSupprimerJe possède également une ESP32-cam, ainsi que 2 servos SG90. j'ai réalisé un boitiers PAN/TILT et j'aurais aimé savoir comment faire pour piloter ces servos via des bouton multi-directionnel sur une page WEB (exemple placé sous la vidéo de la cam), mais il fadrait que jeedom puisse recupérer ces commandes... merci à vous
Chris
Bonjour , ce serait sympa que depuis la page web on puisse régler les points min et max ainsi que la vitesse sans devoir a chaque fois devoir reprogrammer , perso pas assez bon en prog pour le faire ...
RépondreSupprimer