🔥
【XIAO ESP32S3】ファイルサーバー化でmicroSDカードのファイルをダウンロードする
目的
こちらの記事で温湿度センサーDHT20の値をmicroSDカードに保存しましたが、
データを確認するにはmicroSDカードを取り出してPCに挿入する必要があり、確認中は温湿度センサーの値を記録できませんでした。
そこで、温湿度センサーでデータを保存しつつ、ESP32S3をファイルサーバー化してWebブラウザからmicroSDカードのファイルをダウンロードできるようにしたいと思います。
また、データを保存する時間についても、起動してからの経過時間ではなく、リアルタイムで記録できるようにしたいと思います。
ピン・配線図
使用するピンは変更ありません。
上記の記事から、付属のアンテナを接続しただけになります。

ブレッドボードでの配線

ライブラリをインストール
追加ライブラリはありません。
スケッチ
スケッチ
#include <WiFi.h>
#include <WebServer.h>
#include <SD.h>
#include <SPI.h>
#include <Wire.h>
#include <DHT20.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include "time.h"
// --- 設定 (環境に合わせて書き換えてください) ---
const char* ssid = "your_ssid";
const char* password = "your_password";
// NTPサーバー設定
const char* ntpServer = "pool.ntp.org";
const long gmtOffset_sec = 9 * 3600; // JST (UTC+9)
const int daylightOffset_sec = 0;
// Webサーバー設定 (ポート80)
WebServer server(80);
// OLED設定
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// DHT20設定
DHT20 dht20;
// SDカード設定
// Seeed Studio XIAO ESP32S3 SenseのデフォルトSPIピン(CS=21)
#define SD_CS_PIN 21
#define LOG_FILENAME "/data_log.csv"
// タイマー管理
const unsigned long displayInterval = 1000; // 1秒
unsigned long lastLogTime = 0;
const unsigned long logInterval = 60000; // 1分 (60秒)
// --- グローバル変数 ---
float temperature = 0.0;
float humidity = 0.0;
bool wifiConnected = false;
// FreeRTOS ハンドル
SemaphoreHandle_t sdMutex;
TaskHandle_t loggerTaskHandle;
// --- 関数プロトタイプ宣言 ---
void handleRoot();
void handleFileDownload();
void printDirectory(File dir, String& html);
String getContentType(String filename);
void updateDisplay();
void logDataToSD();
void loggerTask(void * pvParameters);
void setup() {
Serial.begin(115200);
// while (!Serial); // デバッグ時は有効化
// Mutex作成
sdMutex = xSemaphoreCreateMutex();
// Wire初期化 (I2C)
// I2CはloggerTask(Core 0)で使用するため、setup(Core 1)で初期化を行うが、
// 排他制御が必要な場合がある。今回はloggerTaskのみがI2Cを使用するため、
// setup完了後にloggerTaskを開始すれば競合はしない。
Wire.begin();
// --- OLED初期化 ---
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306 allocation failed"));
for (;;);
}
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println(F("Initializing..."));
display.display();
// --- DHT20初期化 ---
dht20.begin();
delay(1000); // センサー安定化待ち
// --- SDカード初期化 ---
Serial.print("Initializing SD card...");
// SDカードはloggerTaskとserver(Core 1)の両方で使用するためMutexで保護するが、
// setup中はまだマルチタスクではないためそのままアクセス可能
if (!SD.begin(SD_CS_PIN)) {
Serial.println("Card Mount Failed");
display.println(F("SD Init Failed!"));
display.display();
return;
}
Serial.println("SD Card initialized.");
// ログファイルのヘッダー書き込み (ファイルが存在しない場合のみ)
if (!SD.exists(LOG_FILENAME)) {
File file = SD.open(LOG_FILENAME, FILE_WRITE);
if (file) {
file.println("Timestamp,Temperature (C),Humidity (%RH)");
file.close();
}
}
// --- WiFi接続 ---
Serial.print("Connecting to ");
Serial.println(ssid);
display.println(F("Connecting WiFi..."));
display.display();
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected.");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
wifiConnected = true;
// OLEDにIP表示
display.clearDisplay();
display.setCursor(0, 0);
display.println(F("WiFi Connected!"));
display.print(F("IP: "));
display.println(WiFi.localIP());
display.display();
// 時刻同期
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
// --- Webサーバー設定 ---
// ルートパスへのアクセス時
server.on("/", handleRoot);
// その他のパスへのアクセス時 (ファイルダウンロード用)
server.onNotFound(handleFileDownload);
server.begin();
Serial.println("HTTP server started");
delay(2000); // IPアドレスなどの表示を確認するため少し待機
display.clearDisplay();
// --- Logger Task 作成 (Core 0) ---
xTaskCreatePinnedToCore(
loggerTask, // タスク関数
"LoggerTask", // タスク名
8192, // スタックサイズ (必要に応じて調整)
NULL, // パラメータ
1, // 優先度
&loggerTaskHandle, // ハンドル
0 // Core 0
);
Serial.println("Logger Task started on Core 0");
}
void loop() {
// Core 1 (デフォルト)
// Webサーバーのリクエスト処理のみを担当
server.handleClient();
delay(1); // Watchdogタイマーリセットのため少し待つ
}
// --- Logger Task (Core 0) ---
void loggerTask(void * pvParameters) {
while (true) {
unsigned long currentMillis = millis();
// --- 1秒ごとにセンサー読み取り & ディスプレイ更新 ---
if (currentMillis - dht20.lastRead() >= displayInterval) {
updateDisplay();
}
// --- 1分ごとにSDカードへログ保存 ---
if (currentMillis - lastLogTime >= logInterval) {
lastLogTime = currentMillis;
logDataToSD();
}
vTaskDelay(10 / portTICK_PERIOD_MS); // 少し待機して他の処理に譲る
}
}
// --- Webサーバー リクエストハンドラ ---
// ルートパス(/)へのアクセス: ファイル一覧を表示
// SDカードへアクセスするためMutexを使用
void handleRoot() {
if (xSemaphoreTake(sdMutex, portMAX_DELAY) == pdTRUE) {
File root = SD.open("/");
if (!root) {
server.send(500, "text/plain", "Failed to open directory");
xSemaphoreGive(sdMutex);
return;
}
if (!root.isDirectory()) {
server.send(500, "text/plain", "Not a directory");
xSemaphoreGive(sdMutex);
return;
}
String html = "<!DOCTYPE html><html><head><meta charset='UTF-8'><title>SD Card Files</title></head><body>";
html += "<h1>SD Card Files</h1>";
html += "<p><a href='/'>Refresh</a></p>";
html += "<ul>";
printDirectory(root, html);
html += "</ul>";
html += "</body></html>";
server.send(200, "text/html", html);
root.close();
xSemaphoreGive(sdMutex);
} else {
server.send(500, "text/plain", "SD Card Busy");
}
}
// ファイル一覧をHTMLリストに追加 (再帰呼び出し対応だが今回はフラット)
void printDirectory(File dir, String& html) {
while (true) {
File entry = dir.openNextFile();
if (!entry) {
// no more files
break;
}
if (!entry.isDirectory()) {
String fileName = entry.name();
if (fileName.startsWith(".")) {
entry.close();
continue;
}
html += "<li><a href='/";
html += fileName;
html += "'>";
html += fileName;
html += "</a> (";
html += entry.size();
html += " bytes)</li>";
}
entry.close();
}
}
// ファイルダウンロード処理
// SDカードへアクセスするためMutexを使用
void handleFileDownload() {
String path = server.uri();
if (path.indexOf("/..") != -1) {
server.send(403, "text/plain", "Forbidden");
return;
}
if (xSemaphoreTake(sdMutex, portMAX_DELAY) == pdTRUE) {
if (SD.exists(path)) {
File file = SD.open(path, FILE_READ);
String contentType = getContentType(path);
server.streamFile(file, contentType);
file.close();
} else {
server.send(404, "text/plain", "File Not Found");
}
xSemaphoreGive(sdMutex);
} else {
server.send(500, "text/plain", "SD Card Busy");
}
}
String getContentType(String filename) {
if (server.hasArg("download")) return "application/octet-stream";
else if (filename.endsWith(".htm")) return "text/html";
else if (filename.endsWith(".html")) return "text/html";
else if (filename.endsWith(".css")) return "text/css";
else if (filename.endsWith(".js")) return "application/javascript";
else if (filename.endsWith(".png")) return "image/png";
else if (filename.endsWith(".gif")) return "image/gif";
else if (filename.endsWith(".jpg")) return "image/jpeg";
else if (filename.endsWith(".ico")) return "image/x-icon";
else if (filename.endsWith(".xml")) return "text/xml";
else if (filename.endsWith(".pdf")) return "application/x-pdf";
else if (filename.endsWith(".zip")) return "application/x-zip";
else if (filename.endsWith(".csv")) return "text/csv";
return "text/plain";
}
// --- ロガー関連関数 ---
// センサー読み取りとOLED表示 (Core 0で実行)
void updateDisplay() {
int status = dht20.read();
// I2C (Wire) は thread-safe ではない場合があるが、
// 今回は Core 0 のみで Wire を使用するため競合はしない。
// もし Core 1 でも Wire を使う場合は Mutex が必要。
display.clearDisplay();
display.setCursor(0, 0);
if (status == DHT20_OK) {
temperature = dht20.getTemperature();
humidity = dht20.getHumidity();
display.setTextSize(1);
display.println(F("DHT20 Sensor"));
display.println(F("----------------"));
display.setTextSize(2);
display.print(temperature, 1);
display.println(F(" C"));
display.print(humidity, 1);
display.println(F(" %"));
} else {
display.setTextSize(1);
display.println(F("Sensor Error"));
display.print(F("Status: "));
display.println(status);
}
// 現在時刻の表示
if (wifiConnected) {
struct tm timeinfo;
if(getLocalTime(&timeinfo)){
char timeStr[9];
strftime(timeStr, sizeof(timeStr), "%H:%M:%S", &timeinfo);
display.setTextSize(1);
display.println(timeStr);
}
}
// ipアドレスの表示
display.print(F("IP: "));
display.println(WiFi.localIP());
display.display();
}
// データログ保存 (Core 0で実行)
// SDカードへアクセスするためMutexを使用
void logDataToSD() {
if (!wifiConnected) return;
struct tm timeinfo;
if(!getLocalTime(&timeinfo)){
Serial.println(F("Failed to obtain time"));
return;
}
char timeStr[25];
strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
String dataString = String(timeStr) + "," + String(temperature, 2) + "," + String(humidity, 2);
// Mutex取得
if (xSemaphoreTake(sdMutex, portMAX_DELAY) == pdTRUE) {
File file = SD.open(LOG_FILENAME, FILE_APPEND);
if (file) {
file.println(dataString);
file.close();
Serial.println("Logged (Core 0): " + dataString);
} else {
Serial.println(F("Failed to open file for append"));
}
xSemaphoreGive(sdMutex);
} else {
Serial.println("Failed to take mutex for SD logging");
}
}
動作確認
ブレッドボード
ファイルサーバーが正常に起動しました。(IPアドレス:192.168.3.16)

Webブラウザ
ESP32S3のIPアドレスにアクセスします。

microSDカードに保存されているファイル一覧が表示されます。
data_log.csvをクリックします。

ファイルがダウンロードされます。

ダウンロードしたファイルを確認すると、記録された時間、温度、湿度が記録されています。

Discussion