iT邦幫忙

2026 iThome 鐵人賽

DAY 3
0
ChatGPT & Codex

窮鬼冰箱升級計劃系列 第 3

【Day 3】ESP32 上的連網功能

  • 分享至 

  • xImage
  •  

前言

前一回將 Prompt 出來的程式編譯後,寫進板子後,發生了一些問題⋯以下先列出今天預計處理的部分。

  1. 問題1 - SD卡槽的腳位確認
  2. 問題2 - 關於記憶卡的格式
  3. 開發1 - 使 ESP32 連上網路 / 從 ESP32 下載相片

修正問題

問題1

由於 AI 可能會有幻覺,所以檢查 code 這部分,也不能完全交給 AI。 肉眼看一下大致流程吧!

  • 當程式燒錄完成後,他就會自己運行起來。 打開 IDE 的 Tools > Serial Monitor (如下圖),查看那些被 printf / println 印出的訊息。
    https://ithelp.ithome.com.tw/upload/images/20260918/20130149ct4Y1Qh1Kd.png
  • 錯誤訊息
    sdmmc_init_ocr: send_op_cond returned 0x107
    sdmmc_card_init failed
    

https://ithelp.ithome.com.tw/upload/images/20260917/20130149YB05Vshcr6.jpg

  • 從官方文件看出,跟原始碼沒有對上;修正腳位定義
    • GPIO38 - SD-CMD
    • GPIO39 - SD-CLK
    • GPIO40 - SD-DATA

問題2

  • 新的錯誤訊息
    mount_to_vfs failed (0xffffffff)
  • 將其連接到電腦後,重新進行格式化
    • FAT32 + Master Boot Record

開發

Prompt

#include "esp_camera.h"
#include "FS.h"
#include "SD_MMC.h"
#include <WiFi.h>
#include <WebServer.h>


WebServer server(80);

// ======================================================
// GOOUUU ESP32-S3-CAM V1.5
// OV3660 + SR602 PIR + microSD
// ======================================================

// ---------- SR602 ----------
#define PIR_PIN 2

// 必須連續偵測 3 秒
const unsigned long MOTION_TRIGGER_MS = 3000;

// ---------- Wi-Fi ----------
const char* WIFI_SSID = "<修改成 Wifi 名稱>";
const char* WIFI_PASSWORD = "<修改成 Wifi 密碼>";
unsigned long lastWiFiCheck = 0;
const unsigned long WIFI_TIMEOUT = 15000;
const unsigned long WIFI_CHECK_INTERVAL = 10000;

// ---------- Camera ----------
#define PWDN_GPIO_NUM     -1
#define RESET_GPIO_NUM    -1

#define XCLK_GPIO_NUM     15
#define SIOD_GPIO_NUM      4
#define SIOC_GPIO_NUM      5

#define Y9_GPIO_NUM       16
#define Y8_GPIO_NUM       17
#define Y7_GPIO_NUM       18
#define Y6_GPIO_NUM       12
#define Y5_GPIO_NUM       10
#define Y4_GPIO_NUM        8
#define Y3_GPIO_NUM        9
#define Y2_GPIO_NUM       11

#define VSYNC_GPIO_NUM     6
#define HREF_GPIO_NUM      7
#define PCLK_GPIO_NUM     13

// ---------- microSD ----------
#define SD_CMD 38
#define SD_CLK 39
#define SD_D0  40


unsigned long motionStart = 0;

bool timingMotion = false;
bool photoTaken = false;

uint32_t photoNumber = 0;


// ======================================================
// Camera Initialization
// ======================================================

bool initCamera() {

  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_sccb_sda = SIOD_GPIO_NUM;
  config.pin_sccb_scl = SIOC_GPIO_NUM;

  config.pin_pwdn  = PWDN_GPIO_NUM;
  config.pin_reset = RESET_GPIO_NUM;

  config.xclk_freq_hz = 20000000;

  // 直接讓 Camera 輸出 JPEG
  config.pixel_format = PIXFORMAT_JPEG;

  // 第一階段先 VGA,確認整套流程穩定
  config.frame_size = FRAMESIZE_VGA;

  config.jpeg_quality = 10;

  if (psramFound()) {

    Serial.println("PSRAM detected");

    config.fb_location = CAMERA_FB_IN_PSRAM;
    config.fb_count = 2;

    config.grab_mode = CAMERA_GRAB_LATEST;

  } else {

    Serial.println("WARNING: PSRAM not detected");

    config.fb_location = CAMERA_FB_IN_DRAM;
    config.fb_count = 1;
  }


  esp_err_t err = esp_camera_init(&config);

  if (err != ESP_OK) {

    Serial.printf(
      "Camera init failed: 0x%x\n",
      err
    );

    return false;
  }


  sensor_t *sensor = esp_camera_sensor_get();

  if (sensor != nullptr) {

    Serial.printf(
      "Camera sensor PID: 0x%04X\n",
      sensor->id.PID
    );
  }


  Serial.println("Camera OK");

  return true;
}


// ======================================================
// SD Initialization
// ======================================================

bool initSD() {

  // GOOUUU V1.5 SD pins
  SD_MMC.setPins(
    SD_CLK,
    SD_CMD,
    SD_D0
  );


  // true = 1-bit SDMMC mode
  if (!SD_MMC.begin(
        "/sdcard",
        true
      )) {

    Serial.println(
      "SD initialization failed"
    );

    return false;
  }


  if (SD_MMC.cardType() == CARD_NONE) {

    Serial.println(
      "No SD card detected"
    );

    return false;
  }


  uint64_t sizeMB =
    SD_MMC.cardSize() / (1024 * 1024);


  Serial.printf(
    "SD Card: %llu MB\n",
    sizeMB
  );


  if (!SD_MMC.exists("/photos")) {

    SD_MMC.mkdir("/photos");
  }


  Serial.println("SD OK");

  return true;
}


// ======================================================
// Wi-Fi Initialization
// ======================================================

bool initWiFi() {

  Serial.println();
  Serial.println("[3] Wi-Fi");

  // ESP32 作為 Wi-Fi Client
  WiFi.mode(WIFI_STA);

  // 避免使用之前儲存的連線狀態
  WiFi.disconnect();
  delay(100);

  Serial.printf(
    "Connecting to: %s\n",
    WIFI_SSID
  );

  WiFi.begin(
    WIFI_SSID,
    WIFI_PASSWORD
  );

  unsigned long startTime = millis();

  // 最多等待 15 秒
  while (
    WiFi.status() != WL_CONNECTED &&
    millis() - startTime < 15000
  ) {

    Serial.print(".");
    delay(500);
  }

  Serial.println();

  if (WiFi.status() != WL_CONNECTED) {

    Serial.println(
      "Wi-Fi connection FAILED"
    );

    return false;
  }

  Serial.println(
    "Wi-Fi connected!"
  );

  Serial.print(
    "IP address : "
  );

  Serial.println(
    WiFi.localIP()
  );

  Serial.print(
    "Gateway    : "
  );

  Serial.println(
    WiFi.gatewayIP()
  );

  Serial.print(
    "Subnet Mask: "
  );

  Serial.println(
    WiFi.subnetMask()
  );

  Serial.print(
    "RSSI       : "
  );

  Serial.print(
    WiFi.RSSI()
  );

  Serial.println(
    " dBm"
  );

  return true;
}


// ======================================================
// Wi-Fi Connection Check
// ======================================================

void checkWiFi() {
  if (millis() - lastWiFiCheck < WIFI_CHECK_INTERVAL) {
    return;
  }
  lastWiFiCheck = millis();
  if (WiFi.status() == WL_CONNECTED) {
    return;
  }
  Serial.println(
    "Wi-Fi disconnected. Reconnecting..."
  );
  WiFi.disconnect();
  WiFi.begin(
    WIFI_SSID,
    WIFI_PASSWORD
  );
}


// ======================================================
// Find next photo number
// 避免 ESP32 重開機後從 000001 開始覆寫
// ======================================================

void findNextPhotoNumber() {

  File dir = SD_MMC.open("/photos");

  if (!dir) {
    return;
  }


  uint32_t maxNumber = 0;


  while (true) {

    File file = dir.openNextFile();

    if (!file) {
      break;
    }


    String name = file.name();


    int start =
      name.lastIndexOf("photo_");


    if (start >= 0) {

      String number =
        name.substring(
          start + 6,
          start + 12
        );


      uint32_t n =
        number.toInt();


      if (n > maxNumber) {
        maxNumber = n;
      }
    }


    file.close();
  }


  dir.close();

  photoNumber = maxNumber;


  Serial.printf(
    "Last photo: %06lu\n",
    (unsigned long)photoNumber
  );
}


// ======================================================
// Capture
// ======================================================

bool takePhoto() {

  Serial.println();
  Serial.println("------------------------");
  Serial.println("Taking photo...");


  camera_fb_t *fb =
    esp_camera_fb_get();


  if (!fb) {

    Serial.println(
      "Capture failed"
    );

    return false;
  }


  photoNumber++;


  char path[40];

  snprintf(
    path,
    sizeof(path),
    "/photos/photo_%06lu.jpg",
    (unsigned long)photoNumber
  );


  Serial.printf(
    "Resolution: %d x %d\n",
    fb->width,
    fb->height
  );


  Serial.printf(
    "JPEG: %u bytes\n",
    fb->len
  );


  File file =
    SD_MMC.open(
      path,
      FILE_WRITE
    );


  if (!file) {

    Serial.println(
      "Cannot create photo"
    );

    esp_camera_fb_return(fb);

    return false;
  }


  size_t written =
    file.write(
      fb->buf,
      fb->len
    );


  file.close();

  esp_camera_fb_return(fb);


  if (written == 0) {

    Serial.println(
      "SD write failed"
    );

    return false;
  }


  Serial.printf(
    "Saved: %s\n",
    path
  );

  Serial.printf(
    "Written: %u bytes\n",
    written
  );

  Serial.println("------------------------");

  return true;
}


// ======================================================
// Setup
// ======================================================

void setup() {

  Serial.begin(115200);

  delay(2000);


  Serial.println();
  Serial.println("==============================");
  Serial.println(" Smart Fridge Camera");
  Serial.println(" GOOUUU ESP32-S3-CAM V1.5");
  Serial.println(" OV3660 + SR602");
  Serial.println("==============================");


  pinMode(
    PIR_PIN,
    INPUT
  );


  Serial.println();
  Serial.println("[1] Camera");

  if (!initCamera()) {

    Serial.println(
      "FATAL: Camera failed"
    );

    while (true) {
      delay(1000);
    }
  }


  Serial.println();
  Serial.println("[2] SD Card");

  if (!initSD()) {

    Serial.println(
      "FATAL: SD failed"
    );

    while (true) {
      delay(1000);
    }
  }


  findNextPhotoNumber();


  Serial.println();
  Serial.println("[3] Wi-Fi");
  bool wifiOK = initWiFi();
  if (!wifiOK) {
    Serial.println(
      "WARNING: Wi-Fi unavailable"
    );
    Serial.println(
      "Continue in offline mode..."
    );
  }

  Serial.println();
  Serial.println("==============================");
  Serial.println("SYSTEM READY");

  if (WiFi.status() == WL_CONNECTED) {
    Serial.print("Wi-Fi IP: ");
    Serial.println(
      WiFi.localIP()
    );
    initWebServer();
  } else {
    Serial.println(
      "Wi-Fi: OFFLINE"
    );
  }

  Serial.println(
    "Waiting for motion..."
  );

  Serial.println(
    "=============================="
  );

  Serial.println("Waiting for motion...");
  Serial.println("==============================");
}


// ======================================================
// handlePhoto
// ======================================================

void handlePhoto() {

  // 使用方式:
  // http://172.16.71.51/photo?name=photo_000001.jpg

  if (!server.hasArg("name")) {
    server.send(
      400,
      "text/plain",
      "Missing parameter: name"
    );
    return;
  }

  String filename = server.arg("name");

  // 基本防止 path traversal
  if (
    filename.indexOf("..") >= 0 ||
    filename.indexOf("/") >= 0
  ) {
    server.send(
      400,
      "text/plain",
      "Invalid filename"
    );
    return;
  }

  String path =
    "/photos/" + filename;

  Serial.printf(
    "HTTP request: %s\n",
    path.c_str()
  );

  if (!SD_MMC.exists(path)) {

    server.send(
      404,
      "text/plain",
      "Photo not found"
    );

    return;
  }

  File file =
    SD_MMC.open(
      path,
      FILE_READ
    );

  if (!file) {

    server.send(
      500,
      "text/plain",
      "Cannot open photo"
    );

    return;
  }

  server.streamFile(
    file,
    "image/jpeg"
  );

  file.close();
}


// ======================================================
// initWebServer
// ======================================================

void initWebServer() {

  server.on(
    "/",
    HTTP_GET,
    []() {

      server.send(
        200,
        "text/plain",
        "Smart Fridge Camera Online"
      );
    }
  );


  server.on(
    "/photo",
    HTTP_GET,
    handlePhoto
  );


  server.begin();

  Serial.println(
    "HTTP server started"
  );

  Serial.print(
    "URL: http://"
  );

  Serial.println(
    WiFi.localIP()
  );
}


// ======================================================
// Loop
// ======================================================

void loop() {

  checkWiFi();
  
  server.handleClient();
  
  bool motion =
    digitalRead(PIR_PIN);


  // ==============================================
  // SR602 HIGH
  // ==============================================

  if (motion == HIGH) {

    // 第一次 HIGH
    if (!timingMotion) {

      timingMotion = true;

      motionStart = millis();

      Serial.println(
        "Motion detected..."
      );
    }


    // HIGH 必須連續 3 秒
    if (
      !photoTaken &&
      millis() - motionStart >= MOTION_TRIGGER_MS
    ) {

      Serial.println(
        "Motion sustained for 3 seconds"
      );


      if (takePhoto()) {

        photoTaken = true;

      } else {

        Serial.println(
          "Photo failed"
        );
      }
    }

  }

  // ==============================================
  // SR602 LOW
  // ==============================================

  else {

    // HIGH 不足三秒
    if (
      timingMotion &&
      !photoTaken
    ) {

      Serial.println(
        "Motion ended before 3 seconds"
      );
    }


    // 已經拍照,等人離開才重新 Arm
    if (
      timingMotion &&
      photoTaken
    ) {

      Serial.println(
        "Motion ended - system rearmed"
      );
    }


    timingMotion = false;
    photoTaken = false;

    motionStart = 0;
  }


  delay(20);
}

小結

  • 在板子上殿後,會自己嘗試連上網路,並且加入了斷線重新連線的機制。
  • 後續仍需要抽出 Wifi SSID 和密碼,不要放在同一份 ino。

上一篇
【Day 2】初探物聯網晶片 ESP32 及開發環境設定
系列文
窮鬼冰箱升級計劃3
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言