projectTA/esp32cam.cpp

721 lines
21 KiB
C++

#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <HTTPClient.h>
#include "esp_camera.h"
#include "time.h"
#include "SPIFFS.h" // Tambahkan SPIFFS untuk penyimpanan file
// WiFi credentials
const char* ssid = "didinganteng";
const char* password = "didin123";
// NTP Server untuk waktu
const char* ntpServer = "pool.ntp.org";
const long gmtOffset_sec = 25200; // GMT+7 (WIB) in seconds (7*3600)
const int daylightOffset_sec = 0;
// Web server on port 80
WebServer server(80);
// Server Laravel (ganti dengan alamat server Laravel Anda)
const char* laravelServerUrl = "https://monitoring-anggrek.oyi.web.id/api/esp32cam/upload"; // Ganti dengan IP komputer Anda
// Status variables
bool cameraInitialized = false;
unsigned long startTime = 0;
// Menghilangkan timeout 5 menit
// const unsigned long TIMEOUT = 300000; // 5 menit (300000 ms) sebelum sleep
// Variabel untuk pengambilan gambar otomatis
const unsigned long AUTO_CAPTURE_DELAY = 5000; // 5 detik setelah boot
bool hasAutoCapture = false; // Flag untuk menandai sudah auto capture atau belum
// Variabel untuk streaming
bool isStreaming = false; // Flag untuk status streaming
const int streamingFrameRate = 10; // Frame rate untuk streaming (fps)
const int streamingDelay = 1000 / streamingFrameRate; // Delay antara frame dalam ms
// Variabel untuk pengaturan kamera
framesize_t streamResolution = FRAMESIZE_SVGA; // Default 800x600
int streamQuality = 10; // Default quality (0-63, lower is better)
// Variabel untuk pengiriman ulang foto
const int MAX_RETRY_PHOTOS = 5; // Maksimal foto yang disimpan sementara
const unsigned long RETRY_INTERVAL = 30000; // Interval pengiriman ulang (30 detik)
unsigned long lastRetryTime = 0; // Waktu terakhir mencoba mengirim ulang
int pendingPhotoCount = 0; // Jumlah foto yang menunggu dikirim
bool isRetryScheduled = false; // Flag untuk menandai jadwal pengiriman ulang
// ESP32 Camera pins
#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
// Helper function to add CORS headers to all responses
void setCorsHeaders() {
server.sendHeader("Access-Control-Allow-Origin", "*");
server.sendHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
server.sendHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
}
// Helper function to initialize SPIFFS
bool initSPIFFS() {
if (!SPIFFS.begin(true)) { // Format SPIFFS if mounting fails
Serial.println("SPIFFS gagal diinisialisasi");
return false;
}
Serial.println("SPIFFS berhasil diinisialisasi");
// Bersihkan file foto lama jika ada
cleanupPendingPhotos();
return true;
}
// Helper function untuk membersihkan file foto yang pending
void cleanupPendingPhotos() {
// Cek jumlah foto pending
int count = 0;
File root = SPIFFS.open("/");
File file = root.openNextFile();
while (file) {
String fileName = file.name();
if (fileName.startsWith("/photo_") && fileName.endsWith(".jpg")) {
count++;
Serial.println("Found pending photo: " + fileName);
}
file = root.openNextFile();
}
pendingPhotoCount = count;
Serial.println("Jumlah foto pending: " + String(pendingPhotoCount));
// Jika ada foto pending, jadwalkan pengiriman ulang
if (pendingPhotoCount > 0) {
isRetryScheduled = true;
Serial.println("Pengiriman ulang foto terjadwal");
}
}
bool initCamera() {
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;
// Initialize with high quality
config.frame_size = FRAMESIZE_SVGA; // 800x600
config.jpeg_quality = 10; // 0-63, lower is better quality
config.fb_count = 2; // Meningkatkan jumlah frame buffer untuk streaming
// Camera init
esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK) {
Serial.printf("Kamera gagal diinisialisasi, error 0x%x", err);
return false;
}
Serial.println("Kamera berhasil diinisialisasi");
return true;
}
void connectToWifi() {
Serial.print("Menghubungkan ke WiFi...");
WiFi.begin(ssid, password);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500);
Serial.print(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println();
Serial.println("WiFi terhubung!");
Serial.print("IP Address: ");
Serial.println(WiFi.localIP());
Serial.print("Subnet Mask: ");
Serial.println(WiFi.subnetMask());
Serial.print("Gateway IP: ");
Serial.println(WiFi.gatewayIP());
} else {
Serial.println();
Serial.println("Gagal terhubung ke WiFi. Restart ESP32...");
delay(1000);
ESP.restart();
}
}
void initTime() {
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) {
Serial.println("Gagal mendapatkan waktu dari NTP server");
return;
}
Serial.println("NTP waktu diinisialisasi");
Serial.print("Waktu saat ini: ");
Serial.print(&timeinfo, "%A, %d-%m-%Y %H:%M:%S");
Serial.println();
}
// Simpan foto ke SPIFFS untuk dikirim nanti
bool savePhotoForLater(camera_fb_t *fb) {
if (pendingPhotoCount >= MAX_RETRY_PHOTOS) {
Serial.println("Terlalu banyak foto pending, tidak bisa menyimpan lagi");
return false;
}
// Buat nama file dengan timestamp
char fileName[32];
struct tm timeinfo;
if(getLocalTime(&timeinfo)){
sprintf(fileName, "/photo_%04d%02d%02d_%02d%02d%02d.jpg",
timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday,
timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
} else {
sprintf(fileName, "/photo_%d.jpg", millis());
}
// Simpan ke SPIFFS
File file = SPIFFS.open(fileName, FILE_WRITE);
if (!file) {
Serial.println("Gagal membuka file untuk menulis");
return false;
}
if (file.write(fb->buf, fb->len) != fb->len) {
Serial.println("Gagal menulis file");
file.close();
return false;
}
file.close();
Serial.println("Foto berhasil disimpan di SPIFFS: " + String(fileName));
pendingPhotoCount++;
isRetryScheduled = true;
return true;
}
// Fungsi untuk mengirim ulang foto yang tersimpan di SPIFFS
void retryPendingPhotos() {
if (pendingPhotoCount <= 0) {
isRetryScheduled = false;
return;
}
if (WiFi.status() != WL_CONNECTED) {
Serial.println("Tidak ada koneksi WiFi, menunda pengiriman ulang");
return;
}
Serial.println("Mencoba mengirim foto yang tertunda...");
File root = SPIFFS.open("/");
File file = root.openNextFile();
bool anySuccess = false;
while (file && pendingPhotoCount > 0) {
String fileName = file.name();
if (fileName.startsWith("/photo_") && fileName.endsWith(".jpg")) {
Serial.println("Mengirim foto: " + fileName);
// Baca file
if (file.size() > 0) {
uint8_t *buffer = (uint8_t*) malloc(file.size());
if (buffer) {
size_t size = file.read(buffer, file.size());
// Kirim ke server
HTTPClient http;
http.begin(laravelServerUrl);
http.addHeader("Content-Type", "image/jpeg");
int httpResponseCode = http.POST(buffer, size);
free(buffer);
if (httpResponseCode > 0) {
String response = http.getString();
Serial.println("HTTP Response code: " + String(httpResponseCode));
Serial.println("Response: " + response);
// Jika sukses, hapus file
String fullFileName = fileName;
file.close();
if (SPIFFS.remove(fullFileName)) {
Serial.println("File berhasil dihapus: " + fullFileName);
pendingPhotoCount--;
anySuccess = true;
} else {
Serial.println("Gagal menghapus file: " + fullFileName);
}
} else {
Serial.print("Error pada HTTP request: ");
Serial.println(httpResponseCode);
}
http.end();
}
}
if (!anySuccess) {
// Jika tidak ada yang berhasil dikirim, kita hentikan dulu
// untuk menghemat resource dan mencoba lagi nanti
break;
}
}
file = root.openNextFile();
}
// Update status
if (pendingPhotoCount <= 0) {
isRetryScheduled = false;
Serial.println("Semua foto tertunda berhasil dikirim");
} else {
Serial.println("Masih ada " + String(pendingPhotoCount) + " foto tertunda");
}
}
// Rute untuk halaman utama
void handleRoot() {
setCorsHeaders();
String html = "<html><body>";
html += "<h1>ESP32-CAM Monitoring Anggrek</h1>";
html += "<p>Akses <a href='/capture'>capture</a> untuk mengambil foto</p>";
// Hilangkan link sleep
// html += "<p>Akses <a href='/sleep'>sleep</a> untuk masuk mode deep sleep</p>";
html += "<p>Akses <a href='/status'>status</a> untuk melihat status perangkat</p>";
html += "<p>Akses <a href='/stream'>stream</a> untuk melihat live streaming</p>";
struct tm timeinfo;
if (getLocalTime(&timeinfo)) {
char timeString[50];
strftime(timeString, sizeof(timeString), "%A, %d-%m-%Y %H:%M:%S", &timeinfo);
html += "<p>Waktu saat ini: " + String(timeString) + "</p>";
}
html += "<p>Waktu hidup: " + String((millis() - startTime) / 1000) + " detik</p>";
// Tambahkan info foto pending
html += "<p>Foto tertunda: " + String(pendingPhotoCount) + "</p>";
html += "</body></html>";
server.send(200, "text/html", html);
}
// Rute untuk mengambil foto
void handleCapture() {
setCorsHeaders();
if (!cameraInitialized) {
server.send(500, "text/plain", "Kamera tidak diinisialisasi");
return;
}
camera_fb_t *fb = NULL;
// Ambil foto
fb = esp_camera_fb_get();
if (!fb) {
Serial.println("Gagal mengambil foto");
server.send(500, "text/plain", "Gagal mengambil foto");
return;
}
Serial.println("Foto berhasil diambil");
Serial.print("Ukuran: ");
Serial.print(fb->len);
Serial.println(" bytes");
// Kirim foto sebagai respons HTTP
server.sendHeader("Content-Type", "image/jpeg");
server.sendHeader("Content-Disposition", "inline; filename=capture.jpg");
server.sendHeader("Content-Length", String(fb->len));
server.send_P(200, "image/jpeg", (const char *)fb->buf, fb->len);
// Bebaskan memori
esp_camera_fb_return(fb);
Serial.println("Foto berhasil dikirim ke client");
}
// Rute untuk status perangkat
void handleStatus() {
setCorsHeaders();
String status = "{";
// Informasi sistem
status += "\"heap_size\":" + String(ESP.getFreeHeap()) + ",";
status += "\"uptime\":" + String(millis() / 1000) + ",";
// Informasi WiFi
status += "\"wifi\":{";
status += "\"connected\":" + String(WiFi.status() == WL_CONNECTED ? "true" : "false") + ",";
status += "\"ip\":\"" + WiFi.localIP().toString() + "\",";
status += "\"rssi\":" + String(WiFi.RSSI());
status += "},";
// Informasi kamera
status += "\"camera\":{";
status += "\"initialized\":" + String(cameraInitialized ? "true" : "false") + ",";
status += "\"streaming\":" + String(isStreaming ? "true" : "false");
status += "},";
// Informasi foto pending
status += "\"pending_photos\":{";
status += "\"count\":" + String(pendingPhotoCount) + ",";
status += "\"retry_scheduled\":" + String(isRetryScheduled ? "true" : "false");
status += "}";
status += "}";
server.sendHeader("Content-Type", "application/json");
server.send(200, "application/json", status);
}
// Handler untuk streaming MJPEG
void handleStream() {
if (!cameraInitialized) {
setCorsHeaders();
server.send(500, "text/plain", "Kamera tidak diinisialisasi");
return;
}
Serial.println("Permintaan streaming diterima");
// Set header HTTP untuk streaming MJPEG
WiFiClient client = server.client();
// Tambahkan CORS headers manuallly (tanpa DefaultHeaders)
client.println("HTTP/1.1 200 OK");
client.println("Content-Type: multipart/x-mixed-replace; boundary=frame");
client.println("Access-Control-Allow-Origin: *");
client.println("Connection: keep-alive");
client.println();
// Set flag streaming
isStreaming = true;
Serial.println("Streaming dimulai");
// Loop untuk mengirim frame selama klien terhubung dan streaming aktif
while (client.connected() && isStreaming) {
camera_fb_t *fb = esp_camera_fb_get();
if (!fb) {
Serial.println("Capture gagal selama streaming");
delay(100);
continue;
}
client.println("--frame");
client.println("Content-Type: image/jpeg");
client.println("Content-Length: " + String(fb->len));
client.println();
client.write(fb->buf, fb->len);
client.println();
// Bebaskan memori
esp_camera_fb_return(fb);
// Delay sesuai frame rate
delay(streamingDelay);
}
// Jika keluar dari loop, streaming dihentikan
isStreaming = false;
Serial.println("Streaming dihentikan");
}
// Handler untuk menghentikan streaming
void handleStopStream() {
isStreaming = false;
setCorsHeaders();
Serial.println("Permintaan menghentikan streaming diterima");
server.send(200, "text/plain", "Streaming dihentikan");
}
// Fungsi untuk mengirim gambar ke server Laravel
void sendImageToServer() {
if (!cameraInitialized) {
Serial.println("Kamera tidak diinisialisasi, tidak bisa mengirim gambar");
return;
}
camera_fb_t *fb = NULL;
// Ambil foto
fb = esp_camera_fb_get();
if (!fb) {
Serial.println("Gagal mengambil foto untuk dikirim ke server");
return;
}
Serial.println("Foto berhasil diambil untuk dikirim ke server");
Serial.print("Ukuran: ");
Serial.print(fb->len);
Serial.println(" bytes");
// Cek koneksi WiFi
if (WiFi.status() != WL_CONNECTED) {
Serial.println("Tidak ada koneksi WiFi, menyimpan foto untuk dikirim nanti");
bool saved = savePhotoForLater(fb);
if (saved) {
Serial.println("Foto disimpan untuk dikirim nanti");
} else {
Serial.println("Gagal menyimpan foto untuk dikirim nanti");
}
esp_camera_fb_return(fb);
return;
}
// Kirim foto ke server Laravel
HTTPClient http;
http.begin(laravelServerUrl);
http.addHeader("Content-Type", "image/jpeg");
int httpResponseCode = http.POST(fb->buf, fb->len);
if (httpResponseCode > 0) {
String response = http.getString();
Serial.println("HTTP Response code: " + String(httpResponseCode));
Serial.println("Response: " + response);
} else {
Serial.print("Error pada HTTP request: ");
Serial.println(httpResponseCode);
// Jika gagal kirim, simpan untuk dikirim nanti
bool saved = savePhotoForLater(fb);
if (saved) {
Serial.println("Foto disimpan untuk dikirim nanti");
} else {
Serial.println("Gagal menyimpan foto untuk dikirim nanti");
}
}
http.end();
// Bebaskan memori
esp_camera_fb_return(fb);
Serial.println("Proses pengiriman gambar ke server selesai");
}
// Fungsi untuk pengambilan gambar otomatis
void autoCapture() {
Serial.println("Melakukan pengambilan gambar otomatis...");
sendImageToServer();
hasAutoCapture = true;
Serial.println("Pengambilan gambar otomatis selesai");
}
// Handler untuk CORS preflight requests
void handleOptions() {
setCorsHeaders();
server.send(204);
}
// Handler untuk mengubah pengaturan kamera
void handleCameraSettings() {
setCorsHeaders();
// Ambil parameter
String resolution = server.arg("resolution");
String quality = server.arg("quality");
bool changed = false;
// Ubah resolusi jika diperlukan
if (resolution.length() > 0) {
framesize_t newResolution = FRAMESIZE_SVGA; // Default
if (resolution == "UXGA") newResolution = FRAMESIZE_UXGA; // 1600x1200
else if (resolution == "SXGA") newResolution = FRAMESIZE_SXGA; // 1280x1024
else if (resolution == "XGA") newResolution = FRAMESIZE_XGA; // 1024x768
else if (resolution == "SVGA") newResolution = FRAMESIZE_SVGA; // 800x600
else if (resolution == "VGA") newResolution = FRAMESIZE_VGA; // 640x480
else if (resolution == "CIF") newResolution = FRAMESIZE_CIF; // 400x296
else if (resolution == "QVGA") newResolution = FRAMESIZE_QVGA; // 320x240
else if (resolution == "HQVGA") newResolution = FRAMESIZE_HQVGA; // 240x176
else if (resolution == "QQVGA") newResolution = FRAMESIZE_QQVGA; // 160x120
if (newResolution != streamResolution) {
streamResolution = newResolution;
changed = true;
// Terapkan ke sensor
sensor_t *s = esp_camera_sensor_get();
if (s) {
s->set_framesize(s, streamResolution);
Serial.println("Resolusi diubah ke " + resolution);
}
}
}
// Ubah kualitas jika diperlukan
if (quality.length() > 0) {
int newQuality = quality.toInt();
if (newQuality >= 0 && newQuality <= 63 && newQuality != streamQuality) {
streamQuality = newQuality;
changed = true;
// Terapkan ke sensor
sensor_t *s = esp_camera_sensor_get();
if (s) {
s->set_quality(s, streamQuality);
Serial.println("Kualitas gambar diubah ke " + quality);
}
}
}
// Kirim response
String response = "{";
response += "\"success\":" + String(changed ? "true" : "false") + ",";
response += "\"resolution\":\"";
switch (streamResolution) {
case FRAMESIZE_UXGA: response += "UXGA"; break;
case FRAMESIZE_SXGA: response += "SXGA"; break;
case FRAMESIZE_XGA: response += "XGA"; break;
case FRAMESIZE_SVGA: response += "SVGA"; break;
case FRAMESIZE_VGA: response += "VGA"; break;
case FRAMESIZE_CIF: response += "CIF"; break;
case FRAMESIZE_QVGA: response += "QVGA"; break;
case FRAMESIZE_HQVGA: response += "HQVGA"; break;
case FRAMESIZE_QQVGA: response += "QQVGA"; break;
default: response += "Unknown"; break;
}
response += "\",";
response += "\"quality\":" + String(streamQuality);
response += "}";
server.sendHeader("Content-Type", "application/json");
server.send(200, "application/json", response);
}
// Handler untuk mencoba pengiriman ulang foto secara manual
void handleRetryPhotos() {
setCorsHeaders();
retryPendingPhotos();
server.send(200, "text/plain", "Pengiriman ulang foto dimulai");
}
void setup() {
Serial.begin(115200);
delay(1000); // Tunggu serial siap
Serial.println("\n\n=== ESP32-CAM Monitoring Anggrek ===");
Serial.println("Starting as Web Server");
// Catat waktu mulai
startTime = millis();
// Inisialisasi SPIFFS
bool spiffsReady = initSPIFFS();
// Inisialisasi kamera
cameraInitialized = initCamera();
// Hubungkan ke WiFi
connectToWifi();
// Inisialisasi waktu
initTime();
// Konfigurasi rute server
server.on("/", handleRoot);
server.on("/capture", handleCapture);
server.on("/status", handleStatus);
// Tambahkan rute untuk streaming
server.on("/stream", HTTP_GET, handleStream);
server.on("/stopstream", HTTP_GET, handleStopStream);
// Tambahkan rute untuk pengambilan gambar dan pengiriman ke server
server.on("/send-to-server", []() {
setCorsHeaders();
sendImageToServer();
server.send(200, "text/plain", "Gambar berhasil dikirim ke server");
});
// Tambahkan rute untuk mencoba mengirim ulang foto
server.on("/retry-photos", HTTP_GET, handleRetryPhotos);
// Tambahkan rute untuk mengubah pengaturan kamera
server.on("/camera-settings", HTTP_GET, handleCameraSettings);
// Handle CORS preflight requests
server.on("/capture", HTTP_OPTIONS, handleOptions);
server.on("/status", HTTP_OPTIONS, handleOptions);
server.on("/stream", HTTP_OPTIONS, handleOptions);
server.on("/stopstream", HTTP_OPTIONS, handleOptions);
server.on("/send-to-server", HTTP_OPTIONS, handleOptions);
server.on("/retry-photos", HTTP_OPTIONS, handleOptions);
server.on("/camera-settings", HTTP_OPTIONS, handleOptions);
// Start server
server.begin();
Serial.println("HTTP server dimulai");
Serial.println("Buka http://" + WiFi.localIP().toString() + " di browser untuk mengakses ESP32-CAM");
Serial.println("Pengambilan gambar otomatis akan dilakukan dalam 5 detik...");
Serial.println("Streaming tersedia di http://" + WiFi.localIP().toString() + "/stream");
}
void loop() {
// Tangani request yang masuk
server.handleClient();
// Cek jika sudah waktunya untuk pengambilan gambar otomatis
if (!hasAutoCapture && (millis() - startTime > AUTO_CAPTURE_DELAY)) {
autoCapture();
}
// Cek jika perlu mengirim ulang foto yang tersimpan
if (isRetryScheduled && pendingPhotoCount > 0 && WiFi.status() == WL_CONNECTED) {
unsigned long currentTime = millis();
if (currentTime - lastRetryTime > RETRY_INTERVAL) {
lastRetryTime = currentTime;
retryPendingPhotos();
}
}
// Beri waktu untuk WiFi stack
delay(1);
}