ESP32のSPIFFSにパソコンからWebDavアクセスする

概要

これまでESP32とパソコンでデータ転送をする場合にはWebサーバーや、FTPサーバーを利用したデータ転送を利用することが多かったです。

今回はESP32のフラッシュ領域にファイルを保存できるSPIFFSにWebDavでのアクセスできるかを検証してみました。

WebDavとは?

ざっくりいうとFTPみたいに利用できるファイル転送の仕組みです。Webサーバーの機能を利用してファイル転送をする便利な機能です。インターネット経由でファイルをやり取りするのはセキュリティ的に微妙なので思ったより普及は進んでいません。ローカルだとファイル共有使えばいいですからね。

今回はESP32とパソコンを同じWi-Fiネットワークに接続させて、ローカルネットワークとしてESP32にパソコンからファイルを取りにいく構成を実験してみたいと思います。

利用ライブラリと情報

上記ライブラリを利用します。ライブラリマネージャに登録済みなので比較的かんたんに利用できます。

ESPWebDAVの存在は知っていたのですが、リナさんの記事が参考になると思います。上記はSDカードを保存領域に使っていますが、今回はSPIFFSを使っていきたいと思います。

Wi-Fiアクセスポイントの書き込み

#include <WiFi.h>

void setup() {
  Serial.begin(115200);
  delay(500);

  WiFi.begin("SSID", "KEY");
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(500);
  }
  Serial.println();
  Serial.println("WiFi Connected.");
  Serial.printf("IP Address  : ");
  Serial.println(WiFi.localIP());
}

void loop() {
}

ESP32は最後にアクセスしたWi-Fiアクセスポイントの情報を保存しているので、一番最初にWi-Fiにアクセスするだけのプログラムを実行して保存しておきます。これをしておくことで後段で利用するプログラムにはWi-Fiアクセスポイントの情報を設定する必要がなくなります。

ミニマム構成の実験

#include <WiFi.h>
#include <SPIFFS.h>
#include <ESPWebDAV.h>

ESPWebDAV dav;
WiFiServer tcp(80);

void setup() {
  Serial.begin(115200);

  // SPIFFS begin
  if (!SPIFFS.begin()) {
    // SPIFFS is unformatted
    Serial.print("SPIFFS Format ... (please wait)");
    delay(100);
    SPIFFS.format();
    Serial.println("Down");
    ESP.restart();
  }

  // Wi-Fi begin
  WiFi.begin();
  Serial.printf("Wi-Fi begin");
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(500);
  }
  Serial.println();
  Serial.print("IP Address  : ");
  Serial.println(WiFi.localIP());
  Serial.print("WebDav      : file://");
  Serial.print(WiFi.localIP());
  Serial.println("/DavWWWRoot");

  // NTP begin
  configTime(9 * 3600, 0, "pool.ntp.org");
  struct tm timeInfo;
  if (getLocalTime(&timeInfo)) {
    Serial.print("Local Time  : ");
    Serial.printf("%04d-%02d-%02d ", timeInfo.tm_year + 1900, timeInfo.tm_mon + 1, timeInfo.tm_mday);
    Serial.printf("%02d:%02d:%02d\n", timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec);
  }

  // WebDav begin
  tcp.begin();
  dav.begin(&tcp, &SPIFFS);
  dav.setTransferStatusCallback([](const char* name, int percent, bool receive) {
    Serial.printf("%s: '%s': %d%%\n", receive ? "recv" : "send", name, percent);
  });

  // Mac Address Write
  uint8_t mac[6];
  esp_read_mac(mac, ESP_MAC_WIFI_STA);
  Serial.printf("Mac Address : %02X:%02X:%02X:%02X:%02X:%02X\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
  SPIFFS.remove("/mac.txt");
  auto starttime = millis();
  File file = SPIFFS.open("/mac.txt", "w");
  file.printf("Mac Address : %02X:%02X:%02X:%02X:%02X:%02X\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
  file.close();
  Serial.printf("Write time  : %d ms\n", millis() - starttime);
}

void loop() {
  dav.handleClient();

  // 50だと動きが怪しい。軽い処理のみか、重い処理は別タスクで動かす必要がある
  delay(40);
}

ライブラリのスケッチ例を見ながら最低限の構成を作ってみました。下で個別の解説をしています。

SPIFFSの初期化

  // SPIFFS begin
  if (!SPIFFS.begin()) {
    // SPIFFS is unformatted
    Serial.print("SPIFFS Format ... (please wait)");
    delay(100);
    SPIFFS.format();
    Serial.println("Down");
    ESP.restart();
  }

上記でSPIFFSを開始しています。開始時にエラーがでた場合には未フォーマットのため、フォーマット処理を入れています。この処理は結構時間かかりますが、最初の1回だけになります。フォーマットが完了したらリブートして最初から実行しなおしています。

Wi-Fiアクセスポイントに接続

  // Wi-Fi begin
  WiFi.begin();
  Serial.printf("Wi-Fi begin");
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(500);
  }
  Serial.println();
  Serial.print("IP Address  : ");
  Serial.println(WiFi.localIP());
  Serial.print("WebDav      : file://");
  Serial.print(WiFi.localIP());
  Serial.println("/DavWWWRoot");

接続が完了するまで無限ループしています。先程SSIDとKEYは書き込んでいたのでWiFi.begin()と引数をなくし、最後に接続した情報を使うようにしています。また、接続するときようにIPアドレスとWebDavのアドレスも表示しています。このWebDavのアドレスをエクスプローラーなどに入れることで普通のフォルダとしてアクセスできるようになります。

今回はIPアドレスで接続をします。ライブラリのスケッチ例ではmDNSを使って、名前でアクセスできるようになっています。

NTPに接続

  // NTP begin
  configTime(9 * 3600, 0, "pool.ntp.org");
  struct tm timeInfo;
  if (getLocalTime(&timeInfo)) {
    Serial.print("Local Time  : ");
    Serial.printf("%04d-%02d-%02d ", timeInfo.tm_year + 1900, timeInfo.tm_mon + 1, timeInfo.tm_mday);
    Serial.printf("%02d:%02d:%02d\n", timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec);
  }

NTPに接続してESP32の時間合わせをしています。必須ではないですがファイル保存したときの時間が正しくなるように時間合わせはしたほうがよいと思います。

WebDav起動

  // WebDav begin
  tcp.begin();
  dav.begin(&tcp, &SPIFFS);
  dav.setTransferStatusCallback([](const char* name, int percent, bool receive) {
    Serial.printf("%s: '%s': %d%%\n", receive ? "recv" : "send", name, percent);
  });

SPIFFSを使うことにして、WebDavを起動しています。アクセスがあったときにコールバック関数で情報出力をしています。

SPIFFSに書き込み

  // Mac Address Write
  uint8_t mac[6];
  esp_read_mac(mac, ESP_MAC_WIFI_STA);
  Serial.printf("Mac Address : %02X:%02X:%02X:%02X:%02X:%02X\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
  SPIFFS.remove("/mac.txt");
  auto starttime = millis();
  File file = SPIFFS.open("/mac.txt", "w");
  file.printf("Mac Address : %02X:%02X:%02X:%02X:%02X:%02X\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
  file.close();
  Serial.printf("Write time  : %d ms\n", millis() - starttime);

ファイルがないとわかりにくいので、起動したESP32のMACアドレスをmac.txtファイルに保存する処理をいれました。

loop関数

void loop() {
  dav.handleClient();

  // 50だと動きが怪しい。軽い処理のみか、重い処理は別タスクで動かす必要がある
  delay(40);
}

dav.handleClient()を定期的に実行することでWebDavサーバーとして動作するようになります。いろいろ試したのですがdelay()を50以上にするとWindowsからアクセスが不安定になりました。loop()関数でなにか処理をする場合には40ms以内に確実に処理を完了させる必要があります。

ただし、SPIFFSに書き込みしたときに処理時間を確認したところ200ms以上かかっていました。つまりloop()関数内でSPIFFSに書き込むことはできません。

別タスクで書き込み

#include <WiFi.h>
#include <SPIFFS.h>
#include <ESPWebDAV.h>

ESPWebDAV dav;
WiFiServer tcp(80);

// タスク
void task(void *pvParameters) {
  struct tm timeInfoOld = {};
  struct tm timeInfo;
  File file;

  while (1) {
    if (getLocalTime(&timeInfo)) {
      if (timeInfo.tm_min != timeInfoOld.tm_min || timeInfo.tm_hour != timeInfoOld.tm_hour) {
        Serial.print("Local Time  : ");
        Serial.printf("%04d-%02d-%02d ", timeInfo.tm_year + 1900, timeInfo.tm_mon + 1, timeInfo.tm_mday);
        Serial.printf("%02d:%02d:%02d\n", timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec);
        timeInfoOld = timeInfo;
        char filename[24];
        if(file){
          file.close();
        }
        snprintf(filename, 24, "/%04d-%02d-%02d_%02d%02d.txt", timeInfo.tm_year + 1900, timeInfo.tm_mon + 1, timeInfo.tm_mday, timeInfo.tm_hour, timeInfo.tm_min);
        file = SPIFFS.open(filename, "w");
      }
      file.println(millis());
    }

    delay(100);
  }
}

void setup() {
  Serial.begin(115200);

  // SPIFFS begin
  if (!SPIFFS.begin()) {
    // SPIFFS is unformatted
    Serial.print("SPIFFS Format ... (please wait)");
    delay(100);
    SPIFFS.format();
    Serial.println("Down");
    ESP.restart();
  }

  // Wi-Fi begin
  WiFi.begin();
  Serial.printf("Wi-Fi begin");
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(500);
  }
  Serial.println();
  Serial.print("IP Address  : ");
  Serial.println(WiFi.localIP());
  Serial.print("WebDav      : file://");
  Serial.print(WiFi.localIP());
  Serial.println("/DavWWWRoot");

  // NTP begin
  configTime(9 * 3600, 0, "pool.ntp.org");
  struct tm timeInfo;
  if (getLocalTime(&timeInfo)) {
    Serial.print("Local Time  : ");
    Serial.printf("%04d-%02d-%02d ", timeInfo.tm_year + 1900, timeInfo.tm_mon + 1, timeInfo.tm_mday);
    Serial.printf("%02d:%02d:%02d\n", timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec);
  }

  // WebDav begin
  tcp.begin();
  dav.begin(&tcp, &SPIFFS);
  dav.setTransferStatusCallback([](const char* name, int percent, bool receive) {
    Serial.printf("%s: '%s': %d%%\n", receive ? "recv" : "send", name, percent);
  });

  // Mac Address Write
  uint8_t mac[6];
  esp_read_mac(mac, ESP_MAC_WIFI_STA);
  Serial.printf("Mac Address : %02X:%02X:%02X:%02X:%02X:%02X\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
  SPIFFS.remove("/mac.txt");
  auto starttime = millis();
  File file = SPIFFS.open("/mac.txt", "w");
  file.printf("Mac Address : %02X:%02X:%02X:%02X:%02X:%02X\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
  file.close();
  Serial.printf("Write time  : %d ms\n", millis() - starttime);

  // Task begin
  xTaskCreateUniversal(
    task,           // タスク関数
    "task",         // タスク名(あまり意味はない)
    8192,           // スタックサイズ
    NULL,           // 引数
    1,              // 優先度(loopが2で大きい方が高い)
    NULL,           // タスクハンドル
    APP_CPU_NUM     // 実行するCPU(PRO_CPU_NUM or APP_CPU_NUM)
  );
}

void loop() {
  dav.handleClient();
  delay(1);
}

ほぼさきほどと同じですが、タスクを作成して定期的に起動経過時間を保存するようにしてみました。重要なのloop()関数の優先度が2ですのでloop()関数より低い1として実行しています。これでWebDavの処理が優先されて、書き込みはアイドル中に実行されます。

ファイルのローテーション

    if (getLocalTime(&timeInfo)) {
      if (timeInfo.tm_min != timeInfoOld.tm_min || timeInfo.tm_hour != timeInfoOld.tm_hour) {
        timeInfoOld = timeInfo;
        char filename[24];
        if(file){
          file.close();
        }
        snprintf(filename, 24, "/%04d-%02d-%02d_%02d%02d.txt", timeInfo.tm_year + 1900, timeInfo.tm_mon + 1, timeInfo.tm_mday, timeInfo.tm_hour, timeInfo.tm_min);
        file = SPIFFS.open(filename, "w");
      }
      file.println(millis());
    }

    delay(100);

上記は重要な部分だけ抜き出してありますが、分が切り替わった瞬間に新しいファイルを作成しています。常にだらだらと追記しながら、0秒になったところで新しいファイルが開くようになります。

ある程度問題はないのですがWebDavが動いているときにはこのタスクより優先順位が高いので、書き込み処理が遅延します。保存されているファイルの中身を見てみるとたまにdelay(100)で指定している100ms以上の間隔が開いています。

また、delay(10)にしたところぼろぼろの動作になりましたので、この方式だと高精度の測定はできなそうです。

タイマーを利用

#include <WiFi.h>
#include <SPIFFS.h>
#include <ESPWebDAV.h>

ESPWebDAV dav;
WiFiServer tcp(80);
QueueHandle_t xQueueTimer;
QueueHandle_t xQueueRec;
hw_timer_t * timer = NULL;

#define QUEUE_LENGTH 100

struct RecData {
  struct tm timeInfo;
  uint32_t  data;
};

// タイマー割り込み
void IRAM_ATTR onTimer() {
  // キューを送信
  xQueueSendFromISR(xQueueTimer, NULL, 0);
}

void taskTimer(void *pvParameters) {
  RecData recData = {};
  while (1) {
    // タイマー割り込みがあるまで待機する
    xQueueReceive(xQueueTimer, NULL, portMAX_DELAY);

    // 実際の処理
    getLocalTime(&recData.timeInfo);
    recData.data = millis();

    // データ登録
    xQueueSend(xQueueRec, &recData, 0);
  }
}

// タスク
void taskRec(void *pvParameters) {
  struct tm timeInfoOld = {};
  struct tm timeInfo;
  File file;

  while (1) {
    if (getLocalTime(&timeInfo)) {
      if (timeInfo.tm_min != timeInfoOld.tm_min || timeInfo.tm_hour != timeInfoOld.tm_hour) {
        Serial.print("Local Time  : ");
        Serial.printf("%04d-%02d-%02d ", timeInfo.tm_year + 1900, timeInfo.tm_mon + 1, timeInfo.tm_mday);
        Serial.printf("%02d:%02d:%02d\n", timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec);
        timeInfoOld = timeInfo;
        char filename[24];
        if (file) {
          file.flush();
          file.close();
        }
        snprintf(filename, 24, "/%04d-%02d-%02d_%02d%02d.txt", timeInfo.tm_year + 1900, timeInfo.tm_mon + 1, timeInfo.tm_mday, timeInfo.tm_hour, timeInfo.tm_min);
        file = SPIFFS.open(filename, "w");
      }
    }
    RecData recData;
    if (xQueueReceive(xQueueRec, &recData, 0) == pdTRUE) {
      file.println(recData.data);
    }

    delay(1);
  }
}

void setup() {
  Serial.begin(115200);

  // SPIFFS begin
  if (!SPIFFS.begin()) {
    // SPIFFS is unformatted
    Serial.print("SPIFFS Format ... (please wait)");
    delay(100);
    SPIFFS.format();
    Serial.println("Down");
    ESP.restart();
  }

  // Wi-Fi begin
  WiFi.begin();
  Serial.printf("Wi-Fi begin");
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(500);
  }
  Serial.println();
  Serial.print("IP Address  : ");
  Serial.println(WiFi.localIP());
  Serial.print("WebDav      : file://");
  Serial.print(WiFi.localIP());
  Serial.println("/DavWWWRoot");

  // NTP begin
  configTime(9 * 3600, 0, "pool.ntp.org");
  struct tm timeInfo;
  if (getLocalTime(&timeInfo)) {
    Serial.print("Local Time  : ");
    Serial.printf("%04d-%02d-%02d ", timeInfo.tm_year + 1900, timeInfo.tm_mon + 1, timeInfo.tm_mday);
    Serial.printf("%02d:%02d:%02d\n", timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec);
  }

  // WebDav begin
  tcp.begin();
  dav.begin(&tcp, &SPIFFS);
  dav.setTransferStatusCallback([](const char* name, int percent, bool receive) {
    Serial.printf("%s: '%s': %d%%\n", receive ? "recv" : "send", name, percent);
  });

  // Mac Address Write
  uint8_t mac[6];
  esp_read_mac(mac, ESP_MAC_WIFI_STA);
  Serial.printf("Mac Address : %02X:%02X:%02X:%02X:%02X:%02X\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
  SPIFFS.remove("/mac.txt");
  auto starttime = millis();
  File file = SPIFFS.open("/mac.txt", "w");
  file.printf("Mac Address : %02X:%02X:%02X:%02X:%02X:%02X\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
  file.close();
  Serial.printf("Write time  : %d ms\n", millis() - starttime);

  // キュー作成
  xQueueTimer = xQueueCreate(1, 0);
  xQueueRec = xQueueCreate(QUEUE_LENGTH, sizeof(RecData));

  // Task begin
  xTaskCreateUniversal(
    taskRec,        // タスク関数
    "taskRec",      // タスク名(あまり意味はない)
    8192,           // スタックサイズ
    NULL,           // 引数
    1,              // 優先度(loopが2で大きい方が高い)
    NULL,           // タスクハンドル
    APP_CPU_NUM     // 実行するCPU(PRO_CPU_NUM or APP_CPU_NUM)
  );
  xTaskCreateUniversal(
    taskTimer,      // タスク関数
    "taskTimer",    // タスク名(あまり意味はない)
    8192,           // スタックサイズ
    NULL,           // 引数
    4,              // 優先度(loopが2で大きい方が高い)
    NULL,           // タスクハンドル
    APP_CPU_NUM     // 実行するCPU(PRO_CPU_NUM or APP_CPU_NUM)
  );

  // Timer begin
  timer = timerBegin(0, getApbFrequency() / 1000000, true);
  timerAttachInterrupt(timer, &onTimer, true);
  timerAlarmWrite(timer, 100 * 1000, true);
  timerAlarmEnable(timer);
}

void loop() {
  dav.handleClient();
  delay(1);
}

タイマー割り込みを利用して、100ms間隔で呼び出すスケッチです。ただし、割り込み中は実行できる処理が非常に限られるのでキューを利用して、優先度4のtaskTimerを起動する処理になっています。taskTimerでは必要なデータを収集して、xQueueRecにデータを登録するまでを高速で実行します。この処理は40ms以内であればWebDavサーバーへの影響はたぶん無いはずです。

SPIFFSに保存するtaskRecタスクは先ほどと同じ優先度1で一番低い優先度になります。このタスクではxQueueRecキューに溜まったデータをすべて保存する動きへと変更になっています。これでタイマーで時間的に高精度にデータを収集し、アイドル時間にSPIFFSに書き込むような処理になったと思います。

まとめ

100ms間隔ぐらいのデータ保存であれば今回の方法でWebDavサーバーを動かしつつSPIFFSに書き込むことができそうなのがわかりました。データ量が増えるともう少し調整が必要だと思いますが、実際に使いたいのは1秒間隔ぐらいなのでたぶん大丈夫だと思います。

今回はWi-FiをCore0、それ以外をCore1にしていますので、シビアなタイミングで利用した場合には調整したほうがいいかもしれません。測定をしてからWebDavでデータを取得するだけであれば最低限の実装で問題ありません。測定中にWebDavでファイルを取得した場合に、測定結果に影響を与えないようにするのはちょっと気をつける必要がありそうでした。

あと検証してみると書き込み途中のファイルを開いてしまうとWindowsがファイルをキャッシュしてしまい、更新しても開き直してくれません。これは開いてから1分程度するとまた取得しなおすようでして少し時間をおいてから取得するしか方法はないようです。なるべく書き込んでいるファイルにはアクセスしないか、最終的に利用するときには時間をおいてからダウンロードし直すのが好ましいと思います。

また、SPIFFSは1.5MBぐらいしかありませんので、大きなデータは保存できません。とはいえ、ちょっとしたセンシングデータを保存するのには十分な領域かなとは思っています。

コメント