M5Stack FTP Server (SD用)プログラミングメモ


表題通りM5StackにFTP Serverを立てた件のメモです。接続先はM5Stackに搭載されたSDカードです。これができて何が嬉しいかというと、M5Stack上のSDカードにFTPを通してデータの出し入れができるようになります。いちいちSDカードをM5Stackから引き抜いて、パソコンに挿してデータ編集したらM5Stackに入れ直す、という手間が省けます。
そういう話だったら、ユーザビリティ的にはSMB3とかのプロトコルでファイル共有したほうがいいという話ではあるのですが、あいにくSMBのサンプルが見つかりませんでした。
クライアントはMacOS 10.13.6 High Sierra上で、Cyberduck 7.8.0(https://cyberduck.io) を使いました。それ以外の環境は考慮していません。FTPの動作としても、接続、ファイル一覧の取得、アップロード、ダウンロードの動作程度です。特にディレクトリ関連については全く考慮していません。階層がないものとして動作します。

(FTPのRFCを全て読んだわけではないですし、この記事には間違いがあるかもしれません。思うところがありましたら、コメント等でお手柔らかにご指摘いただけると、嬉しいです。「ちゃんとしたものを作ろう」というよりは、「とにかく動くものを作ろう」という方向性で作りましたので、ある意味いい加減な記事であることはご了承ください)


早速ですが、メインのスケッチは以下の通りです。

#include <M5Stack.h>
#include <WiFi.h>
#include <WiFiClient.h>
#include "ESP32FtpServer.h"

const char* ssid = "";
const char* password = "";

//set #define FTP_DEBUG in ESP32FtpServer.h to see ftp verbose on serial
FtpServer ftpSrv;   

void setup(void) {

  M5.begin();
  Serial.begin(115200);
  WiFi.begin(ssid, password);

  // Wait for connection
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("Connected to " + String(ssid));
  Serial.println("IP address: " + WiFi.localIP().toString());

  /////FTP Setup, ensure SD is started before ftp;  /////////
  // username, password for ftp.  
  // set ports in ESP32FtpServer.h  (default 21, 50009 for PASV)
  ftpSrv.begin("esp32", "esp32");   

}

void loop(void) {
  //make sure in loop you call handleFTP()!!
  ftpSrv.handleFTP();        
}

簡単ですね。Wifiに接続したら、ftpSrv.begin()で、サービスを開始して、loop()の中でftpSrv.handleFTP()を呼んでいるだけです。一応注意点としては、ftpを開始する前にSD.begin()を呼ぶ必要があります。これは、M5.begin()の中で行われているので、通常は不要です。M5以外のesp32では必要ですね。

メインのスケッチの他に
 ESP32FtpServer.h
 ESP32FtpServer.cpp
が必要なのですが、この二つのファイルは
https://github.com/lovyan03/M5Stack_LovyanLauncher
から持ってきました。(loveyan03さん、robo8080さん、有難うございます)

さて、本題はここからです

最初に書いた通り、MacOS 10.13.6 High Sierra上で、Cyberduck 7.8.0(https://cyberduck.io) というFTPクライアントを使いました。が、このCyberduckというクライアントが結構クセがあるソフトで、これ以降はCyberduckに特化した話がほとんどです。以降のソースはESP32FtpServer.cppを修正した箇所です。


  • 次の修正は完全に付け焼き刃的処理で、お勧めもできないのですが、元ソースではFEATコマンドがサポートされてないため、上手く動作しません。しかも、CyberduckはFEATコマンドをUSERコマンドの前に送信します。このため、その場凌ぎの処理を加えましたが、本当は状態遷移自体を見直すべきと思います。内容も"UTF8"とか書いてますが、ハッキリ言って適当で、とりあえず通るようにしただけです。ごめんなさい。
ESP32FtpServer.cpp
    if ( cmdStatus == 3 ) {          // Ftp server waiting for user identity
+      ///////////
+      // 付け焼き刃的処理、 USER/PASSの設定前にFEATコマンドが来ることに備える。
+      // 内容も以下でよいのか不明。
+      if ( strcmp( command, "FEAT" ) == 0) {
+        client.println( "211- Extensions supported");
+        client.println( "UTF8");
+        client.println( "211 End");
+        millisEndConnection = millis() + millisTimeOut;
+      } else
+      ///////////
        if ( userIdentity() )
          cmdStatus = 4;
        else
          cmdStatus = 0;
  • LISTコマンド中の処理です。これはちょっと必要かどうかハッキリしないのですが、totalを出力するようにしました。totalはディレクトリ中の総ブロック数のことで、512バイトごとに1ブロックという考え方だそうです。ちなみに、ここでコマンドというのはFTPのプロトコル上のことで、コマンドラインのFTPだと"ls"と打つのが一般的ですが、内部的には"LIST"という文字列をFTPサーバに送っています。
ESP32FtpServer.cpp
  else if ( ! strcmp( command, "LIST" ))
  {
// (中略)
      if ((!dir) || (!dir.isDirectory()))
        client.println( "550 Can't open directory " + String(cwdName) );
      else
      {
+        {
+          File file = dir.openNextFile();
+          uint32_t block = 0;
+          while (file) {
+            block += file.size() / 512 + ((file.size() % 512) > 0);
+            file = dir.openNextFile();
+          }
+          dir.rewindDirectory();
+          data.println( "total " + String(block) );
+        }
        File file = dir.openNextFile();
  • これもLISTコマンドの処理ですが、まず、ディレクトリ処理をバッサリ削除してしまいます。とりあえず、ディレクトリに関する処理はしない、ということで。(積極的な理由をいうと、GUIクライアント上からダブルクリックでディレクトリ遷移されたりすると危険なので)それと、出力書式をUNIXの標準的なものに変更しました。この書式にしないとCyberduckに受け取ってもらえませんでした。LISTコマンドの書式はRFC765/RFC959によると、人間が理解できればそれでいいというもので、本来はGUIツールを意識したものではなかったようです。日付時刻に関しても、本来のFTPにはそのような取り決めはないようですが、GUIクライアントはLISTコマンドの結果から「良きに計らって」処理することで、日付時刻を表示しているようです。今回は元日午前零時に決め打ちしています。
ESP32FtpServer.cpp
          fs = String(file.size());
          if (file.isDirectory()) {
-            data.println( "01-01-2000  00:00AM <DIR> " + fn);
+            // ディレクトリは未サポート
          } else {
-            data.println( "01-01-2000  00:00AM " + fs + " " + fn);
+            data.printf( "-r-------- 1 user group %14d Jan 01 00:00 %s\n", file.size(), fn.c_str());
            //            data.println( fn);
            //          data.println( " " + fn );
          }
  • Cyberduckではひとつの動作ごとにいちいち接続し直して、いちいちUSER, PASSの設定などをしています。このため、状態遷移のリセットが必要です。結果としてはたった1行の追加ですが、この修正はどう行うべきか結構考えました。
ESP32FtpServer.cpp
void FtpServer::handleFTP()
//(中略)
  if (ftpServer.hasClient()) {
 //   if (ftpServer.available()) {
      client.stop();
      client = ftpServer.available();
+      cmdStatus = 1; // CYBERDUCKクライアントではコマンド毎にUSER/PASS認証が必要なため、リセットする。
  }

言い忘れましたが、ここまでやった上でCyberduckをパッシブモードにしないと動作しません。アクティブモードだとLISTコマンドでなく、MLSDコマンドを使うから、だったような気がします。

まとめ

FTPというのはかなり古いプロトコルで、当初はこんなに多く使われるとは想定していなかったのか、いろいろ問題があり、今は使われなくなりつつあるそうです。大まかにいうとセキュリティの件と、動作の互換性、安定性の問題かと思われます。

それにしても、ひとつのクライアントソフトに対応させるだけでこれだけ変更が必要というのはいかがなものか? どうしてこうなるのだろうか? クライアントソフトの問題なのか? FTPの仕様の問題なのか? 元ソースの問題なのか?(勝手に取ってきて、勝手に改変したのに失礼な物言いで申し訳ありません)

ちなみにMacのファインダーに「サーバへ接続」というメニューがあって、そこからFTPに接続できるように見えますが、他のFTPサーバ(QuickFTP)で試してたところ、箸にも棒にもかかりませんでした。SFTPとかそっちのプロトコル用かもしれません。

あと、Cyberduckというのはこんな奴です。

https://cdn.cyberduck.io/img/cyberduck-icon-384.png
(どこらへんがCyberなんでしょうね)