commit 62db70958dd544c6e9c42400ec43e15088a3bc95 Author: hafizirawan111 Date: Wed Aug 5 12:32:28 2026 +0700 first commit diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7a73a41 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/backend b/backend new file mode 160000 index 0000000..1acec44 --- /dev/null +++ b/backend @@ -0,0 +1 @@ +Subproject commit 1acec44344cc56261345eb795e38f78312eb3630 diff --git a/esp32cam/esp32cam.ino b/esp32cam/esp32cam.ino new file mode 100644 index 0000000..49bb17c --- /dev/null +++ b/esp32cam/esp32cam.ino @@ -0,0 +1,476 @@ +/* + 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"); + } +} diff --git a/frontend b/frontend new file mode 160000 index 0000000..4ecdcf6 --- /dev/null +++ b/frontend @@ -0,0 +1 @@ +Subproject commit 4ecdcf6a4cc11a0c8b0592f15142cca3b68e7458 diff --git a/yolo-service b/yolo-service new file mode 160000 index 0000000..290e855 --- /dev/null +++ b/yolo-service @@ -0,0 +1 @@ +Subproject commit 290e855f6d9c2cf0e520c8f45fd8065d1d761387