ta-azis-pendeteksi-nominal-.../mira_esp32cam2/mira_esp32cam2.ino

727 lines
21 KiB
C++

#include "esp_camera.h"
#include <WiFi.h>
#include <WiFiClient.h>
#include <WebServer.h>
#include <SPIFFS.h>
#include <driver/i2s.h>
// 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/<filename> → 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(
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>MIRA - Wi-Fi Setting</title>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Poppins', sans-serif;
background: #6D60B4;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.wrapper {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
max-width: 360px;
gap: 20px;
}
.brand { text-align: center; color: white; }
.brand h2 { font-size: 28px; font-weight: 700; letter-spacing: 2px; line-height: 1; }
.brand p { font-size: 11px; opacity: 0.9; margin-top: 6px; line-height: 1.5; font-weight: 400; }
.card { background: #FFFFFF; border-radius: 24px; padding: 32px 28px 28px; width: 100%; }
.card h2 { font-size: 20px; font-weight: 700; color: #6D60B4; text-align: center; margin-bottom: 28px; }
.field { margin-bottom: 18px; }
.field label { display: block; font-size: 12px; color: #555; margin-bottom: 6px; font-weight: 400; }
.field input {
width: 100%; padding: 12px 16px;
border: 2px solid #C4BFDF; border-radius: 40px;
background: #ECEAF6; font-family: 'Poppins', sans-serif;
font-size: 14px; color: #6D60B4; outline: none; transition: border 0.2s;
}
.field input:focus { border-color: #6D60B4; }
.field input::placeholder { color: #B0AAD4; }
.btn {
width: 100%; padding: 13px; background: #6D60B4; color: white;
border: none; border-radius: 40px; font-family: 'Poppins', sans-serif;
font-size: 15px; font-weight: 600; cursor: pointer; margin-top: 8px; transition: background 0.2s;
}
.btn:hover { background: #5c50a0; }
.btn:active { background: #4e4490; }
</style>
</head>
<body>
<div class="wrapper">
<div class="brand">
<h2>MIRA</h2>
<p>Money Identification and<br>Recognition Assistant</p>
</div>
<div class="card">
<h2>Wi-Fi Setting</h2>
<form method="POST" action="/save">
<div class="field">
<label>SSID WiFi</label>
<input type="text" name="ssid" placeholder="Nama WiFi" required />
</div>
<div class="field">
<label>Password WiFi</label>
<div style="position:relative;">
<input type="password" name="pass" id="passInput" placeholder="Password WiFi"
style="width:100%;padding:12px 44px 12px 16px;border:2px solid #C4BFDF;
border-radius:40px;background:#ECEAF6;font-family:'Poppins',sans-serif;
font-size:14px;color:#6D60B4;outline:none;" />
<span onclick="togglePass()" id="eyeBtn"
style="position:absolute;right:16px;top:50%;transform:translateY(-50%);
cursor:pointer;font-size:18px;user-select:none;">👁️</span>
</div>
</div>
<div class="field">
<label>IP Server</label>
<input type="text" name="server" placeholder="192.168.x.x" required />
</div>
<button class="btn" type="submit">Connect</button>
</form>
</div>
</div>
<script>
function togglePass() {
var input = document.getElementById("passInput");
var btn = document.getElementById("eyeBtn");
if (input.type === "password") {
input.type = "text";
btn.textContent = "🙈";
} else {
input.type = "password";
btn.textContent = "👁️";
}
}
</script>
</body>
</html>
)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"(
<!DOCTYPE html><html><head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;700&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box;}
body{font-family:'Poppins',sans-serif;background:#6D60B4;min-height:100vh;
display:flex;align-items:center;justify-content:center;}
.card{background:white;border-radius:24px;padding:40px 32px;text-align:center;max-width:320px;width:100%;}
.icon{font-size:48px;margin-bottom:16px;}
h2{color:#6D60B4;font-size:20px;font-weight:700;margin-bottom:10px;}
p{color:#888;font-size:13px;line-height:1.7;}
</style></head>
<body><div class="card">
<div class="icon">🔗</div>
<h2>Menghubungkan...</h2>
<p>ESP32-CAM sedang konek ke WiFi.<br>Tunggu sebentar.</p>
</div></body></html>
)");
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);
}