Hardware — čo potrebuješ

Nahranie CameraWebServer príkladu

Arduino IDE obsahuje hotový príklad File → Examples → ESP32 → Camera → CameraWebServer. Tento príklad spustí MJPEG stream na porte 80 a foto capture na porte 81.

// V CameraWebServer.ino nastav:
#define CAMERA_MODEL_AI_THINKER   // ESP32-CAM AI-Thinker
const char* ssid     = "WIFI_SSID";
const char* password = "WIFI_PASS";

// Po nahratí otvor Serial Monitor (115200 baud)
// vypíše: "Camera Ready! Use 'http://192.168.1.XXX' to connect"

Detekcia pohybu — porovnanie snímok

ESP32-CAM nemá hardvérový PIR senzor, ale pohyb vieme detekovať softvérovo — porovnaním jasu pixelov dvoch po sebe nasledujúcich snímok. Kľúčový detail: pre porovnanie musíme použiť formát PIXFORMAT_GRAYSCALE, kde fb->buf obsahuje surové pixely (1 bajt = 1 pixel, hodnota jasu 0–255). Pri formáte JPEG by sme porovnávali komprimované bajty, čo nedáva zmysel a nefunguje spoľahlivo. Keď detekujeme pohyb, kameru prepneme do JPEG režimu pre kvalitný záber a odošleme ho na server.

#include "esp_camera.h"
#include <WiFi.h>
#include <HTTPClient.h>

#define CAMERA_MODEL_AI_THINKER
#include "camera_pins.h"

const char* WIFI_SSID  = "WIFI_SSID";
const char* WIFI_PASS  = "WIFI_PASS";
const char* SERVER_URL = "https://api.gear.sk/api/motion";
const char* API_KEY    = "tajny-kluc";
const char* DEVICE_ID  = "cam-01";
const float THRESHOLD  = 0.10; // 10 % zmenených pixelov = pohyb
const int   DELTA      = 25;   // rozdiel jasu > 25 = pixel sa zmenil

uint8_t* prevGray = nullptr;
size_t   prevLen  = 0;

bool initCamera(pixformat_t fmt, framesize_t size) {
    camera_config_t cfg;
    cfg.ledc_channel = LEDC_CHANNEL_0; cfg.ledc_timer = LEDC_TIMER_0;
    cfg.pin_d0 = Y2_GPIO_NUM; cfg.pin_d1 = Y3_GPIO_NUM;
    cfg.pin_d2 = Y4_GPIO_NUM; cfg.pin_d3 = Y5_GPIO_NUM;
    cfg.pin_d4 = Y6_GPIO_NUM; cfg.pin_d5 = Y7_GPIO_NUM;
    cfg.pin_d6 = Y8_GPIO_NUM; cfg.pin_d7 = Y9_GPIO_NUM;
    cfg.pin_xclk = XCLK_GPIO_NUM; cfg.pin_pclk = PCLK_GPIO_NUM;
    cfg.pin_vsync = VSYNC_GPIO_NUM; cfg.pin_href = HREF_GPIO_NUM;
    cfg.pin_sscb_sda = SIOD_GPIO_NUM; cfg.pin_sscb_scl = SIOC_GPIO_NUM;
    cfg.pin_pwdn = PWDN_GPIO_NUM; cfg.pin_reset = RESET_GPIO_NUM;
    cfg.xclk_freq_hz = 20000000;
    cfg.pixel_format = fmt;
    cfg.frame_size   = size;
    cfg.jpeg_quality = 12;
    cfg.fb_count     = 1;
    cfg.grab_mode    = CAMERA_GRAB_WHEN_EMPTY;
    return esp_camera_init(&cfg) == ESP_OK;
}

// Porovnávame surové pixely (GRAYSCALE) — každý bajt = jas jedného pixelu
bool detectMotion(camera_fb_t* fb) {
    if (!prevGray || prevLen != fb->len) {
        free(prevGray);
        prevGray = (uint8_t*)malloc(fb->len);
        prevLen  = fb->len;
        memcpy(prevGray, fb->buf, fb->len);
        return false;
    }
    int changed = 0, step = 4, total = fb->len / step;
    for (int i = 0; i < (int)fb->len; i += step) {
        if (abs((int)fb->buf[i] - (int)prevGray[i]) > DELTA) changed++;
    }
    memcpy(prevGray, fb->buf, fb->len);
    return (float)changed / total > THRESHOLD;
}

void sendPhotoToServer() {
    // Prepni kameru na JPEG pre kvalitný záber
    esp_camera_deinit();
    if (!initCamera(PIXFORMAT_JPEG, FRAMESIZE_VGA)) return;
    delay(300); // kamera sa stabilizuje

    camera_fb_t* fb = esp_camera_fb_get();
    if (fb) {
        HTTPClient http;
        http.begin(SERVER_URL);
        http.addHeader("X-API-Key",    API_KEY);
        http.addHeader("Content-Type", "image/jpeg");
        http.addHeader("X-Device-ID",  DEVICE_ID);
        http.POST(fb->buf, fb->len);
        http.end();
        esp_camera_fb_return(fb);
    }

    // Vráť sa do detekčného GRAYSCALE režimu
    esp_camera_deinit();
    prevLen = 0; // invaliduj buffer — zmenil sa formát
    initCamera(PIXFORMAT_GRAYSCALE, FRAMESIZE_QVGA);
}

void setup() {
    Serial.begin(115200);
    WiFi.begin(WIFI_SSID, WIFI_PASS);
    while (WiFi.status() != WL_CONNECTED) delay(500);
    Serial.println("WiFi: " + WiFi.localIP().toString());

    if (!initCamera(PIXFORMAT_GRAYSCALE, FRAMESIZE_QVGA)) {
        Serial.println("CHYBA: kamera sa nespustila!");
        while (true) delay(1000);
    }
    Serial.println("Kamera OK — monitorujem pohyb...");
}

void loop() {
    camera_fb_t* fb = esp_camera_fb_get();
    if (!fb) { delay(100); return; }

    if (detectMotion(fb)) {
        Serial.println("Pohyb detekovaný!");
        esp_camera_fb_return(fb); // uvoľni GRAYSCALE frame pred reinitom
        sendPhotoToServer();      // pošle JPEG, potom prepne späť na GRAYSCALE
        delay(5000);              // cooldown 5 sekúnd
        return;
    }

    esp_camera_fb_return(fb);
    delay(200); // ~5 FPS
}

Laravel endpoint — príjem snímky a notifikácia

// app/Http/Controllers/MotionController.php
class MotionController extends Controller
{
    public function receive(Request $request): JsonResponse
    {
        if ($request->header('X-API-Key') !== config('iot.motion_key')) {
            return response()->json(['error' => 'Unauthorized'], 401);
        }

        $deviceId = $request->header('X-Device-ID', 'unknown');
        $jpeg     = $request->getContent();
        $filename = 'motion/' . $deviceId . '/' . now()->format('Y-m-d_H-i-s') . '.jpg';

        // Ulož snímku do private storage (nie public)
        Storage::put($filename, $jpeg);

        // Zaloguj udalosť
        MotionEvent::create([
            'device_id' => $deviceId,
            'image_path' => $filename,
            'detected_at' => now(),
        ]);

        // Odošli notifikáciu asynchrónne (queue job)
        SendMotionAlert::dispatch($deviceId, $filename);

        return response()->json(['status' => 'received']);
    }
}

Job — e-mailová notifikácia

// app/Jobs/SendMotionAlert.php
class SendMotionAlert implements ShouldQueue
{
    public function __construct(
        public string $deviceId,
        public string $imagePath,
    ) {}

    public function handle(): void
    {
        $imageData = Storage::get($this->imagePath);

        Mail::to(config('iot.alert_email'))
            ->send(new MotionDetectedMail(
                $this->deviceId,
                $imageData,
                now(),
            ));
    }
}

Ukladanie na SD kartu

#include "SD_MMC.h"

void saveToSD(camera_fb_t* fb) {
    if (!SD_MMC.begin()) return;

    String path = "/motion_" + String(millis()) + ".jpg";
    File file = SD_MMC.open(path, FILE_WRITE);
    if (file) {
        file.write(fb->buf, fb->len);
        file.close();
    }
}

Praktické tipy