first commit

This commit is contained in:
hafizirawan111 2026-08-05 12:32:28 +07:00
commit 62db70958d
6 changed files with 481 additions and 0 deletions

2
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,2 @@
{
}

0
README.md Normal file
View File

1
backend Submodule

@ -0,0 +1 @@
Subproject commit 1acec44344cc56261345eb795e38f78312eb3630

476
esp32cam/esp32cam.ino Normal file
View File

@ -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 <WiFi.h>
#include <WiFiClientSecure.h>
#include "esp_http_server.h"
#include "soc/soc.h"
#include "soc/rtc_cntl_reg.h"
#include <WebSocketsClient.h>
// ============================================================
// 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");
}
}

1
frontend Submodule

@ -0,0 +1 @@
Subproject commit 4ecdcf6a4cc11a0c8b0592f15142cca3b68e7458

1
yolo-service Submodule

@ -0,0 +1 @@
Subproject commit 290e855f6d9c2cf0e520c8f45fd8065d1d761387