#include "camera_pins.h"
#include <WiFi.h>
#include "esp_camera.h"
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
// Edge Impulse模型库(需手动添加到项目目录)
#include "shibie_inferencing.h"
#include "edge-impulse-sdk/dsp/image/image.hpp"
#include "esp_task_wdt.h"
#include "freertos/semphr.h"
#include <SD_MMC.h> // SD卡库(使用SPI或SDMMC接口)
#include <SPIFFS.h>
#include <SD.h> // SPI接口SD卡库(新增)
#include <Time.h> // 时间函数库(新增)
#include "esp_task_wdt.h"
#include "freertos/semphr.h"
// 功能开关
#define ENABLE_INFERENCE 1
#define ENABLE_HTTP_SERVER 1
#define ENABLE_OLED_DISPLAY 1
#define ENABLE_SD_CARD 1 // 启用SD卡功能
#define SUPPORT_OBJECT_DETECTION 0
// 摄像头配置
#define CAMERA_MODEL_AI_THINKER
#define XCLK_FREQ_HZ 2000000 // 降低时钟频率
#define FRAME_SIZE FRAMESIZE_QVGA // 320x240分辨率
#define JPEG_QUALITY 12
#define MAX_CAPTURE_RETRIES 3
// 图像尺寸
#define EI_CAMERA_COLS 320
#define EI_CAMERA_ROWS 240
#define MODEL_INPUT_WIDTH EI_CLASSIFIER_INPUT_WIDTH
#define MODEL_INPUT_HEIGHT EI_CLASSIFIER_INPUT_HEIGHT
// OLED配置
#define OLED_WIDTH 128
#define OLED_HEIGHT 64
#define OLED_RESET -1
#define OLED_SDA 21
#define OLED_SCL 22
Adafruit_SSD1306 display(OLED_WIDTH, OLED_HEIGHT, &Wire, OLED_RESET);
// WiFi配置
const char* ssid = "88888888";
const char* password = "11111111";
// SD卡配置(SPI接口)
#define SD_MOUNT_POINT "/sdcard"
#define SD_PIN_CS 15 // 改为常用的SPI CS引脚(原5可能冲突)
// 全局变量
WiFiServer server(80);
static bool is_initialised = false;
static bool wifi_connected = false;
static bool sd_ready = false;
uint8_t* model_buf = NULL;
camera_fb_t* fb = NULL;
SemaphoreHandle_t camera_mutex = NULL;
SemaphoreHandle_t sd_mutex = NULL;
// 摄像头配置
static camera_config_t camera_config = {
.pin_pwdn = PWDN_GPIO_NUM,
.pin_reset = RESET_GPIO_NUM,
.pin_xclk = XCLK_GPIO_NUM,
.pin_sscb_sda = SIOD_GPIO_NUM,
.pin_sscb_scl = SIOC_GPIO_NUM,
.pin_d7 = Y9_GPIO_NUM,
.pin_d6 = Y8_GPIO_NUM,
.pin_d5 = Y7_GPIO_NUM,
.pin_d4 = Y6_GPIO_NUM,
.pin_d3 = Y5_GPIO_NUM,
.pin_d2 = Y4_GPIO_NUM,
.pin_d1 = Y3_GPIO_NUM,
.pin_d0 = Y2_GPIO_NUM,
.pin_vsync = VSYNC_GPIO_NUM,
.pin_href = HREF_GPIO_NUM,
.pin_pclk = PCLK_GPIO_NUM,
.xclk_freq_hz = XCLK_FREQ_HZ,
.ledc_timer = LEDC_TIMER_0,
.ledc_channel = LEDC_CHANNEL_0,
.pixel_format = PIXFORMAT_JPEG,
.frame_size = FRAME_SIZE,
.jpeg_quality = JPEG_QUALITY,
.fb_count = 1,
.fb_location = CAMERA_FB_IN_PSRAM,
.grab_mode = CAMERA_GRAB_WHEN_EMPTY,
};
/* -------------------------- 时间初始化 -------------------------- */
// 初始化时间(使用NTP同步)
void init_time() {
configTime(8 * 3600, 0, "pool.ntp.org", "time.nist.gov"); // 北京时间(UTC+8)
Serial.println("等待时间同步...");
time_t now = time(nullptr);
int retry_count = 0;
// 等待时间同步,超时则跳过
while (now < 1609459200 && retry_count < 10) { // 等待同步到2021年之后
delay(1000);
Serial.print(".");
now = time(nullptr);
retry_count++;
}
if (now >= 1609459200) {
Serial.println("\n时间同步完成");
struct tm* tm_info = localtime(&now);
char time_str[26];
strftime(time_str, 26, "%Y-%m-%d %H:%M:%S", tm_info);
Serial.print("当前时间: ");
Serial.println(time_str);
} else {
Serial.println("\n时间同步超时,使用默认时间");
}
}
/* -------------------------- SD卡操作 -------------------------- */
#ifdef ENABLE_SD_CARD
bool sd_init() {
sd_mutex = xSemaphoreCreateMutex();
if (!sd_mutex) {
Serial.println("SD卡互斥锁创建失败");
return false;
}
Serial.println("初始化SD卡...");
// 尝试SDMMC模式(仅适用于支持SDMMC的板子)
if (!SD_MMC.begin("/sdcard", true)) { // 1线模式
Serial.println("SDMMC模式失败,尝试SPI模式...");
// SPI模式初始化(低速模式提高兼容性)
if (!SD.begin(SD_PIN_CS, SPI, 1000000)) { // 1MHz低速率
Serial.println("SPI模式初始化失败,检查:");
Serial.println("1. 引脚连接是否正确(CS=" + String(SD_PIN_CS) + ", SCK=18, MOSI=23, MISO=19)");
Serial.println("2. SD卡是否为FAT32格式");
Serial.println("3. 尝试更换SD卡");
return false;
}
}
// 验证SD卡读写功能
File testFile = SD.open("/test.txt", FILE_WRITE);
if (testFile) {
testFile.println("SD卡测试成功");
testFile.close();
// 读取测试
testFile = SD.open("/test.txt", FILE_READ);
if (testFile) {
String content = testFile.readString();
testFile.close();
SD.remove("/test.txt"); // 删除测试文件
Serial.println("SD卡读写验证成功: " + content);
}
} else {
Serial.println("SD卡写入测试失败");
return false;
}
// 创建目录
if (!SD.exists("/logs")) SD.mkdir("/logs");
if (!SD.exists("/images")) SD.mkdir("/images");
sd_ready = true;
Serial.println("SD卡初始化成功");
return true;
}
// 写入日志到SD卡
void sd_log(const char* message) {
if (!sd_ready) return;
if (xSemaphoreTake(sd_mutex, 1000 / portTICK_PERIOD_MS) != pdTRUE) return;
time_t now = time(nullptr);
struct tm* tm = localtime(&now);
char filename[32];
sprintf(filename, "/logs/%04d%02d%02d.txt",
tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday);
File file = SD.open(filename, FILE_APPEND);
if (file) {
char timestamp[32];
sprintf(timestamp, "[%02d:%02d:%02d] ",
tm->tm_hour, tm->tm_min, tm->tm_sec);
file.print(timestamp);
file.println(message);
file.close();
}
xSemaphoreGive(sd_mutex);
}
// 保存图像到SD卡
bool sd_save_image(const uint8_t* data, size_t size) {
if (!sd_ready || !data || size == 0) return false;
if (xSemaphoreTake(sd_mutex, 1000 / portTICK_PERIOD_MS) != pdTRUE) return false;
time_t now = time(nullptr);
struct tm* tm = localtime(&now);
char filename[64];
sprintf(filename, "/images/img_%04d%02d%02d_%02d%02d%02d.jpg",
tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday,
tm->tm_hour, tm->tm_min, tm->tm_sec);
File file = SD.open(filename, FILE_WRITE);
if (!file) {
xSemaphoreGive(sd_mutex);
return false;
}
bool success = (file.write(data, size) == size);
file.close();
xSemaphoreGive(sd_mutex);
if (success) {
Serial.printf("图像保存到: %s\n", filename);
sd_log((String("图像保存: ") + filename).c_str());
}
return success;
}
// 从SD卡读取图像
size_t sd_read_image(const char* filename, uint8_t* buffer, size_t max_size) {
if (!sd_ready || !buffer || max_size == 0) return 0;
if (xSemaphoreTake(sd_mutex, 1000 / portTICK_PERIOD_MS) != pdTRUE) return 0;
File file = SD.open(filename, FILE_READ);
if (!file) {
xSemaphoreGive(sd_mutex);
return 0;
}
size_t size = file.size();
if (size > max_size) size = max_size;
size_t read = file.read(buffer, size);
file.close();
xSemaphoreGive(sd_mutex);
return read;
}
#endif
/* -------------------------- 图像处理函数 -------------------------- */
bool convert_jpeg_to_rgb888_from_sd(const char* jpeg_path, uint8_t* rgb_data, size_t rgb_size) {
if (!sd_ready || !rgb_data) return false;
File file = SD.open(jpeg_path, FILE_READ);
if (!file) return false;
size_t jpeg_size = file.size();
if (jpeg_size == 0 || jpeg_size > 1024*200) {
file.close();
return false;
}
uint8_t* jpeg_buf = (uint8_t*)ps_malloc(jpeg_size);
if (!jpeg_buf) {
file.close();
return false;
}
size_t read = file.read(jpeg_buf, jpeg_size);
file.close();
if (read != jpeg_size) {
free(jpeg_buf);
return false;
}
bool success = fmt2rgb888(jpeg_buf, jpeg_size, PIXFORMAT_JPEG, rgb_data);
free(jpeg_buf);
return success;
}
// 备用缩放函数(当默认缩放失败时使用)
bool backup_resize_rgb888(const uint8_t* src, uint32_t src_width, uint32_t src_height,
uint8_t* dst, uint32_t dst_width, uint32_t dst_height) {
if (!src || !dst || src_width == 0 || src_height == 0 || dst_width == 0 || dst_height == 0) {
return false;
}
if (((uintptr_t)dst) % 4 != 0) {
Serial.println("错误:输出缓冲区未4字节对齐");
return false;
}
float x_ratio = (float)src_width / (float)dst_width;
float y_ratio = (float)src_height / (float)dst_height;
for (uint32_t y = 0; y < dst_height; y++) {
for (uint32_t x = 0; x < dst_width; x++) {
float src_x = x * x_ratio;
float src_y = y * y_ratio;
uint32_t x1 = (uint32_t)src_x;
uint32_t y1 = (uint32_t)src_y;
uint32_t x2 = (x1 < src_width - 1) ? x1 + 1 : x1;
uint32_t y2 = (y1 < src_height - 1) ? y1 + 1 : y1;
float fx = src_x - x1;
float fy = src_y - y1;
for (uint8_t c = 0; c < 3; c++) {
uint8_t v11 = src[(y1 * src_width + x1) * 3 + c];
uint8_t v12 = src[(y2 * src_width + x1) * 3 + c];
uint8_t v21 = src[(y1 * src_width + x2) * 3 + c];
uint8_t v22 = src[(y2 * src_width + x2) * 3 + c];
uint8_t v1 = (uint8_t)((1 - fx) * v11 + fx * v21);
uint8_t v2 = (uint8_t)((1 - fx) * v12 + fx * v22);
dst[(y * dst_width + x) * 3 + c] = (uint8_t)((1 - fy) * v1 + fy * v2);
}
}
}
return true;
}
/* -------------------------- 工具函数 -------------------------- */
void oled_print(const char* line1, const char* line2 = "", const char* line3 = "") {
#ifdef ENABLE_OLED_DISPLAY
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println(line1);
if (strlen(line2) > 0) {
display.setCursor(0, 16);
display.println(line2);
}
if (strlen(line3) > 0) {
display.setCursor(0, 32);
display.println(line3);
}
display.display();
#endif
}
/* -------------------------- 摄像头操作 -------------------------- */
bool camera_init() {
if (is_initialised) return true;
Serial.println("\n===== 摄像头初始化 =====");
camera_mutex = xSemaphoreCreateMutex();
#ifdef ENABLE_SD_CARD
// 初始化SD卡(允许失败,不影响主功能)
if (!sd_init()) {
Serial.println("SD卡初始化失败,继续启动系统");
}
#endif
gpio_uninstall_isr_service();
esp_err_t err;
int init_retry = 0;
while (init_retry < 3) {
err = esp_camera_init(&camera_config);
if (err == ESP_OK) break;
Serial.printf("摄像头初始化失败(重试%d): %s\n",
init_retry+1, esp_err_to_name(err));
#ifdef ENABLE_SD_CARD
if (sd_ready) {
sd_log((String("摄像头初始化失败: ") + esp_err_to_name(err)).c_str());
}
#endif
if (err == ESP_ERR_INVALID_STATE) {
gpio_uninstall_isr_service();
}
init_retry++;
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
if (err != ESP_OK) {
Serial.println("摄像头初始化彻底失败");
#ifdef ENABLE_SD_CARD
if (sd_ready) {
sd_log("摄像头初始化彻底失败");
}
#endif
return false;
}
sensor_t* s = esp_camera_sensor_get();
if (s) {
s->set_vflip(s, 1);
s->set_hmirror(s, 1);
s->set_awb_gain(s, 1);
}
size_t model_size = MODEL_INPUT_WIDTH * MODEL_INPUT_HEIGHT * 3;
model_size = (model_size + 3) & ~3; // 4字节对齐
model_buf = (uint8_t*)aligned_alloc(4, model_size);
if (!model_buf) {
Serial.println("模型缓冲区分配失败");
camera_deinit();
return false;
}
Serial.printf("模型缓冲区: %zu bytes\n", model_size);
is_initialised = true;
Serial.println("摄像头初始化成功");
#ifdef ENABLE_SD_CARD
if (sd_ready) {
sd_log("摄像头初始化成功");
}
#endif
return true;
}
void camera_deinit() {
if (!is_initialised) return;
esp_camera_deinit();
if (model_buf) free(model_buf);
model_buf = NULL;
is_initialised = false;
}
bool camera_capture_to_sd() {
if (!is_initialised) return false;
if (xSemaphoreTake(camera_mutex, 2000 / portTICK_PERIOD_MS) != pdTRUE) {
return false;
}
bool success = false;
fb = esp_camera_fb_get();
if (fb && fb->len > 1024) {
#ifdef ENABLE_SD_CARD
if (sd_ready) {
success = sd_save_image(fb->buf, fb->len);
}
#endif
esp_camera_fb_return(fb);
fb = NULL;
}
xSemaphoreGive(camera_mutex);
return success;
}
bool process_image_from_sd(uint8_t* model_buf, size_t model_size) {
if (!sd_ready || !model_buf) return false;
File dir = SD.open("/images");
if (!dir) return false;
File latest_file;
time_t latest_time = 0;
while (File file = dir.openNextFile()) {
if (!file.isDirectory() && strstr(file.name(), ".jpg")) {
time_t t = file.getLastWrite();
if (t > latest_time) {
latest_time = t;
if (latest_file) latest_file.close();
latest_file = file;
} else {
file.close();
}
}
}
dir.close();
if (!latest_file) return false;
const char* img_path = latest_file.path();
latest_file.close();
size_t rgb_size = EI_CAMERA_COLS * EI_CAMERA_ROWS * 3;
uint8_t* rgb_buf = (uint8_t*)ps_malloc(rgb_size);
if (!rgb_buf) return false;
bool success = false;
if (convert_jpeg_to_rgb888_from_sd(img_path, rgb_buf, rgb_size)) {
success = ei::image::processing::crop_and_interpolate_rgb888(
rgb_buf, EI_CAMERA_COLS, EI_CAMERA_ROWS,
model_buf, MODEL_INPUT_WIDTH, MODEL_INPUT_HEIGHT
);
if (!success) {
success = backup_resize_rgb888(
rgb_buf, EI_CAMERA_COLS, EI_CAMERA_ROWS,
model_buf, MODEL_INPUT_WIDTH, MODEL_INPUT_HEIGHT
);
}
}
free(rgb_buf);
return success;
}
/* -------------------------- 推理核心 -------------------------- */
#ifdef ENABLE_INFERENCE
void run_inference() {
if (!is_initialised || !model_buf) {
oled_print("未就绪", "设备异常");
return;
}
Serial.println("\n===== 开始推理 =====");
oled_print("识别中...");
if (!camera_capture_to_sd()) {
oled_print("处理失败", "图像捕获失败");
#ifdef ENABLE_SD_CARD
if (sd_ready) {
sd_log("图像捕获失败");
}
#endif
return;
}
if (!process_image_from_sd(model_buf, MODEL_INPUT_WIDTH * MODEL_INPUT_HEIGHT * 3)) {
oled_print("处理失败", "图像转换失败");
#ifdef ENABLE_SD_CARD
if (sd_ready) {
sd_log("图像转换失败");
}
#endif
return;
}
ei::signal_t signal;
signal.total_length = MODEL_INPUT_WIDTH * MODEL_INPUT_HEIGHT;
signal.get_data = [](size_t offset, size_t length, float* out) {
size_t pixel_ix = offset * 3;
if (pixel_ix + 3*length > MODEL_INPUT_WIDTH * MODEL_INPUT_HEIGHT * 3) {
Serial.println("错误:模型输入数据越界");
return -1;
}
for (size_t i = 0; i < length; i++) {
uint8_t r = model_buf[pixel_ix];
uint8_t g = model_buf[pixel_ix + 1];
uint8_t b = model_buf[pixel_ix + 2];
out[i] = (r - 127.5f) / 127.5f;
out[i + length] = (g - 127.5f) / 127.5f;
out[i + length * 2] = (b - 127.5f) / 127.5f;
pixel_ix += 3;
}
return 0;
};
ei_impulse_result_t result;
memset(&result, 0, sizeof(result));
EI_IMPULSE_ERROR err = run_classifier(&signal, &result, false);
if (err != EI_IMPULSE_OK) {
Serial.printf("推理失败: %d\n", err);
oled_print("推理错误", String(err).c_str());
#ifdef ENABLE_SD_CARD
if (sd_ready) {
sd_log((String("推理失败: ") + String(err)).c_str());
}
#endif
return;
}
float max_prob = 0;
const char* max_label = "未知";
for (uint16_t i = 0; i < EI_CLASSIFIER_LABEL_COUNT; i++) {
if (result.classification[i].value > max_prob) {
max_prob = result.classification[i].value;
max_label = ei_classifier_inferencing_categories[i];
}
}
Serial.printf("识别结果: %s (%.1f%%)\n", max_label, max_prob*100);
oled_print("识别为:", max_label, (String(max_prob*100, 1) + "%").c_str());
#ifdef ENABLE_SD_CARD
if (sd_ready) {
sd_log((String("识别结果: ") + max_label + " (" + String(max_prob*100) + "%)").c_str());
}
#endif
}
#endif
/* -------------------------- HTTP服务 -------------------------- */
#ifdef ENABLE_HTTP_SERVER
void handle_client(WiFiClient& client) {
String req = client.readStringUntil('\n');
req.trim();
Serial.println("HTTP请求: " + req);
if (req.startsWith("GET /photo")) {
client.println("HTTP/1.1 200 OK");
client.println("Content-Type: image/jpeg");
client.println("Connection: close");
client.println();
#ifdef ENABLE_SD_CARD
if (sd_ready) {
File dir = SD.open("/images");
File latest_file;
while (File file = dir.openNextFile()) {
if (!file.isDirectory() && strstr(file.name(), ".jpg")) {
if (!latest_file || file.getLastWrite() > latest_file.getLastWrite()) {
if (latest_file) latest_file.close();
latest_file = file;
} else {
file.close();
}
}
}
dir.close();
if (latest_file) {
size_t fileSize = latest_file.size();
const size_t bufferSize = 1024;
uint8_t buffer[bufferSize];
while (fileSize > 0) {
size_t read = latest_file.read(buffer, min(bufferSize, fileSize));
client.write(buffer, read);
fileSize -= read;
}
latest_file.close();
} else {
client.print("无图像文件");
}
} else {
client.print("SD卡未就绪");
}
#else
client.print("SD卡功能未启用");
#endif
} else if (req.startsWith("GET /infer")) {
client.println("HTTP/1.1 200 OK");
client.println("Content-Type: text/plain");
client.println("Connection: close");
client.println();
run_inference();
client.println("推理已触发");
} else if (req.startsWith("GET /")) {
client.println("HTTP/1.1 200 OK");
client.println("Content-Type: text/html");
client.println("Connection: close");
client.println();
client.println("<html><body>");
client.println("<h1>ESP32 识别系统</h1>");
client.println("<a href=\"/photo\">查看最新照片</a><br>");
client.println("<a href=\"/infer\">运行识别</a><br>");
client.println("</body></html>");
} else {
client.println("HTTP/1.1 404 Not Found");
client.println("Content-Type: text/plain");
client.println("Connection: close");
client.println();
client.println("404 Not Found");
}
delay(1);
client.stop();
}
#endif
/* -------------------------- WiFi连接 -------------------------- */
void connect_wifi() {
if (wifi_connected) return;
Serial.println("\n===== WiFi连接 =====");
oled_print("连接WiFi...", ssid);
WiFi.begin(ssid, password);
int attempt = 0;
while (WiFi.status() != WL_CONNECTED && attempt < 20) {
delay(500);
Serial.print(".");
attempt++;
}
if (WiFi.status() == WL_CONNECTED) {
wifi_connected = true;
Serial.println("\nWiFi连接成功");
Serial.print("IP地址: ");
Serial.println(WiFi.localIP());
oled_print("WiFi已连接", WiFi.localIP().toString().c_str());
#ifdef ENABLE_HTTP_SERVER
server.begin();
Serial.println("HTTP服务启动");
#endif
#ifdef ENABLE_SD_CARD
if (sd_ready) {
sd_log((String("WiFi连接成功 - IP: ") + WiFi.localIP().toString()).c_str());
}
#endif
} else {
Serial.println("\nWiFi连接失败");
oled_print("WiFi连接失败", "请检查配置");
#ifdef ENABLE_SD_CARD
if (sd_ready) {
sd_log("WiFi连接失败");
}
#endif
}
}
/* -------------------------- 初始化和主循环 -------------------------- */
void setup() {
Serial.begin(115200);
Serial.println("Edge Impulse 识别系统启动中");
// 初始化OLED
#ifdef ENABLE_OLED_DISPLAY
Wire.begin(OLED_SDA, OLED_SCL);
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306 OLED显示屏初始化失败"));
} else {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println("Edge Impulse");
display.println("识别系统启动中");
display.display();
}
#endif
// 初始化时间
init_time();
// 初始化摄像头
if (!camera_init()) {
oled_print("错误", "摄像头初始化失败");
delay(5000);
}
// 连接WiFi
connect_wifi();
// 启动看门狗
esp_task_wdt_init(10, false);
}
void loop() {
// 喂狗
esp_task_wdt_reset();
// 检查WiFi连接
if (!wifi_connected || WiFi.status() != WL_CONNECTED) {
wifi_connected = false;
connect_wifi();
}
// 处理HTTP请求
#ifdef ENABLE_HTTP_SERVER
if (wifi_connected) {
WiFiClient client = server.available();
if (client) {
handle_client(client);
}
}
#endif
// 主循环逻辑
static unsigned long last_inference_time = 0;
if (millis() - last_inference_time > 10000) { // 每10秒运行一次推理
last_inference_time = millis();
#ifdef ENABLE_INFERENCE
run_inference();
#endif
}
delay(100);
}