/* Smart Farm ESP32-CAM - Live stream MJPEG via HTTP (port 80) - WebSocket ke backend Railway: kirim frame JPEG binary terus-menerus - Byte pertama = 0x01 (frame biasa) | 0x02 (trigger deteksi, saat tombol ditekan) - Library: arduinoWebSockets by Markus Sattler (install via Library Manager) */ #include "esp_camera.h" #include #include #include "esp_http_server.h" #include "soc/soc.h" #include "soc/rtc_cntl_reg.h" #include // ============================================================ // KONFIGURASI // ============================================================ const char* ssid = "ini"; const char* password = "00000000"; const char* WS_HOST = "backendescam-production-cc88.up.railway.app"; const int WS_PORT = 443; const char* WS_PATH = "/ws/esp32"; // ============================================================ // PIN KAMERA 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 LED_PIN 4 #define BUTTON_PIN 13 // ============================================================ // FLAG BYTE // ============================================================ #define FLAG_FRAME 0x01 #define FLAG_DETECT 0x02 // ============================================================ // STREAM MJPEG (HTTP port 80) // ============================================================ #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 capture_httpd = NULL; static SemaphoreHandle_t camMutex = NULL; static SemaphoreHandle_t wsMutex = NULL; WebSocketsClient webSocket; volatile bool wsConnected = false; volatile bool isDetecting = false; bool psramTersedia = false; // ============================================================ // SENSOR KAMERA // ============================================================ void setSensorKamera() { sensor_t* s = esp_camera_sensor_get(); if (!s) return; s->set_framesize(s, FRAMESIZE_QVGA); s->set_vflip(s, 1); s->set_hmirror(s, 1); s->set_exposure_ctrl(s, 1); s->set_aec2(s, 1); s->set_ae_level(s, 0); s->set_aec_value(s, 300); s->set_gain_ctrl(s, 1); s->set_agc_gain(s, 0); s->set_gainceiling(s, (gainceiling_t)2); s->set_whitebal(s, 1); s->set_awb_gain(s, 1); s->set_wb_mode(s, 0); s->set_brightness(s, 1); s->set_contrast(s, 1); s->set_saturation(s, 1); s->set_sharpness(s, 2); s->set_denoise(s, 1); s->set_special_effect(s, 0); s->set_colorbar(s, 0); } // ============================================================ // WEBSOCKET EVENT // ============================================================ void webSocketEvent(WStype_t type, uint8_t* payload, size_t length) { switch (type) { case WStype_DISCONNECTED: wsConnected = false; Serial.println("[WS] Disconnect"); break; case WStype_CONNECTED: wsConnected = true; Serial.println("[WS] Terhubung: " + String((char*)payload)); break; case WStype_TEXT: { String msg = String((char*)payload); Serial.println("[WS] Backend: " + msg); if (msg.indexOf("detect_done") >= 0) { isDetecting = false; Serial.println("[WS] Deteksi selesai, live resume"); } break; } case WStype_ERROR: Serial.println("[WS] Error"); wsConnected = false; break; default: break; } } // ============================================================ // KIRIM FRAME // ============================================================ void kirimFrame(camera_fb_t* fb, uint8_t flag) { if (!wsConnected || !fb) return; if (fb->len < 500 || fb->len > 60000) return; if (ESP.getMaxAllocHeap() < (fb->len + 4096)) return; size_t totalLen = fb->len + 1; uint8_t* buf = (uint8_t*)malloc(totalLen); if (!buf) return; buf[0] = flag; memcpy(buf + 1, fb->buf, fb->len); if (xSemaphoreTake(wsMutex, pdMS_TO_TICKS(50)) == pdTRUE) { webSocket.sendBIN(buf, totalLen); xSemaphoreGive(wsMutex); } free(buf); } // ============================================================ // FRAME TASK // ============================================================ void frameTask(void* param) { uint32_t frameCount = 0; vTaskDelay(8000 / portTICK_PERIOD_MS); // tunggu WS connect dulu for (;;) { if (isDetecting || !wsConnected) { vTaskDelay(300 / portTICK_PERIOD_MS); continue; } camera_fb_t* fb = NULL; if (xSemaphoreTake(camMutex, pdMS_TO_TICKS(300)) == pdTRUE) { camera_fb_t* stale = esp_camera_fb_get(); if (stale) esp_camera_fb_return(stale); fb = esp_camera_fb_get(); xSemaphoreGive(camMutex); } if (fb) { kirimFrame(fb, FLAG_FRAME); esp_camera_fb_return(fb); frameCount++; if (frameCount % 30 == 0) Serial.printf("[FRAME] x%u heap:%d\n", frameCount, ESP.getFreeHeap()); } vTaskDelay(300 / portTICK_PERIOD_MS); // ~3 fps } } // ============================================================ // HTTP HANDLERS // ============================================================ static esp_err_t stream_handler(httpd_req_t* req) { camera_fb_t* fb = NULL; esp_err_t res = ESP_OK; char part_buf[64]; httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); httpd_resp_set_hdr(req, "Cache-Control", "no-cache, no-store"); res = httpd_resp_set_type(req, STREAM_CONTENT_TYPE); if (res != ESP_OK) return res; while (true) { if (xSemaphoreTake(camMutex, pdMS_TO_TICKS(200)) == pdTRUE) { camera_fb_t* stale = esp_camera_fb_get(); if (stale) esp_camera_fb_return(stale); fb = esp_camera_fb_get(); xSemaphoreGive(camMutex); } if (!fb) { res = ESP_FAIL; break; } res = httpd_resp_send_chunk(req, STREAM_BOUNDARY, strlen(STREAM_BOUNDARY)); if (res == ESP_OK) { size_t hlen = snprintf(part_buf, 64, STREAM_PART, fb->len); res = httpd_resp_send_chunk(req, part_buf, hlen); } if (res == ESP_OK) res = httpd_resp_send_chunk(req, (const char*)fb->buf, fb->len); esp_camera_fb_return(fb); fb = NULL; if (res != ESP_OK) break; taskYIELD(); } return res; } static esp_err_t capture_handler(httpd_req_t* req) { camera_fb_t* fb = NULL; if (xSemaphoreTake(camMutex, pdMS_TO_TICKS(500)) == pdTRUE) { camera_fb_t* stale = esp_camera_fb_get(); if (stale) esp_camera_fb_return(stale); fb = esp_camera_fb_get(); xSemaphoreGive(camMutex); } if (!fb) { httpd_resp_send_500(req); return ESP_FAIL; } httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); httpd_resp_set_hdr(req, "Cache-Control", "no-cache"); httpd_resp_set_type(req, "image/jpeg"); esp_err_t res = httpd_resp_send(req, (const char*)fb->buf, fb->len); esp_camera_fb_return(fb); return res; } static esp_err_t status_handler(httpd_req_t* req) { httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); httpd_resp_set_type(req, "application/json"); String json = "{\"status\":\"online\",\"ip\":\"" + WiFi.localIP().toString() + "\",\"ws\":" + String(wsConnected ? "true" : "false") + "}"; httpd_resp_sendstr(req, json.c_str()); return ESP_OK; } void startCameraServer() { httpd_config_t cfg1 = HTTPD_DEFAULT_CONFIG(); cfg1.server_port = 80; cfg1.max_open_sockets = 3; httpd_uri_t uris1[] = { { "/", HTTP_GET, stream_handler, NULL }, { "/status", HTTP_GET, status_handler, NULL }, }; if (httpd_start(&stream_httpd, &cfg1) == ESP_OK) for (auto& u : uris1) httpd_register_uri_handler(stream_httpd, &u); httpd_config_t cfg2 = HTTPD_DEFAULT_CONFIG(); cfg2.server_port = 81; cfg2.max_open_sockets = 2; cfg2.ctrl_port = 32769; httpd_uri_t uris2[] = { { "/capture", HTTP_GET, capture_handler, NULL }, }; if (httpd_start(&capture_httpd, &cfg2) == ESP_OK) for (auto& u : uris2) httpd_register_uri_handler(capture_httpd, &u); Serial.println("[SERVER] HTTP OK (port 80 stream, port 81 capture)"); } // ============================================================ // SETUP // ============================================================ void setup() { WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); Serial.begin(115200); delay(500); pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, LOW); pinMode(BUTTON_PIN, INPUT_PULLUP); for (int i = 0; i < 3; i++) { digitalWrite(LED_PIN, HIGH); delay(100); digitalWrite(LED_PIN, LOW); delay(100); } camMutex = xSemaphoreCreateMutex(); wsMutex = xSemaphoreCreateMutex(); psramTersedia = psramInit(); Serial.println(psramTersedia ? "[PSRAM] OK" : "[PSRAM] Tidak ada"); // WiFi WiFi.mode(WIFI_STA); WiFi.setSleep(false); WiFi.setTxPower(WIFI_POWER_19_5dBm); WiFi.begin(ssid, password); Serial.print("[WiFi] Menghubungkan"); unsigned long t = millis(); while (WiFi.status() != WL_CONNECTED) { if (millis() - t > 20000) { WiFi.begin(ssid, password); t = millis(); } delay(500); Serial.print("."); digitalWrite(LED_PIN, !digitalRead(LED_PIN)); } digitalWrite(LED_PIN, HIGH); Serial.println("\n[WiFi] OK: " + WiFi.localIP().toString()); WiFi.config(WiFi.localIP(), WiFi.gatewayIP(), WiFi.subnetMask(), IPAddress(8,8,8,8), IPAddress(1,1,1,1)); // Init kamera pinMode(PWDN_GPIO_NUM, OUTPUT); digitalWrite(PWDN_GPIO_NUM, HIGH); delay(100); digitalWrite(PWDN_GPIO_NUM, LOW); delay(300); 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_sccb_sda = SIOD_GPIO_NUM; config.pin_sccb_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.grab_mode = CAMERA_GRAB_LATEST; // QVGA untuk semua — stabil dan cukup kecil config.frame_size = FRAMESIZE_QVGA; config.jpeg_quality = 15; config.fb_count = psramTersedia ? 2 : 1; config.fb_location = psramTersedia ? CAMERA_FB_IN_PSRAM : CAMERA_FB_IN_DRAM; if (esp_camera_init(&config) != ESP_OK) { Serial.println("[CAM] Gagal init! Restart..."); delay(3000); ESP.restart(); } Serial.println("[CAM] OK"); setSensorKamera(); // Warmup for (int i = 0; i < 10; i++) { camera_fb_t* w = esp_camera_fb_get(); if (w) esp_camera_fb_return(w); delay(100); } startCameraServer(); Serial.printf("[HEAP] Sebelum WS: %d free / %d max-alloc\n", ESP.getFreeHeap(), ESP.getMaxAllocHeap()); // WebSocket — init terakhir setelah semua siap webSocket.beginSSL(WS_HOST, WS_PORT, WS_PATH); webSocket.onEvent(webSocketEvent); webSocket.setReconnectInterval(5000); webSocket.enableHeartbeat(20000, 6000, 3); // frameTask di core 0, stack 10KB xTaskCreatePinnedToCore(frameTask, "frameTask", 10240, NULL, 1, NULL, 0); Serial.println("\n=== SIAP ==="); Serial.println("IP : " + WiFi.localIP().toString()); Serial.printf("Heap: %d\n", ESP.getFreeHeap()); } // ============================================================ // LOOP // ============================================================ void loop() { // WS loop — wajib dipanggil tiap loop, guard mutex if (xSemaphoreTake(wsMutex, pdMS_TO_TICKS(5)) == pdTRUE) { webSocket.loop(); xSemaphoreGive(wsMutex); } // Reconnect WiFi if (WiFi.status() != WL_CONNECTED) { digitalWrite(LED_PIN, LOW); WiFi.reconnect(); unsigned long rs = millis(); while (WiFi.status() != WL_CONNECTED) { if (millis() - rs > 15000) ESP.restart(); delay(500); digitalWrite(LED_PIN, !digitalRead(LED_PIN)); } digitalWrite(LED_PIN, HIGH); } // ── Debounce tombol ────────────────────────────────── static bool lastBtn = HIGH; static bool stableBtn = HIGH; static unsigned long lastChange = 0; static bool triggered = false; bool raw = digitalRead(BUTTON_PIN); if (raw != lastBtn) { lastChange = millis(); lastBtn = raw; } if (millis() - lastChange >= 80) { if (stableBtn == HIGH && raw == LOW && !triggered) { int lo = 0; for (int i = 0; i < 5; i++) { if (digitalRead(BUTTON_PIN) == LOW) lo++; delayMicroseconds(500); } if (lo >= 4 && wsConnected && !isDetecting) { triggered = true; isDetecting = true; // Flash LED langsung — feedback visual instan digitalWrite(LED_PIN, HIGH); camera_fb_t* fb = NULL; if (xSemaphoreTake(camMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { // Flush stale frames for (int i = 0; i < 5; i++) { camera_fb_t* w = esp_camera_fb_get(); if (w) esp_camera_fb_return(w); vTaskDelay(80 / portTICK_PERIOD_MS); } fb = esp_camera_fb_get(); xSemaphoreGive(camMutex); } vTaskDelay(100 / portTICK_PERIOD_MS); digitalWrite(LED_PIN, LOW); if (fb && fb->len > 2000) { Serial.printf("[BTN] Frame %d bytes → FLAG_DETECT\n", fb->len); kirimFrame(fb, FLAG_DETECT); esp_camera_fb_return(fb); } else { if (fb) esp_camera_fb_return(fb); Serial.println("[BTN] Gagal ambil frame"); isDetecting = false; } } } if (raw == HIGH) triggered = false; stableBtn = raw; } // Timeout isDetecting — 30 detik static unsigned long detectStart = 0; if (isDetecting) { if (detectStart == 0) detectStart = millis(); if (millis() - detectStart > 30000) { isDetecting = false; detectStart = 0; Serial.println("[BTN] Timeout deteksi, reset"); } } else { detectStart = 0; } // Log tiap 15 detik static unsigned long lastLog = 0; if (millis() - lastLog >= 15000UL) { lastLog = millis(); Serial.printf("[LOOP] heap:%d ws:%s detecting:%s\n", ESP.getFreeHeap(), wsConnected ? "on" : "off", isDetecting ? "yes" : "no"); } }