Ten artykuł jest częścią projektu „Platforma gąsienicowa z kamerą na ESP32-CAM”
Przedstawiam wam unikalną aplikację dla ESP32-CAM, która otwiera nowe możliwości w zakresie monitoringu wideo i sterowania. Po długich poszukiwaniach idealnego rozwiązania do przesyłania wideo z kamery ESP32-CAM AI-Thinker na smartfon i jednoczesnego sterowania platformą gąsienicową lub kołową - w końcu stworzyłem taką aplikację i z przyjemnością się nią z wami dzielę.

Szkic dla ESP32-CAM tworzy punkt dostępowy WiFi o nazwie carv. Aby rozpocząć korzystanie, wystarczy uruchomić aplikację na smartfonie, nacisnąć przycisk menu (w prawym górnym rogu) i połączyć się z siecią mikrokontrolera przez ustawienia telefonu.
Interfejs jest intuicyjny: po naciśnięciu przycisku "Wideo" pod elementami sterowania pojawia się transmisja na żywo z kamery. Sterowanie odbywa się za pomocą prostych komend wysyłanych na serwer:
| Komenda | Opis | Link | |
|---|---|---|---|
| Left | Przycisk prawego joysticka | http://host/left | |
| Right | Przycisk prawego joysticka | http://host/right | |
| Up | Przycisk lewego joysticka | http://host/up | |
| Down | Przycisk lewego joysticka | http://host/down | |
| Fara | Sterowanie reflektorami | http://host/fara | |
| Avar | Awaryjne sygnały świetlne | http://host/avar | |
| Sign | Sygnał dźwiękowy | http://host/sign | |
| Dop1 | Dodatkowa funkcja 1 | http://host/dop1 | |
| Dop2 | Dodatkowa funkcja 2 | http://host/dop2 | |
| Reset1 | Centralny przycisk lewego joysticka | http://host/reset1 | |
| Reset2 | Centralny przycisk prawego joysticka | http://host/reset2 |
Transmisja wideo w czasie rzeczywistym
| Przeznaczenie | Link |
|---|---|
| Strumień wideo | http://host:81/stream |
Mikrokontroler ESP32-CAM uruchamia dwa serwery: transmisję wideo na porcie 81 i serwer komend na standardowym porcie 80. Poniższy testowy szkic umożliwia:
- Wyświetlanie wideo na ekranie smartfona z Androidem
- Wyświetlanie komend w monitorze portu
- Sterowanie diodami LED na płytce
#include <WiFi.h>
#include <WebServer.h>
#include "esp_camera.h"
// ==== Piny kamery dla AI Thinker ====
#define PWDN_GPIO_NUM 32
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 0
#define SIOD_GPIO_NUM 26
#define SIOC_GPIO_NUM 27
#define Y9_GPIO_NUM 35
#define Y8_GPIO_NUM 34
#define Y7_GPIO_NUM 39
#define Y6_GPIO_NUM 36
#define Y5_GPIO_NUM 21
#define Y4_GPIO_NUM 19
#define Y3_GPIO_NUM 18
#define Y2_GPIO_NUM 5
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 23
#define PCLK_GPIO_NUM 22
#define LED1_GPIO 33
#define LED2_GPIO 4
WebServer commandServer(80); // Serwer poleceń
WiFiServer videoServer(81); // Serwer strumienia wideo
TaskHandle_t videoTaskHandle; // Zadanie strumienia wideo
bool led1_state = false;
bool led2_state = false;
// === Uniwersalny handler poleceń ===
void handleCommandGeneric(const String& name) {
Serial.println("Odebrano polecenie: " + name);
commandServer.send(200, "text/plain", "OK " + name);
}
// === Specjalne polecenia sterujące diodami LED ===
void handleCommand1() {
led1_state = !led1_state;
digitalWrite(LED1_GPIO, led1_state);
Serial.println("Polecenie dop1: przełączono GPIO 33");
commandServer.send(200, "text/plain", "OK dop1");
}
void handleCommand2() {
led2_state = !led2_state;
digitalWrite(LED2_GPIO, led2_state);
Serial.println("Polecenie dop2: przełączono GPIO 4");
commandServer.send(200, "text/plain", "OK dop2");
}
// === Zadanie strumienia wideo na rdzeniu 0 ===
void videoStreamTask(void *parameter) {
videoServer.begin();
Serial.println("Serwer strumienia wideo uruchomiony na porcie 81");
while (true) {
WiFiClient client = videoServer.available();
if (client) {
Serial.println("Klient połączony ze strumieniem wideo");
String response = "HTTP/1.1 200 OK\r\n";
response += "Content-Type: multipart/x-mixed-replace; boundary=frame\r\n\r\n";
client.print(response);
while (client.connected()) {
camera_fb_t * fb = esp_camera_fb_get();
if (!fb) {
Serial.println("Błąd pobierania klatki");
break;
}
client.print("--frame\r\n");
client.print("Content-Type: image/jpeg\r\n\r\n");
client.write(fb->buf, fb->len);
client.print("\r\n");
esp_camera_fb_return(fb);
delay(50);
yield();
}
client.stop();
Serial.println("Klient rozłączony ze strumieniem wideo");
}
delay(10);
}
}
// === Inicjalizacja kamery ===
void setupCamera() {
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = Y2_GPIO_NUM;
config.pin_d1 = Y3_GPIO_NUM;
config.pin_d2 = Y4_GPIO_NUM;
config.pin_d3 = Y5_GPIO_NUM;
config.pin_d4 = Y6_GPIO_NUM;
config.pin_d5 = Y7_GPIO_NUM;
config.pin_d6 = Y8_GPIO_NUM;
config.pin_d7 = Y9_GPIO_NUM;
config.pin_xclk = XCLK_GPIO_NUM;
config.pin_pclk = PCLK_GPIO_NUM;
config.pin_vsync = VSYNC_GPIO_NUM;
config.pin_href = HREF_GPIO_NUM;
config.pin_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;
config.frame_size = FRAMESIZE_VGA;
config.jpeg_quality = 12;
config.fb_count = 1;
esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK) {
Serial.printf("Błąd kamery: 0x%x\n", err);
}
}
// === Konfiguracja ===
void setup() {
Serial.begin(115200);
pinMode(LED1_GPIO, OUTPUT);
pinMode(LED2_GPIO, OUTPUT);
digitalWrite(LED1_GPIO, LOW);
digitalWrite(LED2_GPIO, LOW);
IPAddress ip(192, 168, 4, 1);
IPAddress gateway(192, 168, 4, 1);
IPAddress subnet(255, 255, 255, 0);
WiFi.softAPConfig(ip, gateway, subnet);
WiFi.softAP("carv");
Serial.println("Punkt dostępowy: carv");
Serial.print("IP: ");
Serial.println(WiFi.softAPIP());
setupCamera();
// Strona główna sterowania
commandServer.on("/", HTTP_GET, []() {
String html = "<html><body><h2>Sterowanie ESP32-CAM</h2>";
html += "<p><a href='http://192.168.4.1:81/stream' target='_blank'>▶ Oglądaj wideo</a></p>";
const char* commands[] = {
"left", "right", "up", "down", "fara", "avar", "sign", "dop1", "dop2", "reset1", "reset2"
};
for (auto& cmd : commands) {
html += "<p><a href='/" + String(cmd) + "'>" + String(cmd) + "</a></p>";
}
html += "</body></html>";
commandServer.send(200, "text/html", html);
});
// Obsługa poleceń
commandServer.on("/dop1", HTTP_GET, handleCommand1);
commandServer.on("/dop2", HTTP_GET, handleCommand2);
commandServer.on("/left", HTTP_GET, []() { handleCommandGeneric("left"); });
commandServer.on("/right", HTTP_GET, []() { handleCommandGeneric("right"); });
commandServer.on("/up", HTTP_GET, []() { handleCommandGeneric("up"); });
commandServer.on("/down", HTTP_GET, []() { handleCommandGeneric("down"); });
// Zmienione linie: fara wykonuje dop2, avar wykonuje dop1
commandServer.on("/fara", HTTP_GET, handleCommand2);
commandServer.on("/avar", HTTP_GET, handleCommand1);
commandServer.on("/sign", HTTP_GET, []() { handleCommandGeneric("sign"); });
commandServer.on("/reset1", HTTP_GET, []() { handleCommandGeneric("reset1"); });
commandServer.on("/reset2", HTTP_GET, []() { handleCommandGeneric("reset2"); });
commandServer.begin();
Serial.println("Serwer poleceń uruchomiony (port 80)");
// Uruchomienie strumienia wideo w osobnym zadaniu
xTaskCreatePinnedToCore(
videoStreamTask,
"VideoStream",
8192,
NULL,
1,
&videoTaskHandle,
0
);
}
// === Główna pętla ===
void loop() {
commandServer.handleClient();
}
To podstawowa implementacja, którą możesz dostosować do własnych potrzeb. W najbliższym czasie przedstawię ulepszoną wersję do sterowania platformą gąsienicową, w tym modele STL i schemat elektryczny. Śledźcie aktualizacje!