727 lines
21 KiB
C++
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);
|
|
} |