2022年2月12日土曜日

ESP32でFTPサーバを立ててみた。

 ESP32-CAMを主にストリーミング用途として使っていたんだけども、microSDカードスロットを使ってみることに。

ESP32-CAMのmicroSDスロットはHS2に接続されているっぽいのでSDMMCライブラリを使用して取り扱うことができるっぽい。HS2に接続しているってことは比較的高速でアクセスできる?

SDカードに写真を保存する方法は色々紹介されているけど、せっかくWiFiにつながるモジュールなのでWiFiからSDカードにアクセスしたいということで色々調べてみた。

一番簡単そうなのはHTTPで一覧を作成してダウンロードする方法。アップロードも一応できるっぽいし。ただこれだと一個ずつダウンロードするのが面倒そう…

次にWebDAVもESP32用のライブラリが出ていた。Windows上でネットワークの場所の追加をしないといけないのが面倒だけど、一応エクスプローラから使用できるのでファイル操作が楽そう。

あとはFTPサーバのライブラリを作ってる人もいた。これならエクスプローラからそのまま行けそうなので便利かも。ということで試してみることに。

今回使用させてもらったのはESPFtpServerライブラリ。
どうやらESP32上で動かせるFTPサーバは色んな方々が開発を進めていてくれてるようで、それをまとめてくれたもののよう。こちらであればSD_MMCライブラリでも内蔵フラッシュメモリ上のSPIFFSでもFTPサーバでファイル転送ができるらしい。

早速サンプルを使用してみることに。

ライブラリのインストールは手動にしか対応していなのでESPFtpServerからCodeのDownload ZIPでダウンロードしてくる。解凍したら中にはいっているフォルダをArduinoのlibrariesにぶち込む。
このライブラリのサンプルスケッチのフォルダ階層がおかしくてちゃんと認識されないので、"ESPFtpServer-master\examples"の中に"ESPFtpServer"っていうフォルダを作って"ESPFtpServer.ino"を作成したフォルダにぶち込む。

これでサンプルスケッチが認識されるようになった。(Arduinoはinoファイルが同じファイル名のフォルダの中に入っていないとダメな仕様)

スケッチ例→ESPFtpServer→ESPFtpServerでサンプルスケッチを開く。
上の方でファイルシステムを設定するのだけども、今回はESP32-CAM上のmicroSDスロットを使用して試してみたので、define FS_LITTLEFSをコメントアウトして#define FS_SD_MMCのコメントアウトを外して有効にした。

ファイルの更新日時用に用意されてるNTPの設定を日本に合わせて変更する。

const char* ntpServer = "ntp.nict.jp";
const long gmtOffset_sec = 3600 * 9;
const int daylightOffset_sec = 0;
struct tm timeinfo;

45~48行目あたりにある設定をこんな感じに変更した。

あとはssidとpasswordを設定してとりあえず書き込み。

‎TGTF016GWA

microSDカードはTeamの16GBを使用した。SDメモリカードフォーマッターでフォーマット済み。SD_MMCライブラリは今のところexFAT非対応なので64GB以上のSDXCには対応していない。でも試しに64GBのmicroSDをFAT32でフォーマットしてみたら使えるようになった。そこまで容量いらないと思うけど…

あとはエクスプローラから"ftp://ESP32のIP"にアクセスするとFTPフォルダエラーが出た。とりあえずOKを押して

エクスプローラの白いところを右クリックしてログイン方法を押してユーザー名とパスワードにesp32と入れてログオンするとフォルダが表示される。(ftp://esp32:esp32@192.168.11.86/のようにユーザー名とパスワードをアドレスに含めてしまってても行ける。)ESP32-CAMの場合フラッシュのLEDにSDMMCで使われてるピンが接続されているため、アクセスするたびにフラッシュが点滅する。

ファイル転送スピードは書き込みは500kb/sぐらいで読み込みが1MB/sぐらい出ていた。文字コードの問題があるみたいで、ファイル名に結構制約がある。試しにFileZillaで文字コードをUTF-8固定にすると日本語のファイル名とかまで行けたのでWindows標準のエクスプローラだとエンコードがうまく設定できてないのかな?

  //
  //  OPTS
  //
  else if (!strcmp (command, "OPTS")) {
    for (uint8_t i=0; i <= strlen (parameters); i++){
      if(parameters[i] > 0x40){
	parameters[i] = parameters[i] & ~(0x20);
      }
    }
    if (!strcmp (parameters, "UTF8 ON")) {
      client.println ("200 Always in UTF8 mode.");
      //client.println ("451 Unable to accept OPTS UTF8");
    }
    else{
      client.println ("500 Unknown command " + String (parameters));
    }
  }

ということでESPFtpServer.cppを少し修正して"OPTS UTF8 ON"に反応するように変更してみた。807行目あたりにこんな感じでOPTSコマンドへの応答を返すように変更して、Windows 10のエクスプローラからアクセスしてみた。
しかしやっぱりWindowsのエクスプローラ内蔵のFTPクライアントが悪いのか、日本語ファイル名が化ける…他のツールならUTF-8モードにすれば何でも行けるんだけどなぁ…

Windows標準のFTPクライアントの仕様を見ているとどうやらLISTコマンドがMS-DOSスタイルにしか対応していないようで、それですべてのファイルがショートカットみたいに見えるようになることがあるらしい。

        while (file) {
          String fname = file.name();
          time_t ftime = file.getLastWrite();
          struct tm* ptm = localtime(&ftime);

          int pos = fname.lastIndexOf("/");
          if (pos >= 0) fname.remove(0, pos + 1);

          char dateStr[9];
          char timeStr[8];
          int hour = ptm->tm_hour;
          const char* ampm = "AM";
          if (hour >= 12) {
            ampm = "PM";
            if (hour > 12) hour -= 12;
          }
          if (hour == 0) hour = 12;
          sprintf(dateStr, "%02d-%02d-%02d", ptm->tm_mon + 1, ptm->tm_mday, (ptm->tm_year + 1900) % 100);
          sprintf(timeStr, "%02d:%02d%s", hour, ptm->tm_min, ampm);
          if (file.isDirectory()) {
            sprintf(buffer, "%s  %s    <DIR>          %s", dateStr, timeStr, fname.c_str());
          } else {
            sprintf(buffer, "%s  %s %12lu %s", dateStr, timeStr, (unsigned long)file.size(), fname.c_str());
          }
          data.println (buffer);
          nm ++;
          file = dir.openNextFile ();
        }

ESPFtpServer.cppのLISTコマンドのESP32用の部分をごっそりUNIXスタイルからMS-DOSスタイルに書き換えてみた。するとエクスプローラーでもちゃんとファイルが見える!ちゃんとフォルダ内のファイルも確認できた。文字コードの問題じゃなかったのか…

色々確認しているときにftpコマンドで接続してみたんだけど、ftpコマンドでopenまではいいんだけど、ユーザー名を入れた瞬間に切断される…
おそらくこのせいでエクスプローラから一発目アクセスしたときにFTPフォルダエラーが出たのかな?ソースを確認してみるとuserIdentity()の戻り値を待っているところでユーザー名が入っていなかったりするとcmdStatus=0で切断されるっぽい。

    if (cmdStatus == 3) {            // Ftp server waiting for user identity
      if (userIdentity ()) {
        cmdStatus = 4;
      }
      else {
        cmdStatus = 3;
      }
    }

とりあえずこんな感じでESPFtpServer.cppの100行目付近をいじってcmdStatus=3に変更してみた。これでftpコマンドでもうまく接続できるようになった。これでも何回かミスってるとタイムアウトのほうが効いてくるので問題ないと思う。

ついでにanonymousログインも追加してみた。

  if (_FTP_USER == "anonymous"){
    client.println ("230 Anonymous login successful");
    return true;
  }

これをESPFtpServer.cppの中のboolean FtpServer::userPassword ()のすぐ下に差し込むと、スケッチでuser名をanonymousにしておけばパスワードは何でも通ってしまうというアレ。

最初にSoftAPモードでテストしていたときにデータ転送できなかった謎も解けた。これはPASVモードのときにサーバのIPアドレスが0.0.0.0になってしまうためだった。300行目ぐらいにある、

  	dataIp = WiFi.localIP ();

と、なっている箇所を

        if (WiFi.getMode() == WIFI_MODE_AP) {
	  dataIp = WiFi.softAPIP();
	}else{
  	  dataIp = WiFi.localIP();
  	}	

のように修正した。SoftAPのときはlocalIP()じゃなくてsotAPIPじゃないとサーバ側のIPアドレスが0.0.0.0で報告してしまうためうまく動かなかった。これでSoftAPでもSTAモードでも動作するはず。SoftAPのときはNTPからRTCを更新できないので、Webサーバも一緒に立てて先日の技が使えるかも。

ちなみにサンプルのままでも便利だけど、IPがわかりにくいのでmDNSを追加しておくと便利かも。

他のESP32用のFTPサーバはスケッチ本体だったりしていたのでライブラリになっていることによりほかのスケッチと統合が楽になって扱いやすいかも。これでESP32-CAMでタイムラプスを撮ってそれをFTPでダウンロードができそう。

2022年2月6日日曜日

ESP32のRTCをオフラインでブラウザから設定する。

 ESP32をSoftAPとかでオフラインで使用する場合に内蔵のRTCの時間を簡単に設定できないか試してみた。IoT系マイコンなのでネットに接続されていてNTPから時刻を取得するのは簡単なんだけど、オフラインの場合は外付けRTCを接続して電池で保持するのが定番なのかな。

その昔WebページにJavascriptでリアルタイム時刻を表示していた時代も有ったなーというところからの発想を得て、パソコンやスマホからESP32にブラウザを使ってアクセスしてJavascriptから時刻をESP32に送ってやれないかなと。

ということで早速作ってみた。ページを開くたびに問答無用で時間セットするサンプル。

#include <WiFi.h>
#include <WebServer.h>
#include "time.h"
#include "esp_mac.h"  //MACアドレス取得用

WebServer server(80);

const char INDEX_HTML[] =
  "<!DOCTYPE HTML>"
  "<html>"
  "<head>"
  "<link rel=\"icon\" href=\"data:,\">"
  "<title>Timeset</title>"
  "</head>"
  "<script type=\"text/javascript\">\n"
  "var xhr = new XMLHttpRequest();\n"
  "xhr.open('POST', './settime', true);\n"
  "xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');\n"
  "const dtime = new Date().getTime() / 1000.0;\n"
  "xhr.send(\"time=\" + dtime);\n"
  "window.onload = function(){\n"
  "document.getElementById(\"textbox\").value = dtime;\n"
  "}\n"
  "</script>\n"
  "<body>"
  "<p>Unix Time</p><br>"
  "<input type=\"text\" id=\"textbox\">"
  "</body>"
  "</html>";

void setup() {
  uint8_t macaddr[6];
  esp_read_mac(macaddr, ESP_MAC_WIFI_SOFTAP);  //WiFi Macアドレス読み込み
  char SSID[14];
  sprintf(SSID, "ESP32-%02X%02X", macaddr[4], macaddr[5]);
  WiFi.softAP(SSID);  //SoftAPを起動
  delay(100);

  server.enableCORS();  //CORS対策 (JavaScriptデバッグ用)
  server.on("/", []() {
    server.send(200, "text/html", INDEX_HTML);
  });
  server.on("/settime", []() {
    if (server.args() > 0) {
      timeval epoch = { server.arg(0).toInt(), 0 };//構造体に格納
      settimeofday(&epoch, NULL);//RTCに時間をセット
    }
    server.send(204);
  });
  server.begin();
  setenv("TZ", "JST-9", 1);  //日本標準時に設定

  xTaskCreatePinnedToCore(task, "task", 4 * 1024, NULL, 5, NULL, 0);  //別タスク起動
}

void loop() {
  server.handleClient();
}

void task(void *pvParameters) {
  Serial.begin(115200);
  while (1) {
    static unsigned long lastUpdate = 0;
    if (lastUpdate + 1000 <= millis()) {
      lastUpdate = millis();  //次の実行のために時間更新//ms周期
      struct tm timeinfo;
      if (!getLocalTime(&timeinfo)) {
        Serial.println("Failed to obtain time");
      } else {
        Serial.println(&timeinfo, "%A, %B %d %Y %H:%M:%S");
      }
    }
    delay(1);  //WDT対策
  }
  vTaskDelete(NULL);
}

ESP32でSoftAPでアクセスポイントを作ったらWebサーバーを立てて、それにスマホやPCのブラウザからアクセスするとシステム時刻をJavaScriptで取得してESP32にPOSTで投げるという荒業。もちろん2038年問題対策はされていない。

getLocalTimeがfalseの場合、処理に時間がかかるのでWebサーバーをブロッキングしないように無駄にマルチタスクでシリアルモニタを実装。

ページを読み込んだ瞬間にJavaScriptから時間を取得してPOSTで投げるのでロードにかかった分とかは時間がずれる。でもシリアルコンソールの時間表示と比べてみる限り1秒もずれていなかった。PC版ChromeとiOSでは動作確認できた。

外付けRTCで電池の心配しなくても良いので(ESP32の内蔵RTCがどのぐらいでずれていくのかわからないけど)簡易的な用途だったらこれで十分かも。ちなみに内蔵RTCはそこそこ制度が悪いので日をまたいだりするとかなりずれる。そんなときは外付けクリスタルにも対応しているらしいので精度が必要であれば32kHzのクリスタルを外付けすれば良さそう。