Copier le code
/*
* ESP32-CAM Currency Detection + Audio Announcement
* WITH WEB PREVIEW SERVER
*
* Changes from original:
* 1. Camera clarity: UXGA (1600×1200) with quality=8, plus sharpness/denoise tuning.
* 2. Web server on port 80:
* / → Dashboard with live MJPEG stream + last detection result
* /stream → Raw MJPEG stream (open in any browser tab or VLC)
* /capture → Single JPEG snapshot download
* /status → JSON with last currency result and heap info
* 3. Web server runs in a FreeRTOS task on Core 0.
* detectCurrency() still runs on Core 1 (loop).
* 4. During detectCurrency(), camera is deinit'd briefly for RAM —
* the stream task detects this and returns a 503 while detection runs.
*/
#include "esp_camera.h"
#include
#include
#include "soc/soc.h"
#include "soc/rtc_cntl_reg.h"
// Audio libraries
#include "AudioFileSourcePROGMEM.h"
#include "AudioGeneratorMP3.h"
#include "AudioOutputI2SNoDAC.h"
// Web server
#include
// ─── Config ────────────────────────────────────────────────────────
const char* WIFI_SSID = "Yourssidname";
const char* WIFI_PASS = "Yourwifiassword";
const char* API_KEY = "Yourapikey";
const char* serverName = "www.raspberryme.cloud";
const char* serverPath = "/api/v1/currency-detection/detect";
const int serverPort = 443;
// ─── Pins ──────────────────────────────────────────────────────────
#define TRIGGER_BTN 12
#define SPEAKER_PIN 13
// ─── AI-Thinker ESP32-CAM Camera Pins ─────────────────────────────
#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
// ─── Globals ───────────────────────────────────────────────────────
static unsigned long lastTrigger = 0;
static volatile bool cameraActive = true; // false while deinit'd for detection
static String lastCurrency = "None yet";
static AudioOutputI2SNoDAC* g_audioOut = nullptr;
static WebServer webServer(80);
// ─── Utility ───────────────────────────────────────────────────────
static void printHeap(const char* tag) {
Serial.printf("[HEAP] %s free=%u maxBlock=%u psram=%u\n",
tag,
(unsigned)ESP.getFreeHeap(),
(unsigned)ESP.getMaxAllocHeap(),
(unsigned)ESP.getFreePsram());
}
// ─── Camera Init ───────────────────────────────────────────────────
void initCamera() {
pinMode(PWDN_GPIO_NUM, OUTPUT);
digitalWrite(PWDN_GPIO_NUM, HIGH); delay(100);
digitalWrite(PWDN_GPIO_NUM, LOW); delay(100);
camera_config_t cfg = {};
cfg.ledc_channel = LEDC_CHANNEL_0; cfg.ledc_timer = LEDC_TIMER_0;
cfg.pin_d0=Y2_GPIO_NUM; cfg.pin_d1=Y3_GPIO_NUM; cfg.pin_d2=Y4_GPIO_NUM;
cfg.pin_d3=Y5_GPIO_NUM; cfg.pin_d4=Y6_GPIO_NUM; cfg.pin_d5=Y7_GPIO_NUM;
cfg.pin_d6=Y8_GPIO_NUM; cfg.pin_d7=Y9_GPIO_NUM;
cfg.pin_xclk=XCLK_GPIO_NUM; cfg.pin_pclk=PCLK_GPIO_NUM;
cfg.pin_vsync=VSYNC_GPIO_NUM; cfg.pin_href=HREF_GPIO_NUM;
cfg.pin_sscb_sda=SIOD_GPIO_NUM; cfg.pin_sscb_scl=SIOC_GPIO_NUM;
cfg.pin_pwdn=PWDN_GPIO_NUM; cfg.pin_reset=RESET_GPIO_NUM;
// Higher clock = sharper image, less motion blur
cfg.xclk_freq_hz = 20000000;
cfg.pixel_format = PIXFORMAT_JPEG;
if (psramFound()) {
// ── CLARITY UPGRADE ──────────────────────────────────────────
// UXGA = 1600×1200 (was VGA 640×480)
// quality 8 = very high (was 12); lower number = more detail
// fb_count 2 keeps a spare frame ready while one is being sent
cfg.frame_size = FRAMESIZE_UXGA;
cfg.jpeg_quality = 8;
cfg.fb_count = 2;
cfg.fb_location = CAMERA_FB_IN_PSRAM;
} else {
// No PSRAM fallback — SVGA is the best DRAM can handle reliably
cfg.frame_size = FRAMESIZE_SVGA;
cfg.jpeg_quality = 10;
cfg.fb_count = 1;
cfg.fb_location = CAMERA_FB_IN_DRAM;
}
esp_err_t err = esp_camera_init(&cfg);
if (err != ESP_OK) {
Serial.printf("Camera init failed: 0x%x — restarting\n", err);
delay(1000); ESP.restart();
}
// ── SENSOR TUNING FOR CLARITY ────────────────────────────────
sensor_t* s = esp_camera_sensor_get();
if (s) {
s->set_brightness(s, 1); // +1 brightness (range -2..2)
s->set_contrast(s, 2); // max contrast for crisp edges
s->set_saturation(s, 0); // neutral saturation
s->set_sharpness(s, 2); // max sharpness ← NEW
s->set_denoise(s, 1); // light denoise (reduces JPEG noise) ← NEW
s->set_whitebal(s, 1); // auto white balance on
s->set_awb_gain(s, 1); // AWB gain on ← NEW
s->set_exposure_ctrl(s, 1); // auto exposure on
s->set_gain_ctrl(s, 1); // auto gain on
s->set_aec2(s, 1); // AEC DSP on (better exposure in dark)
s->set_ae_level(s, 1); // +1 AE bias (slightly brighter)
s->set_aec_value(s, 300); // initial exposure hint ← NEW
s->set_agc_gain(s, 0); // start at gain 0 (let auto take over)
s->set_gainceiling(s, (gainceiling_t)6); // max gain 128× ← NEW
s->set_bpc(s, 1); // bad pixel correction on ← NEW
s->set_wpc(s, 1); // white pixel correction on ← NEW
s->set_raw_gma(s, 1); // gamma correction on ← NEW
s->set_lenc(s, 1); // lens correction (fixes vignetting) ← NEW
s->set_hmirror(s, 0);
s->set_vflip(s, 0);
// Special: OV2640 DCW (down-sample) off gives sharper large frames
s->set_dcw(s, 0); // ← NEW
}
cameraActive = true;
Serial.println("Camera initialized (UXGA, quality=8, full sensor tuning).");
}
// ─── HTTP body reader ──────────────────────────────────────────────
static size_t readBodyFixed(WiFiClientSecure& c, char* buf, size_t maxLen) {
size_t total = 0;
uint32_t t = millis();
while ((c.connected() || c.available()) && total < maxLen - 1) {
if (millis() - t > 8000) break;
if (!c.available()) { delay(10); continue; }
int n = c.read((uint8_t*)buf + total, maxLen - 1 - total);
if (n > 0) { total += n; t = millis(); }
}
buf[total] = '\0';
return total;
}
static void skipHeaders(WiFiClientSecure& c) {
while (c.connected()) {
String line = c.readStringUntil('\n');
if (line == "\r" || line.length() <= 1) break;
}
}
// ─── Phase 2: Currency API ─────────────────────────────────────────
static String sendImageToAPI(const uint8_t* buf, size_t len) {
WiFiClientSecure client;
client.setInsecure();
client.setTimeout(15);
Serial.println("Connecting to currency API...");
printHeap("before API connect");
int retries = 0;
while (!client.connect(serverName, serverPort)) {
Serial.printf("Connection failed (attempt %d)\n", ++retries);
if (retries >= 3) return "Connection Error";
delay(1500);
}
Serial.println("Connected!");
const char* bnd = "----ESP32Boundary";
char partHead[256];
int phLen = snprintf(partHead, sizeof(partHead),
"--%s\r\n"
"Content-Disposition: form-data; name=\"imageFile\"; filename=\"snap.jpg\"\r\n"
"Content-Type: image/jpeg\r\n\r\n", bnd);
char partTail[64];
int ptLen = snprintf(partTail, sizeof(partTail), "\r\n--%s--\r\n", bnd);
size_t contentLen = phLen + len + ptLen;
client.printf(
"POST %s HTTP/1.1\r\n"
"Host: %s\r\n"
"X-API-Key: %s\r\n"
"Content-Type: multipart/form-data; boundary=%s\r\n"
"Content-Length: %u\r\n"
"Connection: close\r\n\r\n",
serverPath, serverName, API_KEY, bnd, (unsigned)contentLen);
client.write((const uint8_t*)partHead, phLen);
for (size_t off = 0; off < len; off += 1024) {
size_t sz = min((size_t)1024, len - off);
client.write(buf + off, sz);
}
client.write((const uint8_t*)partTail, ptLen);
Serial.println("Image sent! Waiting for response...");
uint32_t t = millis();
while (!client.available() && millis() - t < 12000) delay(10);
skipHeaders(client);
static char respBuf[1024];
readBodyFixed(client, respBuf, sizeof(respBuf));
client.stop();
Serial.printf("API Response: %s\n", respBuf);
String currency = "";
auto findField = [&](const char* key) -> String {
const char* p = strstr(respBuf, key);
if (!p) return "";
p += strlen(key);
const char* e = strchr(p, '"');
if (!e) return "";
return String(p).substring(0, e - p);
};
currency = findField("\"label\":\"");
if (currency == "") currency = findField("\"class\":\"");
if (currency == "") currency = findField("\"class_name\":\"");
if (currency == "") currency = findField("\"denomination\":\"");
if (currency == "" && (strstr(respBuf,"no_currency_found") || strstr(respBuf,"no_detections")))
currency = "No currency detected";
if (currency == "") currency = "Unknown currency";
const char* rupeeVals[] = {"10","20","50","100","200","500","2000"};
for (auto v : rupeeVals) if (currency == v) { currency += " rupees"; break; }
return currency;
}
// ─── Phase 3: TTS Download ─────────────────────────────────────────
static uint8_t* downloadTTS(const String& text, size_t* outLen) {
*outLen = 0;
String encoded = text;
encoded.replace(" ", "+");
String path = "/translate_tts?ie=UTF-8&q=" + encoded + "&tl=en&client=tw-ob";
WiFiClient httpClient;
if (!httpClient.connect("translate.google.com", 80)) {
Serial.println("[TTS] HTTP connect failed"); return nullptr;
}
httpClient.printf(
"GET %s HTTP/1.1\r\nHost: translate.google.com\r\nUser-Agent: Mozilla/5.0\r\nConnection: close\r\n\r\n",
path.c_str());
uint32_t t = millis();
while (!httpClient.available() && millis() - t < 8000) delay(10);
size_t contentLength = 0;
while (httpClient.connected()) {
String line = httpClient.readStringUntil('\n'); line.trim();
if (line.startsWith("Content-Length:")) contentLength = (size_t)line.substring(16).toInt();
if (line.length() == 0) break;
}
size_t bufCap = contentLength > 0 ? contentLength + 64 : 131072;
uint8_t* mp3buf = nullptr;
if (psramFound() && ESP.getFreePsram() > bufCap + 65536)
mp3buf = (uint8_t*)ps_malloc(bufCap);
if (!mp3buf) {
Serial.printf("[TTS] Cannot allocate %u bytes\n", (unsigned)bufCap);
httpClient.stop(); return nullptr;
}
size_t total = 0;
t = millis();
while ((httpClient.connected() || httpClient.available()) && total < bufCap) {
if (millis() - t > 15000) break;
if (!httpClient.available()) { delay(5); continue; }
int n = httpClient.read(mp3buf + total, bufCap - total);
if (n > 0) { total += n; t = millis(); }
}
httpClient.stop();
if (total == 0) { Serial.println("[TTS] No MP3 data"); free(mp3buf); return nullptr; }
Serial.printf("[TTS] Downloaded %u bytes\n", (unsigned)total);
*outLen = total;
return mp3buf;
}
// ─── Phase 4: Playback ─────────────────────────────────────────────
static void playMP3(const uint8_t* mp3data, size_t mp3len) {
if (!mp3data || mp3len == 0) return;
printHeap("before playMP3");
AudioFileSourcePROGMEM* src = new AudioFileSourcePROGMEM(mp3data, mp3len);
AudioGeneratorMP3* mp3 = new AudioGeneratorMP3();
if (mp3->begin(src, g_audioOut)) {
while (mp3->isRunning()) { if (!mp3->loop()) mp3->stop(); }
Serial.println("[TTS] Playback complete.");
} else {
Serial.println("[TTS] MP3 begin() failed");
}
delete mp3; delete src;
}
// ═══════════════════════════════════════════════════════════════════
// WEB SERVER — handlers
// ═══════════════════════════════════════════════════════════════════
// ── Dashboard HTML ─────────────────────────────────────────────────
static const char DASHBOARD_HTML[] PROGMEM = R"rawhtml(
ESP32-CAM Currency Detector
CAM
Detecting…
Last Detection
—
Heap: —
PSRAM: —
Uptime: —
)rawhtml";
// ── Handler: dashboard ─────────────────────────────────────────────
void handleRoot() {
webServer.send_P(200, "text/html", DASHBOARD_HTML);
}
// ── Handler: MJPEG stream ──────────────────────────────────────────
void handleStream() {
if (!cameraActive) {
webServer.send(503, "text/plain", "Camera busy during detection");
return;
}
WiFiClient client = webServer.client();
client.print(
"HTTP/1.1 200 OK\r\n"
"Content-Type: multipart/x-mixed-replace; boundary=frame\r\n"
"Cache-Control: no-cache\r\n"
"Connection: close\r\n\r\n");
while (client.connected()) {
if (!cameraActive) break; // detection started — bail out
camera_fb_t* fb = esp_camera_fb_get();
if (!fb) { delay(30); continue; }
client.printf(
"--frame\r\n"
"Content-Type: image/jpeg\r\n"
"Content-Length: %u\r\n\r\n", (unsigned)fb->len);
client.write(fb->buf, fb->len);
client.print("\r\n");
esp_camera_fb_return(fb);
delay(50); // ~20 fps cap — raise to 33ms for ~30 fps if your network handles it
}
}
// ── Handler: single snapshot ───────────────────────────────────────
void handleCapture() {
if (!cameraActive) {
webServer.send(503, "text/plain", "Camera busy");
return;
}
camera_fb_t* fb = esp_camera_fb_get();
if (!fb) { webServer.send(500, "text/plain", "Capture failed"); return; }
webServer.sendHeader("Content-Disposition", "inline; filename=\"snap.jpg\"");
webServer.sendHeader("Cache-Control", "no-cache");
webServer.send_P(200, "image/jpeg", (const char*)fb->buf, fb->len);
esp_camera_fb_return(fb);
}
// ── Handler: JSON status ───────────────────────────────────────────
void handleStatus() {
char buf[256];
snprintf(buf, sizeof(buf),
"{\"currency\":\"%s\","
"\"busy\":%s,"
"\"heap_free\":%u,"
"\"psram_free\":%u,"
"\"uptime_s\":%lu}",
lastCurrency.c_str(),
cameraActive ? "false" : "true",
(unsigned)ESP.getFreeHeap(),
(unsigned)ESP.getFreePsram(),
millis() / 1000UL);
webServer.send(200, "application/json", buf);
}
// ── Web server task (Core 0) ───────────────────────────────────────
static void webServerTask(void*) {
webServer.on("https://raspberryme.com/", handleRoot);
webServer.on("/stream", handleStream);
webServer.on("https://raspberryme.com/capture", handleCapture);
webServer.on("/status", handleStatus);
webServer.begin();
Serial.println("[WEB] Server started on port 80");
for (;;) { webServer.handleClient(); delay(2); }
}
// ═══════════════════════════════════════════════════════════════════
// DETECTION FLOW
// ═══════════════════════════════════════════════════════════════════
void detectCurrency() {
printHeap("detectCurrency start");
// Phase 1: Capture
camera_fb_t* fb = esp_camera_fb_get();
if (fb) esp_camera_fb_return(fb);
delay(250);
fb = esp_camera_fb_get();
if (!fb) { Serial.println("Capture failed!"); return; }
size_t imgLen = fb->len;
Serial.printf("Frame: %u bytes\n", (unsigned)imgLen);
uint8_t* imgBuf = nullptr;
if (psramFound() && ESP.getFreePsram() > imgLen + 65536)
imgBuf = (uint8_t*)ps_malloc(imgLen);
if (!imgBuf && ESP.getMaxAllocHeap() > imgLen + 32768)
imgBuf = (uint8_t*)malloc(imgLen);
if (!imgBuf) {
Serial.println("No memory for frame!");
esp_camera_fb_return(fb);
return;
}
memcpy(imgBuf, fb->buf, imgLen);
esp_camera_fb_return(fb);
// Deinit camera — signal web task to stop streaming
cameraActive = false;
delay(50); // let any in-flight stream handler finish its current frame
esp_camera_deinit();
delay(150);
printHeap("after cam deinit");
// Phase 2: API
String currency = sendImageToAPI(imgBuf, imgLen);
free(imgBuf);
lastCurrency = currency;
Serial.println("=== Detection Result ===");
Serial.println(currency);
Serial.println("========================");
printHeap("after API, before TTS download");
// Phase 3: Download TTS
size_t mp3Len = 0;
uint8_t* mp3Buf = downloadTTS(currency, &mp3Len);
// Phase 4: Play
if (mp3Buf) { playMP3(mp3Buf, mp3Len); free(mp3Buf); }
else { Serial.println("[TTS] Skipping audio (download failed)"); }
// Reinit camera and re-enable stream
initCamera();
cameraActive = true;
printHeap("detectCurrency end");
}
// ═══════════════════════════════════════════════════════════════════
// SETUP / LOOP
// ═══════════════════════════════════════════════════════════════════
void setup() {
WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);
Serial.begin(115200);
delay(500);
pinMode(TRIGGER_BTN, INPUT_PULLUP);
initCamera();
g_audioOut = new AudioOutputI2SNoDAC();
g_audioOut->SetPinout(14, 15, SPEAKER_PIN);
WiFi.mode(WIFI_STA);
WiFi.setSleep(false);
WiFi.begin(WIFI_SSID, WIFI_PASS);
Serial.print("Connecting to WiFi");
while (!WiFi.isConnected()) { delay(500); Serial.print("."); }
Serial.printf("\nConnected: %s\n", WiFi.localIP().toString().c_str());
// Print the URL prominently
Serial.println("─────────────────────────────────────────");
Serial.printf(" WEB PREVIEW → http://%s\n", WiFi.localIP().toString().c_str());
Serial.printf(" MJPEG STREAM → http://%s/stream\n", WiFi.localIP().toString().c_str());
Serial.println("─────────────────────────────────────────");
configTime(19800, 0, "pool.ntp.org", "time.nist.gov");
Serial.print("Syncing time");
for (int i = 0; i < 20 && time(nullptr) < 100000; i++) { delay(500); Serial.print("."); }
Serial.println(time(nullptr) > 100000 ? "\nTime synced!" : "\nTime sync timeout");
// Start web server on Core 0 (loop runs on Core 1 by default)
xTaskCreatePinnedToCore(webServerTask, "WebSrv", 8192, nullptr, 1, nullptr, 0);
printHeap("setup complete");
Serial.println("Ready — press button to detect currency.");
}
void loop() {
if (digitalRead(TRIGGER_BTN) == LOW && millis() - lastTrigger > 2000) {
lastTrigger = millis();
delay(150);
Serial.println("Button pressed! Detecting currency...");
detectCurrency();
}
delay(10);
}
Retrouvez l’histoire de Raspberry Pi dans cette vidéo :

