#include "esp_camera.h" #include #include #include #include #include // LED FLASH bawaan ESP32-CAM = GPIO 4 #define LED_PIN 4 // VIBRATION MOTOR = GPIO 12 #define VIBRO_PIN 12 // I2S AUDIO (MAX98357A) #define I2S_BCLK 14 #define I2S_LRC 15 #define I2S_DOUT 13 // PIN 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 // Global config (dari form, tidak disimpan ke EEPROM) String savedSSID = ""; String savedPassword = ""; String savedServer = ""; // format: "192.168.x.x" int savedPort = 5000; String savedPath = "/detect"; WebServer configServer(80); // Forward declaration void connectWiFi(); void startCamera(); void startConfigPortal(); // ====================== // INIT I2S (on-demand) // ====================== void startI2S() { i2s_config_t i2s_config = { .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX), .sample_rate = 8000, .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, .communication_format = I2S_COMM_FORMAT_STAND_I2S, .intr_alloc_flags = ESP_INTR_FLAG_LEVEL3, .dma_buf_count = 4, .dma_buf_len = 32, .use_apll = false, .tx_desc_auto_clear = true }; i2s_pin_config_t pin_config = { .bck_io_num = I2S_BCLK, .ws_io_num = I2S_LRC, .data_out_num = I2S_DOUT, .data_in_num = I2S_PIN_NO_CHANGE }; i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL); i2s_set_pin(I2S_NUM_0, &pin_config); i2s_zero_dma_buffer(I2S_NUM_0); } void stopI2S() { i2s_zero_dma_buffer(I2S_NUM_0); i2s_driver_uninstall(I2S_NUM_0); } // ====================== // DOWNLOAD WAV DARI SERVER & PLAY // Alur: HTTP GET /audio/ → stream ke I2S // ====================== void playWAVFromServer(const char* filename) { if (savedServer == "" || WiFi.status() != WL_CONNECTED) { Serial.println("āš ļø Skip audio: WiFi tidak konek"); return; } WiFiClient client; client.setTimeout(10000); Serial.printf("šŸ”Š Download audio: /audio/%s\n", filename); if (!client.connect(savedServer.c_str(), savedPort)) { Serial.println("āŒ Gagal konek ke server untuk audio"); return; } // HTTP GET request client.printf("GET /audio/%s HTTP/1.1\r\n", filename); client.printf("Host: %s:%d\r\n", savedServer.c_str(), savedPort); client.print("Connection: close\r\n\r\n"); // Tunggu response header unsigned long t = millis(); while (client.available() == 0) { if (millis() - t > 10000) { Serial.println("āŒ Timeout tunggu audio response"); client.stop(); return; } delay(5); } // Baca & buang HTTP header (sampai \r\n\r\n) bool headerDone = false; String headerBuf = ""; while (client.connected() || client.available()) { if (client.available()) { char c = client.read(); headerBuf += c; if (headerBuf.endsWith("\r\n\r\n")) { headerDone = true; break; } } } if (!headerDone) { Serial.println("āŒ Header audio tidak lengkap"); client.stop(); return; } // Cek status 200 if (headerBuf.indexOf("200 OK") == -1) { Serial.println("āŒ Server tidak kirim 200 OK untuk audio"); Serial.println(" Header: " + headerBuf.substring(0, 100)); client.stop(); return; } // Skip 44 byte WAV header dari body // Baca dulu 44 byte pertama uint8_t wavHeader[44]; int hRead = 0; unsigned long ht = millis(); while (hRead < 44 && (millis() - ht < 3000)) { if (client.available()) { wavHeader[hRead++] = client.read(); } } // Suspend kamera & init I2S esp_camera_deinit(); delay(50); startI2S(); Serial.println("ā–¶ Streaming audio ke I2S..."); // Stream body langsung ke I2S dalam chunk const int bufSize = 512; uint8_t buf[bufSize]; size_t bytesWritten = 0; unsigned long lastData = millis(); while (client.connected() || client.available()) { int available = client.available(); if (available > 0) { int toRead = min(available, bufSize); int bytesRead = client.read(buf, toRead); if (bytesRead > 0) { i2s_write(I2S_NUM_0, buf, bytesRead, &bytesWritten, portMAX_DELAY); lastData = millis(); } } else { if (millis() - lastData > 2000) break; // tidak ada data 2 detik → selesai delay(5); } } client.stop(); // Stop I2S & restart kamera stopI2S(); delay(50); startCamera(); Serial.println("šŸ”Š Audio selesai"); } // ====================== // MAPPING NOMINAL → PLAY AUDIO DARI SERVER // ====================== void playNominal(String response) { int idx = response.indexOf("\"nominal\""); if (idx == -1) { Serial.println("āš ļø Key nominal tidak ditemukan"); return; } int start = response.indexOf(":", idx) + 1; int q1 = response.indexOf("\"", start) + 1; int q2 = response.indexOf("\"", q1); String nominal = response.substring(q1, q2); nominal.trim(); nominal.toLowerCase(); Serial.println("šŸŽµ Nominal: " + nominal); if (nominal == "seribu" || nominal == "1000") playWAVFromServer("seribu.wav"); else if (nominal == "dua ribu" || nominal == "2000") playWAVFromServer("duaribu.wav"); else if (nominal == "lima ribu" || nominal == "5000") playWAVFromServer("limaribu.wav"); else if (nominal == "sepuluh ribu" || nominal == "10000") playWAVFromServer("sepuluhribu.wav"); else if (nominal == "dua puluh ribu" || nominal == "20000") playWAVFromServer("duapuluhribu.wav"); else if (nominal == "lima puluh ribu" || nominal == "50000") playWAVFromServer("limapuluhribu.wav"); else if (nominal == "seratus ribu" || nominal == "100000") playWAVFromServer("seratusribu.wav"); else if (nominal == "tidak terdeteksi") Serial.println("āš ļø Uang tidak terdeteksi"); else Serial.println("āš ļø Nominal tidak dikenali: " + nominal); } // ====================== // PARSE SERVER STRING // Input: "192.168.x.x" atau "http://192.168.x.x:5000/detect" // Hasil disimpan ke savedServer (host), savedPort, savedPath // ====================== void parseServerString(String raw) { raw.trim(); // Hapus prefix http:// if (raw.startsWith("http://")) { raw = raw.substring(7); } else if (raw.startsWith("https://")) { raw = raw.substring(8); } // Pisah host:port/path int slashIdx = raw.indexOf('/'); String hostPort; if (slashIdx != -1) { savedPath = raw.substring(slashIdx); // "/detect" hostPort = raw.substring(0, slashIdx); } else { savedPath = "/detect"; hostPort = raw; } // Pisah host:port int colonIdx = hostPort.indexOf(':'); if (colonIdx != -1) { savedServer = hostPort.substring(0, colonIdx); savedPort = hostPort.substring(colonIdx + 1).toInt(); } else { savedServer = hostPort; savedPort = 5000; } Serial.println(" Host : " + savedServer); Serial.println(" Port : " + String(savedPort)); Serial.println(" Path : " + savedPath); } // ====================== // KIRIM JPEG VIA WiFiClient (raw HTTP/1.1) // Return: body response atau "" jika gagal // - Retry koneksi hingga 5x jika gagal // - Timeout tunggu response: 30 detik (untuk inferensi AI yang lambat) // - Baca response hingga koneksi benar-benar tutup // ====================== String postImageWiFiClient(uint8_t* buf, size_t len) { WiFiClient client; client.setTimeout(30000); // TCP read timeout 30 detik // --- Retry koneksi hingga 5x --- const int MAX_RETRY = 5; bool connected = false; for (int attempt = 1; attempt <= MAX_RETRY; attempt++) { Serial.printf("šŸ”Œ Konek ke %s:%d (percobaan %d/%d)\n", savedServer.c_str(), savedPort, attempt, MAX_RETRY); if (client.connect(savedServer.c_str(), savedPort)) { connected = true; break; } Serial.println(" āš ļø Gagal, coba lagi 1 detik..."); delay(1000); } if (!connected) { Serial.println("āŒ Gagal koneksi setelah " + String(MAX_RETRY) + "x percobaan!"); return ""; } Serial.println("āœ… Terhubung ke server"); // --- Kirim HTTP POST header --- client.print("POST " + savedPath + " HTTP/1.1\r\n"); client.print("Host: " + savedServer + ":" + String(savedPort) + "\r\n"); client.print("Content-Type: image/jpeg\r\n"); client.print("Content-Length: " + String(len) + "\r\n"); client.print("Connection: close\r\n"); client.print("\r\n"); // --- Kirim body JPEG dalam chunk 1024 byte --- const size_t chunkSize = 1024; size_t sent = 0; while (sent < len) { size_t toSend = min(chunkSize, len - sent); size_t written = client.write(buf + sent, toSend); if (written == 0) { Serial.println("āŒ Koneksi putus saat kirim data!"); client.stop(); return ""; } sent += written; } Serial.println("šŸ“¤ Data terkirim (" + String(len) + " bytes), menunggu response..."); // --- Tunggu response dengan timeout 30 detik --- unsigned long waitStart = millis(); while (client.available() == 0) { if (!client.connected()) { Serial.println("āŒ Server menutup koneksi sebelum kirim response!"); client.stop(); return ""; } if (millis() - waitStart > 30000) { Serial.println("āŒ Timeout 30 detik menunggu response!"); client.stop(); return ""; } delay(10); } // --- Baca response sampai koneksi tutup --- // Strategi: baca semua dulu, lalu cari body setelah "\r\n\r\n" String fullResponse = ""; unsigned long readStart = millis(); while (client.connected() || client.available()) { if (client.available()) { char c = client.read(); fullResponse += c; readStart = millis(); // reset timeout jika masih ada data } else { if (millis() - readStart > 3000) break; // tidak ada data 3 detik → selesai delay(5); } } client.stop(); // --- Pisahkan header dan body --- int bodyIdx = fullResponse.indexOf("\r\n\r\n"); String body = ""; if (bodyIdx != -1) { body = fullResponse.substring(bodyIdx + 4); } else { // Fallback: cari \n\n bodyIdx = fullResponse.indexOf("\n\n"); if (bodyIdx != -1) { body = fullResponse.substring(bodyIdx + 2); } else { body = fullResponse; // tidak ada header sama sekali } } body.trim(); if (body.length() == 0) { Serial.println("āš ļø Response kosong"); Serial.println(" Raw: " + fullResponse.substring(0, 200)); // debug 200 char pertama return ""; } Serial.println("šŸ“© Response: " + body); return body; } // ====================== // HALAMAN CONFIG (HTML) // ====================== const char CONFIG_PAGE[] PROGMEM = R"rawliteral( MIRA - Wi-Fi Setting

MIRA

Money Identification and
Recognition Assistant

Wi-Fi Setting

šŸ‘ļø
)rawliteral"; // ====================== // CONFIG PORTAL (SOFTAP) // ====================== void startConfigPortal() { Serial.println("šŸ“” Hotspot config aktif: MIRA WIFI CONFIG"); WiFi.softAP("MIRA WIFI CONFIG", ""); Serial.println("🌐 Buka 192.168.4.1 di browser"); configServer.on("/", HTTP_GET, []() { configServer.send(200, "text/html", CONFIG_PAGE); }); configServer.on("/save", HTTP_POST, []() { savedSSID = configServer.arg("ssid"); savedPassword = configServer.arg("pass"); String rawServer = configServer.arg("server"); // Parse host, port, path dari input user parseServerString(rawServer); Serial.println("šŸ’¾ Config diterima:"); Serial.println(" SSID : " + savedSSID); configServer.send(200, "text/html", R"(
šŸ”—

Menghubungkan...

ESP32-CAM sedang konek ke WiFi.
Tunggu sebentar.

)"); delay(500); configServer.stop(); WiFi.softAPdisconnect(true); connectWiFi(); }); configServer.begin(); while (savedSSID == "") { configServer.handleClient(); delay(10); } } // ====================== // KONEK WIFI // ====================== void connectWiFi() { Serial.print("šŸ“¶ Konek ke: " + savedSSID); WiFi.begin(savedSSID.c_str(), savedPassword.c_str()); int timeout = 0; while (WiFi.status() != WL_CONNECTED && timeout < 20) { delay(500); Serial.print("."); timeout++; } if (WiFi.status() == WL_CONNECTED) { Serial.println("\nāœ… WiFi Connected!"); Serial.println(" IP ESP32 : " + WiFi.localIP().toString()); Serial.println(" Server : " + savedServer + ":" + String(savedPort) + savedPath); playWAVFromServer("wificonnected.wav"); } else { Serial.println("\nāŒ Gagal konek WiFi!"); playWAVFromServer("wifidisconnect.wav"); savedSSID = ""; startConfigPortal(); } } // ====================== // INIT CAMERA // ====================== void startCamera() { 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; config.frame_size = FRAMESIZE_VGA; config.jpeg_quality = 12; config.fb_count = 2; esp_err_t err = esp_camera_init(&config); if (err != ESP_OK) { Serial.println("āŒ Camera init FAILED"); return; } Serial.println("āœ… Camera OK"); sensor_t *s = esp_camera_sensor_get(); s->set_hmirror(s, 0); s->set_vflip(s, 1); Serial.println("āœ… Mirror & Flip OK"); // Matikan SD card controller agar pin I2S bebas periph_module_disable(PERIPH_SDMMC_MODULE); delay(50); pinMode(2, INPUT_PULLUP); pinMode(13, INPUT_PULLUP); pinMode(14, INPUT_PULLUP); pinMode(15, INPUT_PULLUP); Serial.println("āœ… SD card controller dimatikan"); } // ====================== // AMBIL FRAME SEGAR // ====================== camera_fb_t* getFreshFrame() { camera_fb_t* fb = esp_camera_fb_get(); if (fb) esp_camera_fb_return(fb); fb = esp_camera_fb_get(); if (fb) esp_camera_fb_return(fb); return esp_camera_fb_get(); } // ====================== // SETUP // ====================== void setup() { Serial.begin(115200); delay(1000); // LED nyala 60% kecerahan ledcSetup(2, 5000, 8); ledcAttachPin(LED_PIN, 2); ledcWrite(2, 153); Serial.println("šŸ’” LED ON 60%"); // Vibration motor: getar sesaat saat boot pinMode(VIBRO_PIN, OUTPUT); digitalWrite(VIBRO_PIN, HIGH); delay(400); digitalWrite(VIBRO_PIN, LOW); Serial.println("šŸ“³ Vibrate OK"); Serial.println("šŸš€ START PROGRAM - MIRA ESP32-CAM"); // Init SPIFFS if (!SPIFFS.begin(true)) { Serial.println("āŒ SPIFFS gagal mount!"); } else { Serial.println("āœ… OK"); } // Init kamera dulu sebelum I2S startCamera(); // Selalu buka config portal setiap nyala startConfigPortal(); } // ====================== // LOOP // ====================== void loop() { Serial.println("šŸ“ø Ambil gambar..."); if (WiFi.status() == WL_CONNECTED) { camera_fb_t* fb = getFreshFrame(); if (!fb) { Serial.println("āŒ Camera capture failed"); delay(1000); return; } // Kirim gambar ke server via WiFiClient String response = postImageWiFiClient(fb->buf, fb->len); esp_camera_fb_return(fb); if (response.length() > 0) { playNominal(response); } else { Serial.println("āŒ Tidak ada response dari server"); } } else { // WiFi putus → coba reconnect Serial.println("āš ļø WiFi putus, reconnecting..."); WiFi.begin(savedSSID.c_str(), savedPassword.c_str()); int t = 0; while (WiFi.status() != WL_CONNECTED && t < 10) { delay(500); t++; } if (WiFi.status() != WL_CONNECTED) { Serial.println("āŒ Reconnect gagal. Kembali ke config..."); playWAVFromServer("wifidisconnect.wav"); savedSSID = ""; startConfigPortal(); } } delay(5000); }