|
|
|
|
@ -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");
|
|
|
|
|
}
|
|
|
|
|
}
|