Makato-kanのアイコン画像
Makato-kan 2026年08月11日作成
製作品 製作品 閲覧数 78
Makato-kan 2026年08月11日作成 製作品 製作品 閲覧数 78

写真1枚に環境情報も入れた 家庭菜園タイムラプスシステムの製作(2/3)

写真1枚に環境情報も入れた 家庭菜園タイムラプスシステムの製作(2/3)

カメラ部の概要

カメラ部では、小型で強力なカメラ付きマイコン「XIAO ESP32S3 Sense」を使用します。
これに温湿度センサー(DHT11)と明るさ測定用のCDSセルを組み合わせ、撮影と同時に周囲の環境データを取得します。

回路図・接続方法

キャプションを入力できます

【ピン接続の参考】

  • DHT11: D8番ピンに接続
  • CDSセル: D9番ピン(アナログ入力)に接続

開発環境(Arduino IDE)の設定

最新版のArduino IDEを使用しています。事前に以下のボードマネージャーとライブラリをインストールし、設定を行ってください。

  • ボードマネージャー
    • esp32(最新版を使用し、ボードは "XIAO_ESP32S3" を選択)
  • 追加ライブラリ
    • DHT sensor library (by Adafruit) Ver1.4.7
    • FTPClient_Generic(by Leonardo Bispo...)Ver1.6.0
  • メニュー設定(重要)
    • PSRAM: "OPI PSRAM" に必ず設定してください(大容量データを扱うため必須です)。

プログラムの処理フロー

カメラ部は、起動後に以下のステップ(1〜8)を一定のインターバルで繰り返します。

  1. 初期化とネットワーク接続
    各センサーやカメラ、PSRAMを初期化。M5Stack(親機)のアクセスポイントに接続し、IPアドレス(192.168.4.2)を取得します。
  2. 環境データの測定
    DHT11から「温度」「湿度」を、CDSセルから「明るさ(ADC変換値)」を取得します。
  3. 表示用画像の撮影
    M5Stackの画面表示に適したサイズで撮影し、一時的にPSRAMへ保存します。
  4. 保存用(最大画質)画像の撮影とデータ埋め込み
    最大解像度で撮影後、JPEGの先頭(COMマーカー)にステップ2で測定した環境データをアスキーテキストとして埋め込み、PSRAMに保存します。
  5. 音声データの録音
    内蔵マイクなどから2秒間の音声データを取得し、PSRAMに保存します。
  6. M5StackへのFTP送信
    保存したデータを、M5Stack上のFTPサーバーへ以下のファイル名で送信します。
    • 表示用画像: img.jpg
    • 保存用画像: img2.jpg
    • 音声データ: sound.wav
  7. メモリの解放
    送信が完了したら、次の撮影に向けてPSRAM上のデータ(バッファ)をすべて消去します。
  8. インターバル待機
    指定した時間(テスト時は3分間)待機し、ステップ2へ戻ります。

プログラムコード

ESP32S3Sense-001.ino

/* Seeed Studio XIAO ESP32S3 Sense OV2640 Camera + FTP Upload + Sensors Arduino IDE Compatible Features: - WiFi自動再接続 - FTP (PASVモード) - LittleFS (PSRAM優先バッファ + フラッシュファイルシステム) - SDカード不使用 - 撮影データを一旦PSRAM/LittleFSに保存 → 時間差付きFTP送信 - DHT11 + CDS + I2Sマイク対応 - JPEGにCOMマーカーでセンサ情報埋め込み */ #include <WiFi.h> #include "esp_camera.h" #include <FTPClient_Generic.h> #include "ESP_I2S.h" #include <DHT.h> #include <FS.h> #include <LittleFS.h> // LittleFSを使用 // ====================== ピン・定数定義 ====================== #define DHTPIN D8 #define DHTTYPE DHT11 #define ANALOG_CDS D9 // WiFi設定 const char* ssid = "M5Stack_FTP"; const char* password = "12345678"; // FTP設定 char* ftp_server = "192.168.4.1"; int ftp_port = 21; char* ftp_user = "M5"; char* ftp_pass = "M5pass"; // ========================================================= // FTP Client FTPClient_Generic ftp(ftp_server, ftp_user, ftp_pass, 30000); // ボードID #define BOARD_ID 88 // マーカー用 #define MAX_MARKER_LEN 64 char MARKER_TEXT[MAX_MARKER_LEN]; // ====================== カメラピン定義 ====================== #define PWDN_GPIO_NUM -1 #define RESET_GPIO_NUM -1 #define XCLK_GPIO_NUM 10 #define SIOD_GPIO_NUM 40 #define SIOC_GPIO_NUM 39 #define Y9_GPIO_NUM 48 #define Y8_GPIO_NUM 11 #define Y7_GPIO_NUM 12 #define Y6_GPIO_NUM 14 #define Y5_GPIO_NUM 16 #define Y4_GPIO_NUM 18 #define Y3_GPIO_NUM 17 #define Y2_GPIO_NUM 15 #define VSYNC_GPIO_NUM 38 #define HREF_GPIO_NUM 47 #define PCLK_GPIO_NUM 13 // ====================== グローバル変数 ====================== camera_config_t config; I2SClass i2s; DHT dht(DHTPIN, DHTTYPE); // ファイルパス(LittleFS内) const char* FILE_IMG = "/img.jpg"; const char* FILE_IMG2 = "/img2.jpg"; const char* FILE_SOUND = "/sound.wav"; // ====================== 関数プロトタイプ ====================== bool connectWiFi(); bool initCamera(framesize_t size); bool setResolution(framesize_t size); bool uploadFTP(const char* remotePath, const char* localPath); bool saveToLittleFS(const char* path, uint8_t* data, size_t len); uint8_t* insertCOMMarker(uint8_t* original, size_t orig_len, const char* text, size_t* new_len); // ====================== WiFi接続 ====================== bool connectWiFi() { if (WiFi.status() == WL_CONNECTED) return true; Serial.println("===================================="); Serial.println("Connecting to WiFi: " + String(ssid)); WiFi.disconnect(true); WiFi.mode(WIFI_STA); WiFi.setSleep(false); WiFi.setTxPower(WIFI_POWER_11dBm); WiFi.begin(ssid, password, 6); int retry = 0; while (WiFi.status() != WL_CONNECTED && retry < 60) { delay(500); Serial.print("."); retry++; } if (WiFi.status() == WL_CONNECTED) { Serial.println("\nWiFi connected!"); Serial.print("IP: "); Serial.println(WiFi.localIP()); return true; } else { Serial.println("\nWiFi connection FAILED"); return false; } } // ====================== カメラ初期化 ====================== bool initCamera(framesize_t size) { Serial.println("===================================="); Serial.print("Initializing camera: "); Serial.println(size); esp_camera_deinit(); delay(500); 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; config.pixel_format = PIXFORMAT_JPEG; config.frame_size = size; config.jpeg_quality = (size >= FRAMESIZE_UXGA) ? 15 : 12; config.fb_count = (size >= FRAMESIZE_UXGA) ? 1 : 2; config.fb_location = CAMERA_FB_IN_PSRAM; // PSRAM優先 config.grab_mode = CAMERA_GRAB_LATEST; esp_err_t err = esp_camera_init(&config); if (err != ESP_OK) { Serial.print("Camera init failed: 0x"); Serial.println(err, HEX); return false; } delay(800); Serial.println("Camera initialized successfully."); return true; } bool setResolution(framesize_t size) { sensor_t* s = esp_camera_sensor_get(); if (!s) return false; s->set_framesize(s, size); delay(500); return true; } // ====================== LittleFS保存 ====================== bool saveToLittleFS(const char* path, uint8_t* data, size_t len) { if (!data || len == 0) return false; File file = LittleFS.open(path, "w", true); if (!file) { Serial.println("Failed to open file for writing"); return false; } size_t written = file.write(data, len); file.close(); Serial.printf("Saved %s : %d / %d bytes\n", path, written, len); return written == len; } // ====================== FTPアップロード (ファイルから) ====================== bool uploadFTP(const char* remotePath, const char* localPath) { Serial.println("===================================="); Serial.print("Uploading: "); Serial.print(localPath); Serial.print(" → "); Serial.println(remotePath); File file = LittleFS.open(localPath, "r"); if (!file) { Serial.println("Failed to open local file"); return false; } size_t fileSize = file.size(); if (fileSize == 0) { file.close(); return false; } for (int attempt = 1; attempt <= 3; attempt++) { uint32_t start = millis(); ftp.OpenConnection(); delay(1500); ftp.InitFile(COMMAND_XFER_TYPE_BINARY); delay(800); ftp.NewFile(remotePath); delay(800); uint8_t buffer[1024]; size_t totalSent = 0; while (file.available() && totalSent < fileSize) { size_t toRead = min((size_t)1024, fileSize - totalSent); size_t read = file.read(buffer, toRead); if (read == 0) break; ftp.WriteData(buffer, read); totalSent += read; } ftp.CloseFile(); delay(800); ftp.CloseConnection(); file.close(); Serial.print("Transfer Time: "); Serial.print(millis() - start); Serial.println(" ms"); Serial.println("FTP Upload SUCCESS"); return true; } Serial.println("FTP Upload FAILED"); file.close(); return false; } // ====================== JPEGにセンサ情報を埋め込む ====================== uint8_t* insertCOMMarker(uint8_t* original, size_t orig_len, const char* text, size_t* new_len) { if (!original || orig_len < 4) return nullptr; if (original[0] != 0xFF || original[1] != 0xD8) return nullptr; size_t text_len = strlen(text); size_t marker_len = 2 + text_len; *new_len = orig_len + marker_len + 2; uint8_t* new_buf = (uint8_t*)ps_malloc(*new_len); // PSRAM使用 if (!new_buf) return nullptr; size_t pos = 0; new_buf[pos++] = 0xFF; new_buf[pos++] = 0xD8; // SOI new_buf[pos++] = 0xFF; new_buf[pos++] = 0xFE; // COM uint16_t length = text_len + 2; new_buf[pos++] = (length >> 8) & 0xFF; new_buf[pos++] = length & 0xFF; memcpy(&new_buf[pos], text, text_len); pos += text_len; memcpy(&new_buf[pos], original + 2, orig_len - 2); Serial.printf("COM Marker inserted: %d bytes\n", marker_len); return new_buf; } // ====================== 撮影&LittleFS保存 ====================== bool captureAndSave(framesize_t size, const char* localPath, bool withMarker = true) { Serial.println("===================================="); if (!initCamera(size)) return false; if (WiFi.status() != WL_CONNECTED) connectWiFi(); delay(1000); camera_fb_t* fb = esp_camera_fb_get(); if (!fb) { Serial.println("Camera capture failed"); return false; } bool result = false; if (withMarker) { size_t new_len = 0; uint8_t* marked_buf = insertCOMMarker(fb->buf, fb->len, MARKER_TEXT, &new_len); if (marked_buf) { result = saveToLittleFS(localPath, marked_buf, new_len); free(marked_buf); } else { result = saveToLittleFS(localPath, fb->buf, fb->len); } } else { result = saveToLittleFS(localPath, fb->buf, fb->len); } esp_camera_fb_return(fb); delay(500); return result; } // ====================== マイク録音&保存 ====================== bool saveMICToLittleFS(const char* localPath) { Serial.println("Initializing I2S Microphone..."); i2s.setPinsPdmRx(42, 41); if (!i2s.begin(I2S_MODE_PDM_RX, 16000, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO)) { Serial.println("I2S initialization failed!"); return false; } size_t wav_size = 0; uint8_t* wav_buffer = i2s.recordWAV(2, &wav_size); // 2秒録音(必要に応じて調整) bool result = saveToLittleFS(localPath, wav_buffer, wav_size); free(wav_buffer); delay(500); return result; } // ====================== Setup ====================== void setup() { Serial.begin(9600); delay(2000); dht.begin(); Serial.println(F("DHT11 initialized")); // LittleFS初期化(PSRAMが使える環境では大きな領域確保可能) if (!LittleFS.begin(true)) { // true = format if failed Serial.println("LittleFS Mount Failed"); while (1) delay(1000); } Serial.println("LittleFS mounted successfully."); Serial.println("===================================="); Serial.println("XIAO ESP32S3 Sense - Delayed FTP Upload"); Serial.println("===================================="); Serial.print("Free Heap: "); Serial.println(ESP.getFreeHeap()); Serial.print("Free PSRAM: "); Serial.println(ESP.getFreePsram()); connectWiFi(); Serial.println("Setup completed."); } // ====================== Loop ====================== void loop() { // ---------- センサ読み取り ---------- float temp = dht.readTemperature(); float humi = dht.readHumidity(); if (isnan(temp)) temp = -99.9; if (isnan(humi)) humi = -99.9; int cdsValue = analogRead(ANALOG_CDS); snprintf(MARKER_TEXT, MAX_MARKER_LEN, "%d,%.1f,%.1f,0,0,0,0,0,%d", BOARD_ID, temp, humi, cdsValue); Serial.printf("→ Marker: %s\n", MARKER_TEXT); // ---------- データ作成(すべてPSRAM/LittleFSに保存) ---------- Serial.println("\n######## CAPTURING ALL DATA ########"); // 音声 saveMICToLittleFS(FILE_SOUND); // 小さい画像 (img2.jpg) captureAndSave(FRAMESIZE_QVGA, FILE_IMG2, false); // 高解像度画像 (img.jpg) - マーカー付き captureAndSave(FRAMESIZE_UXGA, FILE_IMG, true); Serial.println("All data saved to LittleFS."); // ---------- 送信スケジュール ---------- delay(10000); // 10秒後 Serial.println("\n######## SENDING img.jpg ########"); uploadFTP("/img.jpg", FILE_IMG); delay(15000); // 送信終了から15秒後 Serial.println("\n######## SENDING sound.wav ########"); uploadFTP("/sound.wav", FILE_SOUND); delay(15000); // 送信終了から15秒後 Serial.println("\n######## SENDING img2.jpg ########"); uploadFTP("/img2.jpg", FILE_IMG2); Serial.println("\nCycle complete. Waiting ~3 minutes..."); for (int i = 0; i < 170; i++) { // 合計約3分 delay(1000); if (WiFi.status() != WL_CONNECTED) connectWiFi(); } }

※テスト運用のため、現在は3分毎に撮影・送信を行う設定にしています。


製作・運用の注意点(Tips)

  1. カメラの「緑被り」対策
    ESP32系のカメラは、起動直後や1枚目の撮影時に画像全体が緑色っぽく変色する(緑被り)現象が起きやすいです。
    本システムでは、ホワイトバランス等を安定させるため、フレームバッファを一度破棄(空撮り)してから、実際に使用する本番の画像を撮影する仕様にしています。

  2. 撮影インターバルの調整
    最初は動作確認のために「3分毎」などの短いインターバルでテストすることをおすすめします。無事にM5Stack側へデータが届くことを確認できたら、家庭菜園の成長スピードに合わせて(1時間毎など)インターバルを長くしていくと安心です。

Makato-kanのアイコン画像
屋外で温度湿度を測定し記録するので、壊れやすいと思い大量にDHT11とESP-WROOM-02を購入したのですが思いのほか壊れなかったので、何にでもDHT11をくっつけています。
  • Makato-kan さんが 前の火曜日の10:29 に 編集 をしました。 (メッセージ: 初版)
ログインしてコメントを投稿する