Arduinoのネットワーク通信

FTPファイル転送


FTPによるファイル転送では、Socket通信とファイルシステムを両方使う必要があります。
Arduinoでファイルシステムを使う為には、SDライブラリやSdFatライブラリを使いますが、
メモリの小さいUNOやNANOではメモリ不足で動きません。
そこで、ATMEGA2560を使ったFTPファイル転送を紹介します。

ファイルシステムを使う為のライブラリとしてArduinoCoreに含まれているSDライブラリはハードウェアSPIを使います。
W5100やENC28J60のイーサネットライブラリもハードウェアSPIを使うので、Socket通信とファイルシステムを同時に使うために は
SPIチップセレクトの切り替えが頻繁に発生し非常に面倒です。

そこで、ファイルシステム側はソフトウエアSPIを利用し、イーサネットライブラリとSDライブラリで使用するピンを完全に分離する事にします。
これで何も考えずにSocket通信とファイルシステムを両方同時に使う事ができます。
ソフトウエアSPIで使えるファイルシステムにはこれらのライブラリが有ります。(探せばもっとあるかもしれません)

@https://github.com/adafruit/SD
Ahttps://github.com/jbeynon/sdfatlib
Bhttps://github.com/greiman/SdFat

今回はAのSdFatライブラリを使いました。
@やBのライブラリでも動くかもしれません。

最初に「SdFatConfig.h」を一部変更し、ソフトウェアSPIで使えるように変更します。

//#define USE_ARDUINO_SPI_LIBRARY 1

#define USE_ARDUINO_SPI_LIBRARY 0

//#define USE_SOFTWARE_SPI 0

#define USE_SOFTWARE_SPI 1

//uint8_t const SOFT_SPI_CS_PIN = 10;

uint8_t const SOFT_SPI_CS_PIN = 2;
 
//uint8_t const SOFT_SPI_MOSI_PIN = 11;

uint8_t const SOFT_SPI_MOSI_PIN = 3;

//uint8_t const SOFT_SPI_MISO_PIN = 12;

uint8_t const SOFT_SPI_MISO_PIN = 4;

//uint8_t const SOFT_SPI_SCK_PIN = 13;

uint8_t const SOFT_SPI_SCK_PIN = 5;

これでディジタルピン#2,3,4,5を使ってファイルシステムを利用することができます。
これらのピンは好きなピンに変更することができます。



FTPサーバーはこち らのArduinoのコードと、こちらの ESP8266のコードを参考にしました。
FTPサーバーを使う場合、ArduinoCoreに含まれているEthnernetライブラリを一部修正する必要があります。
どうやらFTPクライアントからの接続方法が標準とは違うようです。

C:\Program Files\Arduino\libraries\Ethernet\src\EthernetServer.h
以下の1行を適当な場所に追加します。
ifndef ethernetserver_h
#define ethernetserver_h

#include "Server.h"

class EthernetClient;

class EthernetServer :
public Server {
private:
  uint16_t _port;
  void accept();
public:
  EthernetServer(uint16_t);
  EthernetClient available();
  EthernetClient connected(); // Add
  virtual void begin();
  virtual size_t write(uint8_t);
  virtual size_t write(const uint8_t *buf, size_t size);
  using Print::write;
};

#endif

C:\Program Files\Arduino\libraries\Ethernet\src\EthernetServer.cpp
以下を適当な場所に追加します。
EthernetClient EthernetServer::connected()
{
 accept();
 for( int sock = 0; sock < MAX_SOCK_NUM; sock++ )
   if( EthernetClass::_server_port[sock] == _port )
   {
     EthernetClient client(sock);
     if( client.status() == SnSR::ESTABLISHED ||
         client.status() == SnSR::CLOSE_WAIT )
       return client;
   }
 return EthernetClient(MAX_SOCK_NUM);
}

私はaccep()とavailable()の間に追加しました。
void EthernetServer::accept()
{
  int listening = 0;

  for (int sock = 0; sock < MAX_SOCK_NUM; sock++) {
    EthernetClient client(sock);

    if (EthernetClass::_server_port[sock] == _port) {
      if (client.status() == SnSR::LISTEN) {
        listening = 1;
      }
      else if (client.status() == SnSR::CLOSE_WAIT && !client.available()) {
        client.stop();
      }
    }
  }

  if (!listening) {
    begin();
  }
}


EthernetClient EthernetServer::connected()
{
 accept();
 for( int sock = 0; sock < MAX_SOCK_NUM; sock++ )
   if( EthernetClass::_server_port[sock] == _port )
   {
     EthernetClient client(sock);
     if( client.status() == SnSR::ESTABLISHED ||
         client.status() == SnSR::CLOSE_WAIT )
       return client;
   }
 return EthernetClient(MAX_SOCK_NUM);
}



EthernetClient EthernetServer::available()
{
  accept();

  for (int sock = 0; sock < MAX_SOCK_NUM; sock++) {
    EthernetClient client(sock);
    if (EthernetClass::_server_port[sock] == _port) {
      uint8_t s = client.status();
      if (s == SnSR::ESTABLISHED || s == SnSR::CLOSE_WAIT) {
        if (client.available()) {
          // XXX: don't always pick the lowest numbered socket.
          return client;
        }
      }
    }
  }

  return EthernetClient(MAX_SOCK_NUM);
}



ATMEGA2560とW5100との接続は以下の様になります。
W5100ではクライアント/サーバーともに動きますが、ENC28J60(UIPEthernet)ではクライアント/サーバーともに動きませ ん。
おそらくFTPのSocket通信に対応していないのでしょう。
W5100 SDカード ATMEGA2560
SS
Pin #10
MISO
Pin #50
MOSI
Pin #51
SCK
Pin #52
VCC
5V
GND
GND

CS Pin #2

MOSI Pin #3

MISO Pin #4

SCK Pin #5

VCC 5V

GND GND



SdFatライブラリの動作を確認するためのスケッチは以下の様になります。
Fileオブジェクトを使うとファイル操作が簡単にできます。
/*
 SdFat Test

 SdFatConfig.hの以下の部分を変更する
 
 //#define USE_ARDUINO_SPI_LIBRARY 1
 #define USE_ARDUINO_SPI_LIBRARY 0

 //#define USE_SOFTWARE_SPI 0
 #define USE_SOFTWARE_SPI 1
 //uint8_t const SOFT_SPI_CS_PIN = 10;
 uint8_t const SOFT_SPI_CS_PIN = 2;
 //uint8_t const SOFT_SPI_MOSI_PIN = 11;
 uint8_t const SOFT_SPI_MOSI_PIN = 3;
 //uint8_t const SOFT_SPI_MISO_PIN = 12;
 uint8_t const SOFT_SPI_MISO_PIN = 4;
 //uint8_t const SOFT_SPI_SCK_PIN = 13;
 uint8_t const SOFT_SPI_SCK_PIN = 5;

 */
#include <SdFat.h> // https://github.com/jbeynon/sdfatlib

// SD chip select pin
const uint8_t chipSelect = 2;

SdFat sd;
SdFile file;

void setup() {
  Serial.begin(9600);
  while (!Serial) {}  // wait for Leonardo

  if (!sd.begin(chipSelect, SPI_HALF_SPEED)) {
    Serial.println("begin failed");
    return;
  }
  sd.remove("FILE1.TXT");
  sd.remove("FILE2.TXT");

}
//------------------------------------------------------------------------------
void loop() {
  int incomingByte;

  Serial.println("1:Create File");
  Serial.println("2:Remove File");
  Serial.println("3:Append File");
  Serial.println("4:Write File");
  Serial.println("5:Rename File");
  Serial.println("6:Read File");
  Serial.println("A:Make DIR");
  Serial.println("B:Remove DIR");
  Serial.println("C:Change DIR to SUBDIR");
  Serial.println("D:Change DIR to ROOT");

  while (!Serial.available()) {}
  incomingByte = Serial.read();
  Serial.print("I received: ");
  Serial.println(incomingByte, HEX);

  if (incomingByte == 0x31) {
    if (!sd.exists("FILE1.TXT")) {
//      file.open("FILE1.TXT", O_WRITE | O_CREAT | O_AT_END);
      file.open("FILE1.TXT", O_WRITE | O_CREAT | O_APPEND);
      Serial.print("file.isOpen=");
      Serial.println(file.isOpen());
      file.close();
    } else {
      Serial.println("**FILE1.TXT IS EXIST**");
    }
  } else if (incomingByte == 0x32) {
    if (sd.exists("FILE1.TXT")) {
      sd.remove("FILE1.TXT");
    } else {
      Serial.println("**FILE1.TXT NOT EXIST**");
    }
  } else if (incomingByte == 0x33) {
    if (sd.exists("FILE1.TXT")) {
      file.open("FILE1.TXT", O_WRITE | O_APPEND);
      Serial.print("file.isOpen=");
      Serial.println(file.isOpen());
      file.println("Hello");
      Serial.print("getWriteError=");
      Serial.println(file.getWriteError());
      file.close();
    } else {
      Serial.println("**FILE1.TXT NOT EXIST**");
    }
  } else if (incomingByte == 0x34) {
    if (sd.exists("FILE1.TXT")) {
      file.open("FILE1.TXT", O_WRITE | O_APPEND);
      file.write('H');
      file.write('E');
      file.write('L');
      file.write('L');
      file.write('O');
      file.write(0x0d);
      file.write(0x0a);
      Serial.print("getWriteError=");
      Serial.println(file.getWriteError());
      file.close();
    } else {
      Serial.println("**FILE1.TXT NOT EXIST**");
    }
  } else if (incomingByte == 0x35) {
    if (sd.exists("FILE1.TXT")) {
      file.open("FILE1.TXT", O_READ);
      file.rename(sd.vwd(), "FILE2.TXT");
      file.close();      
    } else {
      Serial.println("**FILE1.TXT NOT EXIST**");
    }
  } else if (incomingByte == 0x36) {
    if (sd.exists("FILE1.TXT")) {
      file.open("FILE1.TXT", O_READ);
      Serial.println("fileSize=" + String(file.fileSize()) + "byte");
      int line = 0;
      int crlf = 1;
      char c;
      while(file.available())
      {
        if(crlf) {
          line++;
          Serial.print("Line#:" + String(line));
          crlf = 0;
        }
        c=file.read();
        Serial.write(c);
        if (c == 0x0a) crlf = 1;
      }
      file.close();      
    } else {
      Serial.println("**FILE1.TXT NOT EXIST**");
    }
  } else if (incomingByte == 0x41) {
    if (!sd.exists("SUBDIR/")) {
      sd.mkdir("SUBDIR",1);
    } else {
      Serial.println("**SUBDIR EXIST**");
    }
  } else if (incomingByte == 0x42) {
    if (sd.exists("SUBDIR/")) {
      sd.rmdir("SUBDIR");
    } else {
      Serial.println("**SUBDIR NOT EXIST**");
    }
  } else if (incomingByte == 0x43) {
    if (sd.exists("SUBDIR/")) {
      sd.chdir("SUBDIR",1);
    } else {
      Serial.println("**SUBDIR NOT EXIST**");
    }
  } else if (incomingByte == 0x44) {
      sd.chdir("/",1);
  }
  Serial.println("*****************************************");
  sd.ls(LS_DATE | LS_SIZE | LS_R);
  Serial.println("*****************************************");
  Serial.println("Done");
  Serial.read();

}

実行するとこのようになります。




FTPサーバー、FTPクライアントのコードはこちらで 公開しています。
FTPサーバーのコードは複数のファイルに分かれているので、フォルダー毎、コピーしてください。

FTPサーバーはアクティブ/パッシブの両方に対応しています。
ASCII転送モードは有ません。必ずBINARY転送モードになります。
送信の転送レートは10kB/Secと目茶苦茶低いです。
調べてみましたが、SdFatライブラリをSoftwareSPIで使っているのが影響しています。




受信の転送レートは少しだけ早いです。


下はOrangePiに同じファイルを転送した場合です。


ffftpやWinSCPからも操作することができます。
右側のペインはArduino側のファイルシステムを表示しています。




ファイルを受信したり、ファイルが削除された時に、任意のCalklBack関数を呼び出す機能を追加しました。
以下の関数で任意のCallBackを設定することができます。
void setCallBackDelete(void (*functionPointer)(void));  
void setCallBackStor(void (*functionPointer)(void));  
void setCallBackRmdir(void (*functionPointer)(void));  
void setCallBackMkdir(void (*functionPointer)(void));  
void setCallBackRename(void (*functionPointer)(void));  

これで、FTPサーバー内部で何が起こったか、Loop処理の中で知ることができます。
Setup関数の中で以下のコードを書くと、それぞれDelete Rmdir Stor Mkdir Renameが発生した時に
CallBack関数が呼び出され、Deleteされたファイル名などを知ることができます。
これが今回FTPサーバーを開発した最大の目的です。
void CallBackDelete(const char *fileName) {
  Serial.println("[CallBack] DeleteFile=" + String(fileName));
}

void CallBackStor(const char *fileName) {
  Serial.println("[CallBack] StoreFile=" + String(fileName));
}

void CallBackRmdir(const char *dirName) {
  Serial.println("[CallBack] RmDir=" + String(dirName));
}

void CallBackMkdir(const char *dirName) {
  Serial.println("[CallBack] MkDir=" + String(dirName));
}

void CallBackRename(const char *fileName1, const char *fileName2) {
  Serial.println("[CallBack] Rename from=" + String(fileName1) + " to=" + String(fileName2));
}

void setup(void){

{中略}

  // Initialize the FTP server
  ftpSrv.begin(FTP_USER, FTP_PASS);
  // Set Callback Function for Delete
  ftpSrv.setCallBackDelete(CallBackDelete);
  // Set Callback Function for Rmdir
  ftpSrv.setCallBackRmdir(CallBackRmdir);
  // Set Callback Function for Stor
  ftpSrv.setCallBackStor(CallBackStor);
  // Set Callback Function for Mkdir
  ftpSrv.setCallBackMkdir(CallBackMkdir);
  // Set Callback Function for Mkdir
  ftpSrv.setCallBackRename(CallBackRename);

CallBack関数にはDeleteされたファイル名などが渡ります。


これで外部(FTP Client)からのFTPコマンドでATMEGAを操れるようになります。